FV-1 Block Developer Guide

This guide explains how to create new functional blocks for the FV-1 VS Code extension using the Assembly Template Language (ATL).

Overview

ATL allows you to define blocks declaratively using a combination of JSON metadata and a specialized assembly template. This approach replaces complex TypeScript implementations and makes it easier for developers to contribute new effects.

File Structure

An ATL block is a single .atl file containing:

  1. JSON Frontmatter: Metadata about the block (name, category, pins, parameters).

  2. Assembly Template: The FV-1 assembly code with dynamic token substitution and preprocessor macros.

Example structure:

---
{
  "type": "my.effect.id",
  "name": "My Effect",
  "category": "My Category",
  "pins": [...],
  "parameters": [...]
}
---
; Assembly code starts here
rdax ${input.in}, 1.0
...

JSON Metadata Reference

Core Properties

  • type: Unique identifier for the block (e.g., effects.filter.lpf).

  • name: Display name in the palette.

  • category: Grouping in the palette (e.g., Delay, Filter, Dynamics).

  • description: Tooltip text.

  • color: Hex color for the block header.

  • width: Preferred width in the editor (default is 180).

  • labelTemplate: An expression to dynamically generate the block’s label on the canvas based on its parameters or connectedness (e.g., ${param.mix * 100}%).

Pins (inputs and outputs)

Each pin is an object with:

  • id: Unique ID used in the assembly template.

  • name: Label shown on the block.

  • type: audio or control.

  • required: (Input only) If true, the block won’t generate code unless connected.

Parameters

Parameters define the block’s adjustable settings in the Property Panel:

  • id: Used as ${id} in the template.

  • name: Label in the UI.

  • type: number, select, or boolean.

  • default: Initial value.

  • min/max/step: Range for numbers.

  • conversion: (Optional) Automatically scales UI values to FV-1 coefficients.

    • LOGFREQ: 1-pole filter coefficient (Hz → linear).

    • SVFFREQ: 2-pole SVF coefficient (Hz → linear).

    • DBLEVEL: Decibel to linear gain (dB → linear).

    • LENGTHTOTIME: Time to samples (ms → samples).

    • SINLFOFREQ: LFO frequency coefficient.

Memory (memories)

Allocates delay line memory:

  • id: Used as ${mem.id} in the template.

  • size: Size in samples (can use a ${parameter_id} for dynamic sizing).

Registers (registers)

Allocates internal temporary registers:

  • registers: Array of strings. Use ${reg.id} in the template.

Assembly Template Features

Algebraic Syntax

ATL supports an algebraic assignment syntax that automatically translates into FV-1 assembly instructions at compile time, saving you from writing raw RDAX, WRAX, and SOF statements.

Supported operations include:

Assignment

@acc = POT0 translates to RDAX POT0, 1.0

Math/Scaling

@acc = ${reg.f1} * 0.5 translates to RDAX REG_f1, 0.5

Output Storage

${output.out} = @acc translates to WRAX OUT, 0.0

Accumulation

@acc += POT1 translates to RDAX POT1, 1.0

Bitwise Logic

@acc &= 0.9 translates to AND 0.9

Scale Off-Set (SOF)

@acc = @acc * 0.2 + 0.1 translates to SOF 0.2, 0.1

Filtering
  • lpf (Low Pass / WRAX)

  • hpf (High Pass / WRHX)

  • lpf_alt (Shelving / WRLX)

  • lpf_modulated (Envelope control / MULX)

Example: @acc = lpf(${reg.state}, POT0, 0.5) translates to RDFX REG_state, POT0 and WRAX REG_state, 0.5.

Note: If assigning to a register instead of @acc, the compiler automatically passes 0.0 as the scale to clear the accumulator.

Static Math Parsing

The compiler parses pure mathematical statements like multiplication, addition, and grouping parentheses before serializing to FV-1 instructions. For example, expressions composed only of parameter literals, such as @acc = @acc * (${param.max} - ${param.min}) + ${param.min}, are algebraically folded into single floats at compile time safely handling Scale Offsets (SOF).

Note

Algebraic syntax must be cleanly formatted. If the compiler encounters a malformed math operation (e.g., POT0 + * 2), it will generate a compile-time syntax error displayed directly in the VS Code Problems pane.

Compiler Optimizations

The ATL compiler doesn’t just blindly concatenate assembly; it also runs a multi-stage optimization pass to produce highly efficient FV-1 code:

  • Algebraic Folding: Static mathematical expressions are resolved into single floats during compilation.

  • Dead Output Elimination: If an output pin is left unconnected in the block diagram, the compiler automatically skips generating the assembly for calculating and writing that output (when safely possible), preserving DSP cycles.

  • Accumulator Forwarding: Redundant write-then-read steps (e.g., WRAX reg, 0.0 immediately followed by RDAX reg, 1.0) are collapsed into a single WRAX reg, 1.0 instruction to conserve instruction slots.

  • Move Pruning: Trims redundant register moves (such as unnecessary WRAXLDAX sequences).

  • Auto-Clearing: Automatically injects CLR instructions when leaving input stages if the accumulator is left dirty, ensuring no bleed-over between unconnected audio blocks.

Token Substitution

Tokens are replaced at compile time with resolved register names, memory addresses, or parameter values:

  • ${input.pin_id}: The register containing the value for that input.

  • ${output.pin_id}: The register where the output should be written.

  • ${reg.reg_id}: An internal register name.

  • ${mem.mem_id}: A memory address.

  • ${parameter_id}: The resolved/converted value of a parameter.

Preprocessor Macros

Conditional Logic

  • @if / @else / @endif blocks allow for flexible template generation based on active input states or parameters.

Pin Connections

@if pinConnected(pin_id) skips code if the specified pin identifier is unconnected in the visual schematic.

Operations & Logic

@if param_id >= 10.0 enables testing properties explicitly natively. Numeric tests are strictly equivalent matching, falling back safely to strings where variables like "invert" are tested against boolean primitives (e.g., @if ${param.invert} == true).

Compound Checks

@if naturally handles chained logical && (AND) and || (OR) separators (e.g., @if ${param.x} == 1 || ${param.y} != 0).

Compile-Time Assertions

Use @assert condition, "Error Message" to forcibly abort compilation and bubble an error message into VS Code if a user configures a block incorrectly (e.g., @assert ${param.max} > ${param.min}, "Max must be greater than Min!").

Calculations & Special Macros

Use these to pre-calculate constants for your assembly code or invoke built-in hardware behaviors dynamically:

  • @equals result, value: Evaluate expression and assign to variable identifier.

  • @multiplydouble result, a, b / @dividedouble / @plusdouble / @minusdouble: Math evaluated statically against parameters.

Code Sections

Organize code into separate sections:

  • @section header: Definitions and constants.

  • @section init: Code that runs only once (e.g., WLDS).

  • @section main: The main per-sample processing (default).

Custom Blocks

The extension allows you to develop and use your own custom blocks alongside the built-in library. This is the fastest way to extend the system with your own DSP algorithms.

Copying Existing Blocks

The easiest way to start writing a new block is to use an existing one as a template:

  1. Open a block diagram (.spndiagram).

  2. Right-click any block on the canvas.

  3. Select “Copy ATL Source” from the context menu.

  4. The raw ATL (metadata and assembly) is now in your clipboard. You can paste this into a new .atl file and modify it.

Custom Block Paths

To make your own blocks appear in the editor’s palette:

  1. Create a directory on your computer to store your .atl files.

  2. Open VS Code Settings (Ctrl+,).

  3. Search for fv1.customBlockPaths.

  4. Add the absolute path to your custom blocks directory. You can add multiple paths if needed.

Note

The extension searches these directories recursively for any file ending in .atl.

Refreshing the Registry

After you’ve added new .atl files or modified existing ones in your custom paths, you need to tell the extension to reload them:

  1. Open the Command Palette (Ctrl+Shift+P).

  2. Run the command “FV-1: Refresh Custom Blocks”.

  3. The block registry will be reloaded, and any open block diagrams will immediately update to show the new or changed blocks in the palette.

Tips and Tricks

  1. Safety First: Use rdax ${input.in}, 1.0 instead of ldax if you want to sum multiple connections to the same pin (though the extension handles summing for you, it’s a good practice).

  2. Clipping: FV-1 arithmetic is 1.14 fixed point. Be careful of overflows when summing signals.

  3. Optimization: Use @if pinConnected to avoid generating assembly for unused outputs or optional modulation inputs.

  4. Enforce Parameters: Extensively use @assert blocks at the top of routines to explicitly warn users of bad parameter states inside the VS Code Problems UI.

  5. Debugging: Check the compiled .spn file side-by-side with your diagram to see exactly what ATL code was generated and how tokens were replaced.

  6. Writing New Blocks: Creating a block follows a simple lifecycle:

    • Define the required nodes inside its JSON frontmatter to inform the palette

    • Configure the internal math algebraically

    • Handle user boundary configuration with @if/@assert

    • Connect its visual nodes functionally with ${input.foo} tokens

    • Validate logic changes frequently via npm test

Example: Creating a Simple Gain Block

Here’s a minimal example to get you started:

---
{
  "type": "gain.simple",
  "name": "Volume",
  "category": "Gain/Mixing",
  "description": "Simple gain adjustment",
  "color": "#FF6B6B",
  "inputs": [
    {"id": "in", "name": "In", "type": "audio", "required": true}
  ],
  "outputs": [
    {"id": "out", "name": "Out", "type": "audio"}
  ],
  "parameters": [
    {
      "id": "gain",
      "name": "Gain",
      "type": "number",
      "default": 1.0,
      "min": 0.0,
      "max": 2.0,
      "step": 0.01,
      "conversion": "DBLEVEL"
    }
  ],
  "registers": ["acc"]
}
---
@acc = ${input.in} * ${param.gain}
${output.out} = @acc

Contributing Back

Found a useful block or optimization? Consider contributing it back to the project:

  1. Add your .atl block to the appropriate folder in resources/blocks/

  2. Add test cases in test/blocks/ if applicable

  3. Run npm test to validate

  4. Submit a pull request on GitHub

See the Frequently Asked Questions for more information about contributing custom blocks.