{
  "$type": "at.atmosynth.module",
  "descriptionVersion": 1,
  "moduleId": "filter-comb",
  "versionKey": "2",
  "name": "Comb filter",
  "description": "A filter tuned to a pitch rather than set to a cutoff. It delays the sound by exactly one period of that pitch and adds it back to itself, so everything at the pitch and its harmonics reinforces and everything between cancels — a whole row of peaks and notches at once, which is the sound of a short room, a cardboard tube, or a plucked string depending on how much of it goes round again. Resonance is how much: near one it rings on into a pitch of its own long after the sound that started it, and below nothing the feedback is subtracted instead, which shifts every notch by half a spacing and leaves the hollow, woody sound an octave below where you would expect it. Damping takes a little more of the top off each time round, so the ring dies from the treble down as a string does rather than all at once. Pitch is an input as well as a knob and Pitch depth is how far, in cents — an LFO patched in is a flanger you wired yourself — and Key tracking moves the tuning with the note played, which needs the note routed here: at one the comb rings in tune with whatever is played through it. This module carries code, for the one thing the node vocabulary cannot do: a comb's delay is one divided by its pitch, and nothing in that vocabulary divides. Resonance is an input as well, and its depth how far a signal there moves it; the comb holds what arrives inside the knob's own range, so the ring never runs away.",
  "audioInputs": [
    {
      "id": "in",
      "label": "In"
    },
    {
      "id": "pitch",
      "label": "Pitch"
    },
    {
      "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 comb filter: one delay line fed back into itself, tuned in hertz.\n//\n// The delay and the feedback are both ordinary nodes, and the sound is\n// reachable without code — the Delay module is that patch. What is not\n// reachable is the knob. A comb's delay is the *reciprocal* of the pitch it\n// rings at, and the node vocabulary sums and scales signals; it does not\n// divide one. So nothing built from it can put a frequency on the knob, sweep\n// the tuning evenly in cents, or take its pitch from the note played — and a\n// comb that cannot be tuned is a very short delay, which the library already\n// has. The reciprocal is the whole reason this one carries code.\n//\n// One period of delay, read back with the two samples either side of the\n// fractional position mixed in proportion: at 3 kHz a period is sixteen\n// samples, and rounding to the nearer of them would put the ringing a\n// semitone out and step audibly as it swept.\n//\n// The feedback runs through a single pole of lowpass, so each pass round the\n// loop loses a little more of the top than the last. That is what a plucked\n// string does and what a bare comb does not: undamped, the highest partials\n// ring exactly as long as the lowest, which sounds like a metal tube rather\n// than anything with a body.\nconst clamp = (value, low, high) => (value < low ? low : value > high ? high : value);\n\nclass AtmosynthComb extends AudioWorkletProcessor {\n  static get parameterDescriptors() {\n    return [\n      { name: 'frequency', defaultValue: 220, minValue: 20, maxValue: 8000, automationRate: 'a-rate' },\n      { name: 'detune', defaultValue: 0, minValue: -4800, maxValue: 4800, automationRate: 'a-rate' },\n      // Negative is the other polarity, not less of the same one: the feedback\n      // subtracted rather than added moves every notch half a spacing and\n      // leaves the odd harmonics, which is the hollow, half-pitched sound.\n      { name: 'resonance', defaultValue: 0.6, minValue: -0.95, maxValue: 0.95, automationRate: 'a-rate' },\n      { name: 'damping', defaultValue: 0.2, minValue: 0, maxValue: 1, automationRate: 'a-rate' },\n    ];\n  }\n\n  constructor() {\n    super();\n    // A twentieth of a second of line, which is one period of the lowest\n    // tuning the knob reaches, at whatever rate this context runs at.\n    this.line = new Float32Array(Math.ceil(sampleRate / 20) + 4);\n    this.write = 0;\n    this.damped = 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 line = this.line;\n    const size = line.length;\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        sampleRate / 4,\n      );\n      // The reciprocal: one period of that pitch, in samples.\n      const period = clamp(sampleRate / hz, 2, size - 2);\n      let read = this.write - period;\n      if (read < 0) read += size;\n      const whole = Math.floor(read);\n      const next = whole + 1 >= size ? 0 : whole + 1;\n      const fraction = read - whole;\n      const delayed = line[whole] + (line[next] - line[whole]) * fraction;\n\n      // At nothing the pole passes everything and the comb rings evenly; at\n      // one it keeps a fiftieth of each pass, and the ring is over almost\n      // before it starts.\n      const opening = 1 - at(parameters.damping, index) * 0.98;\n      this.damped += opening * (delayed - this.damped);\n\n      const value =\n        (source ? source[index] : 0) + at(parameters.resonance, index) * this.damped;\n      line[this.write] = value;\n      this.write = this.write + 1 >= size ? 0 : this.write + 1;\n      channel[index] = value;\n    }\n\n    for (let other = 1; other < output.length; other += 1) output[other].set(channel);\n    return true;\n  }\n}\n\nregisterProcessor('atmosynth-comb', AtmosynthComb);\n",
        "declaredParams": [
          {
            "default": 220,
            "max": 8000,
            "min": 20,
            "name": "frequency"
          },
          {
            "default": 0,
            "max": 4800,
            "min": -4800,
            "name": "detune"
          },
          {
            "default": "0.6",
            "max": "0.95",
            "min": "-0.95",
            "name": "resonance"
          },
          {
            "default": "0.2",
            "max": 1,
            "min": 0,
            "name": "damping"
          }
        ],
        "numberOfInputs": 1,
        "numberOfOutputs": 1,
        "processorName": "atmosynth-comb"
      }
    },
    {
      "id": "pitchin",
      "kind": "moduleInput",
      "options": {
        "input": "pitch"
      }
    },
    {
      "id": "pitchdepth",
      "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": "pitchin",
        "output": 0
      },
      "to": {
        "node": "pitchdepth",
        "input": 0
      }
    },
    {
      "from": {
        "node": "pitchdepth",
        "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": "pitch",
      "label": "Pitch (Hz)",
      "range": {
        "min": "20",
        "max": "8000"
      },
      "default": "220",
      "targets": [
        {
          "node": "gen",
          "param": "frequency"
        }
      ]
    },
    {
      "id": "resonance",
      "label": "Resonance (below nothing to invert)",
      "range": {
        "min": "-0.95",
        "max": "0.95"
      },
      "default": "0.6",
      "targets": [
        {
          "node": "gen",
          "param": "resonance"
        }
      ]
    },
    {
      "id": "damping",
      "label": "Damping",
      "range": {
        "min": "0",
        "max": "1"
      },
      "default": "0.2",
      "targets": [
        {
          "node": "gen",
          "param": "damping"
        }
      ]
    },
    {
      "id": "pitchAmount",
      "label": "Pitch depth (cents)",
      "range": {
        "min": "-4800",
        "max": "4800"
      },
      "default": "1200",
      "targets": [
        {
          "node": "pitchdepth",
          "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": "0.5",
      "targets": [
        {
          "node": "trim",
          "param": "gain"
        }
      ]
    },
    {
      "id": "resonanceAmount",
      "label": "Resonance depth",
      "range": {
        "min": "-1.9",
        "max": "1.9"
      },
      "default": "0.4",
      "targets": [
        {
          "node": "resonancedepth",
          "param": "gain"
        }
      ]
    }
  ],
  "sampleSlots": [],
  "createdAt": "2026-09-21T00:00:00.000Z"
}