{
  "$type": "at.atmosynth.module",
  "descriptionVersion": 1,
  "moduleId": "beat-repeat",
  "versionKey": "2",
  "name": "Beat repeat",
  "description": "A slice of what just came through, held and played round and round. Repeat at nothing is the signal passing straight through and nothing else; turn it up and the door on the delay line shuts, what is already inside goes round at unity, and the last Slice beats of sound loop where the music was. Slice is counted in beats against the one tempo the whole instrument keeps — the same one the arpeggiator and the sequencer count against — so a quarter is a sixteenth note, a half is an eighth, a four is a whole bar, and moving the tempo moves the stutter with it. There is no tempo knob here, and that is the point: a second tempo would be free to disagree with the first. The knob is a continuum rather than a switch, and the middle of it is worth finding — at nothing the door is open and the loop is silent, at everything the door is shut and nothing new gets in, and half way the door is half open, so fresh sound keeps arriving while what is already round goes round again, which is a synced delay. One knob, because those are three positions of one door rather than three effects. Repeat is an input as well, with a depth for how far a signal there moves it, and that is what this is really played by: a square LFO chopping it in and out, a sample and hold opening it at random, an envelope, or the knob itself under a controller. What it catches is whatever was in the line at the instant the door shut, so a stutter lands in time by being pressed in time — nothing here is told where the bar began. Decay is how much of each pass is lost on the way into the next: at nothing the loop holds for as long as you keep it, turned up it fades where it stands, and at everything it plays once and is gone. Nothing stands between the line and itself but that one knob, which is what lets the hold be exact — a filter in the loop is heard again on every pass, and a lowpass with any resonance at all has a frequency it hands back louder than it was given, so a loop closed through one does not hold, it climbs. Damp is that lowpass moved to where it is heard once: it is the tone of the repeat, and it is what takes the edge off the seam, since the end of a slice and its beginning were never going to meet and that joint is heard once a pass. Moving Slice while the loop runs rewrites what is in the line rather than re-cutting it, which is a tape being pulled: shorter for a pitch up, longer for a pitch down. With Decay at nothing the hold is exact, which takes a little code: a loop made of a delay line alone is rarely a whole number of samples long, and one that is not reads each pass from between two samples, losing some of its top every time round until it fades. This one cuts the slice to the nearest whole sample, a hundredth of a millisecond at most, so each pass is an exact copy of the last. It holds sound that outlives the note that made it, so it is added shared — per voice, each note would take its own loop with it when it ended.",
  "audioInputs": [
    {
      "id": "in",
      "label": "In"
    },
    {
      "id": "repeat",
      "label": "Repeat"
    }
  ],
  "audioOutputs": [
    {
      "id": "out",
      "label": "Out"
    }
  ],
  "midiInputs": [],
  "nodes": [
    {
      "id": "in",
      "kind": "moduleInput",
      "options": {
        "input": "in"
      }
    },
    {
      "id": "repeatin",
      "kind": "moduleInput",
      "options": {
        "input": "repeat"
      }
    },
    {
      "id": "depth",
      "kind": "gain",
      "options": {}
    },
    {
      "id": "beat",
      "kind": "constantSource",
      "options": {
        "signal": "beat"
      }
    },
    {
      "id": "slice",
      "kind": "gain",
      "options": {}
    },
    {
      "id": "loop",
      "kind": "audioWorklet",
      "options": {
        "code": "// A slice of what just came through, held and played round and round.\n//\n// The first version of this was ordinary nodes — a delay line with its door\n// shut and its feedback at one — and it did not hold. A delay line read at a\n// time between two samples mixes the two either side of it, and a loop reads\n// its own output back through that mixing on every pass, so a loop whose\n// length is not a whole number of samples loses a little of its top each time\n// round and fades where it stands, from the highs down, in a few seconds. A\n// slice counted in beats is almost never a whole number of samples. Rounding\n// it to one is the whole of the fix and is not something the vocabulary can\n// do: nothing in it knows the sample rate, and nothing in it rounds a value\n// that moves. That is the reason this carries code, and the only one — the\n// door, the decay and the tone are what they were, and Damp is still an\n// ordinary filter outside it.\n//\n// Settled, the read is a whole number of samples behind the write and a pass\n// is an exact copy of the last one. While the length is moving — Slice being\n// turned, the tempo changing — the read runs at the exact fractional length\n// and mixes the two samples either side of it, because that is what makes a\n// moving slice sound like tape being pulled rather than a stepped glitch; once\n// it stops, the read goes back to the nearest whole sample, which is half a\n// sample's jump at most. The loop is then that half sample short or long of the\n// beat, which is ten microseconds at 48 kHz: it takes a hundred passes to drift\n// a millisecond from the grid.\n//\n// Nothing here closes a loop through the audio graph, so nothing costs a\n// render quantum on the way round, and the slice needs no correction for one.\n//\n// Two outputs, because what passes and what is held are heard differently:\n// the first is the loop, which goes on through the damping filter, and the\n// second is the signal passing by, which does not.\n\n// Longest slice the knob and the slowest tempo can ask for, in seconds: four\n// beats at twenty to the minute.\nconst LONGEST = 12;\n\nconst clamp = (value, low, high) => (value < low ? low : value > high ? high : value);\n\nclass AtmosynthBeatRepeat extends AudioWorkletProcessor {\n  static get parameterDescriptors() {\n    return [\n      // The door: the knob plus whatever the Repeat input brings, which can\n      // take it past either end, so it is held inside them here rather than by\n      // the declaration.\n      { name: 'repeat', defaultValue: 0, minValue: -1000, maxValue: 1000, automationRate: 'a-rate' },\n      // The slice in seconds — the beat times the Slice knob, worked out in\n      // the graph from the one tempo the instrument keeps. Nothing of its own,\n      // because what arrives at a param is added to its value.\n      { name: 'length', defaultValue: 0, minValue: 0, maxValue: LONGEST, automationRate: 'a-rate' },\n      // How much of each pass is lost on the way into the next.\n      { name: 'decay', defaultValue: 0, minValue: 0, maxValue: 1, automationRate: 'a-rate' },\n    ];\n  }\n\n  constructor() {\n    super();\n    this.size = Math.ceil(LONGEST * sampleRate) + 2;\n    // One line per side, made when a side first arrives, so a mono patch\n    // never holds a second one.\n    this.lines = [];\n    this.write = 0;\n    this.lastLength = -1;\n  }\n\n  process(inputs, outputs, parameters) {\n    const input = inputs[0] || [];\n    const [wet, dry] = outputs;\n    const sides = Math.max(1, Math.min(2, input.length));\n    while (this.lines.length < sides) this.lines.push(new Float32Array(this.size));\n    const lines = this.lines;\n    const size = this.size;\n    const frames = wet[0].length;\n    const at = (values, index) => (values.length > 1 ? values[index] : values[0]);\n\n    let write = this.write;\n    for (let index = 0; index < frames; index += 1) {\n      const door = clamp(at(parameters.repeat, index), 0, 1);\n      const feedback = door * (1 - clamp(at(parameters.decay, index), 0, 1));\n      const exact = clamp(at(parameters.length, index) * sampleRate, 1, size - 2);\n      const moving = exact !== this.lastLength;\n      this.lastLength = exact;\n      const behind = moving ? exact : Math.round(exact);\n\n      let read = write - behind;\n      if (read < 0) read += size;\n      const whole = Math.floor(read);\n      const fraction = read - whole;\n      const next = whole + 1 >= size ? 0 : whole + 1;\n\n      for (let side = 0; side < sides; side += 1) {\n        const line = lines[side];\n        const arriving = input.length ? input[Math.min(side, input.length - 1)][index] : 0;\n        const held = fraction === 0 ? line[whole] : line[whole] + (line[next] - line[whole]) * fraction;\n        line[write] = (1 - door) * arriving + feedback * held;\n        wet[side][index] = door * held;\n        dry[side][index] = (1 - door) * arriving;\n      }\n      write = write + 1 >= size ? 0 : write + 1;\n    }\n    this.write = write;\n\n    // A mono input is heard on both sides.\n    for (const output of outputs) {\n      for (let other = sides; other < output.length; other += 1) output[other].set(output[0]);\n    }\n    return true;\n  }\n}\n\nregisterProcessor('atmosynth-beat-repeat', AtmosynthBeatRepeat);\n",
        "declaredParams": [
          {
            "default": 0,
            "max": 1000,
            "min": -1000,
            "name": "repeat"
          },
          {
            "default": 0,
            "max": 12,
            "min": 0,
            "name": "length"
          },
          {
            "default": 0,
            "max": 1,
            "min": 0,
            "name": "decay"
          }
        ],
        "numberOfInputs": 1,
        "numberOfOutputs": 2,
        "outputChannelCount": [
          2,
          2
        ],
        "processorName": "atmosynth-beat-repeat"
      }
    },
    {
      "id": "damp",
      "kind": "biquadFilter",
      "options": {
        "type": "lowpass"
      }
    },
    {
      "id": "out",
      "kind": "moduleOutput",
      "options": {
        "output": "out"
      }
    }
  ],
  "connections": [
    {
      "from": {
        "node": "in",
        "output": 0
      },
      "to": {
        "node": "loop",
        "input": 0
      }
    },
    {
      "from": {
        "node": "loop",
        "output": 0
      },
      "to": {
        "node": "damp",
        "input": 0
      }
    },
    {
      "from": {
        "node": "damp",
        "output": 0
      },
      "to": {
        "node": "out",
        "input": 0
      }
    },
    {
      "from": {
        "node": "loop",
        "output": 1
      },
      "to": {
        "node": "out",
        "input": 0
      }
    },
    {
      "from": {
        "node": "beat",
        "output": 0
      },
      "to": {
        "node": "slice",
        "input": 0
      }
    },
    {
      "from": {
        "node": "slice",
        "output": 0
      },
      "to": {
        "node": "loop",
        "param": "length"
      }
    },
    {
      "from": {
        "node": "repeatin",
        "output": 0
      },
      "to": {
        "node": "depth",
        "input": 0
      }
    },
    {
      "from": {
        "node": "depth",
        "output": 0
      },
      "to": {
        "node": "loop",
        "param": "repeat"
      }
    }
  ],
  "automation": [],
  "parameters": [
    {
      "id": "slice",
      "label": "Slice (beats)",
      "range": {
        "min": "0.0625",
        "max": "4"
      },
      "default": "0.25",
      "targets": [
        {
          "node": "slice",
          "param": "gain"
        }
      ]
    },
    {
      "id": "repeat",
      "label": "Repeat",
      "range": {
        "min": "0",
        "max": "1"
      },
      "default": "0",
      "targets": [
        {
          "node": "loop",
          "param": "repeat"
        }
      ]
    },
    {
      "id": "repeatAmount",
      "label": "Repeat amount",
      "range": {
        "min": "-1",
        "max": "1"
      },
      "default": "1",
      "targets": [
        {
          "node": "depth",
          "param": "gain"
        }
      ]
    },
    {
      "id": "decay",
      "label": "Decay",
      "range": {
        "min": "0",
        "max": "1"
      },
      "default": "0",
      "targets": [
        {
          "node": "loop",
          "param": "decay"
        }
      ]
    },
    {
      "id": "damp",
      "label": "Damp (Hz, lower is darker)",
      "range": {
        "min": "200",
        "max": "18000"
      },
      "default": "12000",
      "targets": [
        {
          "node": "damp",
          "param": "frequency"
        }
      ]
    }
  ],
  "sampleSlots": [],
  "createdAt": "2026-09-23T00:00:00.000Z"
}