{
  "$type": "at.atmosynth.module",
  "descriptionVersion": 1,
  "moduleId": "filter-ladder",
  "versionKey": "2",
  "name": "Ladder filter",
  "description": "A low-pass that sings. Four poles with a feedback around them, and Resonance is how much: below one it emphasises what is at the cutoff, at one the feedback exactly makes up for what the poles take off, and above one the filter holds a tone of its own with nothing played into it at all — the whistle the Moogs and the Junos are asked for, and the one thing the ordinary Filter cannot do however far its resonance is pushed. Cutoff and Cutoff depth work as they do there, in cents and negative to invert; Key tracking moves the cutoff with the note played, which needs the note routed here. Drive is how hard the sound is pushed into the filter's own saturation, at one gently and above it into the thick, compressed sound that saturation is wanted for; it is kept out of the feedback, so driving the filter does not move the point at which it starts to sing. This module carries code: nothing in the vocabulary can be fed back into itself a sample at a time, which is the whole of what a filter like this is. Resonance is an input as well, and its depth how far a signal there moves it — an envelope into it makes the filter sing at the start of a note and settle as it decays. However far it is pushed, it stays inside the knob's own range.",
  "audioInputs": [
    {
      "id": "in",
      "label": "In"
    },
    {
      "id": "cutoff",
      "label": "Cutoff"
    },
    {
      "id": "resonance",
      "label": "Resonance"
    }
  ],
  "audioOutputs": [
    {
      "id": "out",
      "label": "Out"
    }
  ],
  "midiInputs": [
    {
      "id": "midi",
      "label": "Note"
    }
  ],
  "nodes": [
    {
      "id": "in",
      "kind": "moduleInput",
      "options": {
        "input": "in"
      }
    },
    {
      "id": "gen",
      "kind": "audioWorklet",
      "options": {
        "code": "// A ladder filter: four poles, and a feedback around them deep enough that the\n// filter sings on its own. The node vocabulary cannot reach it — a\n// BiquadFilter is linear and unconditionally stable, so however far its Q is\n// pushed it rings after something excites it and then stops. The filters this\n// one is modelled on do not stop: past the point where the feedback makes up\n// for what the four poles take off, they hold a tone of their own at the\n// cutoff, and on those instruments that tone is played rather than avoided.\n//\n// Four one-pole lowpasses in series, the last one's output subtracted from what\n// goes in, through a tanh. A one-pole turns the signal an eighth of a cycle at\n// its own cutoff, so at the cutoff the four together are half a cycle round and\n// the subtraction has become an addition; each one also takes the level down to\n// about seven tenths there, so a feedback of four exactly makes up for the four\n// of them. That is what Resonance is scaled against: below one the filter\n// rings, at one it is on the edge, above it it sings. The tanh is what holds\n// the tone that results at a level rather than doubling it every cycle until\n// the output is a square.\n//\n// Twice per sample, because the feedback is a sample behind: at a high cutoff a\n// whole sample is a large enough part of the cycle to drag the pitch flat, and\n// running the loop at twice the rate halves that error.\nconst clamp = (value, low, high) => (value < low ? low : value > high ? high : value);\n\nclass AtmosynthLadder extends AudioWorkletProcessor {\n  static get parameterDescriptors() {\n    return [\n      { name: 'frequency', defaultValue: 1200, minValue: 20, maxValue: 20000, automationRate: 'a-rate' },\n      { name: 'detune', defaultValue: 0, minValue: -4800, maxValue: 4800, automationRate: 'a-rate' },\n      { name: 'resonance', defaultValue: 0.4, minValue: 0, maxValue: 1.2, automationRate: 'a-rate' },\n      // Into the tanh, and only there: the feedback is left alone, so how hard\n      // the filter is driven does not move the point at which it starts to sing.\n      { name: 'drive', defaultValue: 1, minValue: 0.1, maxValue: 10, automationRate: 'a-rate' },\n    ];\n  }\n\n  constructor() {\n    super();\n    // One state per pole, and the last pole's output as the feedback reads it.\n    this.state = [0, 0, 0, 0];\n    this.last = 0;\n  }\n\n  process(inputs, outputs, parameters) {\n    const output = outputs[0];\n    const channel = output[0];\n    const source = inputs[0] && inputs[0][0] ? inputs[0][0] : null;\n    const at = (values, index) => (values.length > 1 ? values[index] : values[0]);\n    const ceiling = sampleRate * 0.45;\n\n    for (let index = 0; index < channel.length; index += 1) {\n      const hz = clamp(\n        at(parameters.frequency, index) * Math.pow(2, at(parameters.detune, index) / 1200),\n        20,\n        ceiling,\n      );\n      const k = 4 * at(parameters.resonance, index);\n      // The one-pole coefficient at twice the sample rate, prewarped so the\n      // cutoff lands where the knob says rather than flat of it.\n      const g = Math.tan((Math.PI * hz) / (sampleRate * 2));\n      const a = g / (1 + g);\n\n      // What starts it. An analogue filter is pushed into oscillation by its own\n      // noise; this one, handed digital silence, would sit at exactly zero for\n      // ever — and a filter that sings only once something else has sounded is\n      // not the thing being modelled. A millionth of full scale is a hundred and\n      // twenty decibels down, inaudible on its own and enough to grow into the\n      // tone within a note.\n      const x =\n        at(parameters.drive, index) * (source ? source[index] : 0) +\n        1e-6 * (Math.random() * 2 - 1);\n\n      for (let pass = 0; pass < 2; pass += 1) {\n        let value = Math.tanh(x - k * this.last);\n        for (let pole = 0; pole < 4; pole += 1) {\n          const step = (value - this.state[pole]) * a;\n          value = step + this.state[pole];\n          this.state[pole] = value + step;\n        }\n        this.last = value;\n      }\n      channel[index] = this.last;\n    }\n\n    for (let other = 1; other < output.length; other += 1) output[other].set(channel);\n    return true;\n  }\n}\n\nregisterProcessor('atmosynth-ladder', AtmosynthLadder);\n",
        "declaredParams": [
          {
            "default": 1200,
            "max": 20000,
            "min": 20,
            "name": "frequency"
          },
          {
            "default": 0,
            "max": 4800,
            "min": -4800,
            "name": "detune"
          },
          {
            "default": "0.4",
            "max": "1.2",
            "min": 0,
            "name": "resonance"
          },
          {
            "default": 1,
            "max": 10,
            "min": "0.1",
            "name": "drive"
          }
        ],
        "numberOfInputs": 1,
        "numberOfOutputs": 1,
        "processorName": "atmosynth-ladder"
      }
    },
    {
      "id": "cutoffin",
      "kind": "moduleInput",
      "options": {
        "input": "cutoff"
      }
    },
    {
      "id": "cutoffdepth",
      "kind": "gain",
      "options": {}
    },
    {
      "id": "note",
      "kind": "midiInput",
      "options": {
        "midiInput": "midi"
      }
    },
    {
      "id": "trackdepth",
      "kind": "gain",
      "options": {}
    },
    {
      "id": "trim",
      "kind": "gain",
      "options": {}
    },
    {
      "id": "out",
      "kind": "moduleOutput",
      "options": {
        "output": "out"
      }
    },
    {
      "id": "resonancein",
      "kind": "moduleInput",
      "options": {
        "input": "resonance"
      }
    },
    {
      "id": "resonancedepth",
      "kind": "gain",
      "options": {}
    }
  ],
  "connections": [
    {
      "from": {
        "node": "in",
        "output": 0
      },
      "to": {
        "node": "gen",
        "input": 0
      }
    },
    {
      "from": {
        "node": "gen",
        "output": 0
      },
      "to": {
        "node": "trim",
        "input": 0
      }
    },
    {
      "from": {
        "node": "trim",
        "output": 0
      },
      "to": {
        "node": "out",
        "input": 0
      }
    },
    {
      "from": {
        "node": "cutoffin",
        "output": 0
      },
      "to": {
        "node": "cutoffdepth",
        "input": 0
      }
    },
    {
      "from": {
        "node": "cutoffdepth",
        "output": 0
      },
      "to": {
        "node": "gen",
        "param": "detune"
      }
    },
    {
      "from": {
        "node": "note",
        "output": 0
      },
      "to": {
        "node": "trackdepth",
        "input": 0
      }
    },
    {
      "from": {
        "node": "trackdepth",
        "output": 0
      },
      "to": {
        "node": "gen",
        "param": "frequency"
      }
    },
    {
      "from": {
        "node": "resonancein",
        "output": 0
      },
      "to": {
        "node": "resonancedepth",
        "input": 0
      }
    },
    {
      "from": {
        "node": "resonancedepth",
        "output": 0
      },
      "to": {
        "node": "gen",
        "param": "resonance"
      }
    }
  ],
  "automation": [],
  "parameters": [
    {
      "id": "cutoff",
      "label": "Cutoff (Hz)",
      "range": {
        "min": "20",
        "max": "18000"
      },
      "default": "1200",
      "targets": [
        {
          "node": "gen",
          "param": "frequency"
        }
      ]
    },
    {
      "id": "resonance",
      "label": "Resonance (sings above one)",
      "range": {
        "min": "0",
        "max": "1.2"
      },
      "default": "0.4",
      "targets": [
        {
          "node": "gen",
          "param": "resonance"
        }
      ]
    },
    {
      "id": "drive",
      "label": "Drive",
      "range": {
        "min": "0.1",
        "max": "10"
      },
      "default": "1",
      "targets": [
        {
          "node": "gen",
          "param": "drive"
        }
      ]
    },
    {
      "id": "cutoffAmount",
      "label": "Cutoff depth (cents)",
      "range": {
        "min": "-4800",
        "max": "4800"
      },
      "default": "2400",
      "targets": [
        {
          "node": "cutoffdepth",
          "param": "gain"
        }
      ]
    },
    {
      "id": "keyTrack",
      "label": "Key tracking",
      "range": {
        "min": "0",
        "max": "2"
      },
      "default": "0",
      "targets": [
        {
          "node": "trackdepth",
          "param": "gain"
        }
      ]
    },
    {
      "id": "level",
      "label": "Level",
      "range": {
        "min": "0",
        "max": "2"
      },
      "default": "1",
      "targets": [
        {
          "node": "trim",
          "param": "gain"
        }
      ]
    },
    {
      "id": "resonanceAmount",
      "label": "Resonance depth",
      "range": {
        "min": "-1.2",
        "max": "1.2"
      },
      "default": "0.4",
      "targets": [
        {
          "node": "resonancedepth",
          "param": "gain"
        }
      ]
    }
  ],
  "sampleSlots": [],
  "createdAt": "2026-09-21T00:00:00.000Z"
}