{
  "$type": "at.atmosynth.module",
  "descriptionVersion": 1,
  "moduleId": "sample-hold",
  "versionKey": "3",
  "name": "Sample & hold",
  "description": "A value taken at a moment and kept until the next one. Left alone it is the random source every synthesiser with an S&H on its LFO has: a new number each step, unrelated to the last and held flat until the next. It makes no sound of its own. Patch it into a filter's Cutoff for the bubbling it is famous for, into an oscillator's Detune for a line that never repeats, or into a pan for a sound that will not sit still. How far it moves what it arrives at is set there, as it is for the LFO.\n\nRate is how often a new value is taken. Clock is that question asked by something else: whatever is patched there takes the step over from the rate, on its rising zero crossings, so an LFO steps it once a cycle. An input with nothing in it never crosses, so the rate keeps the job.\n\nIn is what gets sampled, and Random is how much of the module's own dice is mixed into it. At nothing it holds the input alone, which is the sample and hold a modular patch means — a slow wave turned into a staircase. At everything it holds its own number and ignores the input, which is the random LFO.\n\nLength makes the dice repeat. Off, every step rolls afresh. At a number, that many values are rolled when the module starts and played round in order: at a slow rate a random phrase that loops, and with a clock at a note's pitch a tone with a timbre of its own rather than noise, pitched that many times below the clock. Per note every note rolls its own; shared, the patch keeps one.\n\nGlide is how much of each step is spent travelling to the new value. At nothing the output steps; at one it is still on its way when the next value lands, which is the smooth random a filtered S&H makes and will not click. It is a fraction of the step, so it means the same at every rate.\n\nScale rounds each value to a note before it is held. With it on the output counts in octaves, so into an oscillator's Detune at a Detune amount of 1200 cents every value is a note of the scale, counted from the note played: a random melody rather than a random tuning. Off rounds nothing. The rounding comes before Glide, so a glide travels from note to note.\n\nDeclared shared it wanders once for the whole patch; per note each note rolls its own, which is what makes a chord shimmer rather than move together.\n\nThis module carries code because nothing in the vocabulary holds a value from one sample to the next. The bitcrusher does, but on a clock of its own that nothing can tick and never below 200 Hz — too fast to be a modulation rather than a texture.",
  "audioInputs": [
    {
      "id": "in",
      "label": "In"
    },
    {
      "id": "clock",
      "label": "Clock"
    }
  ],
  "audioOutputs": [
    {
      "id": "out",
      "label": "Out"
    }
  ],
  "midiInputs": [],
  "nodes": [
    {
      "id": "gen",
      "kind": "audioWorklet",
      "options": {
        "code": "// A value taken at a moment and kept until the next one.\n//\n// Nothing in the vocabulary holds a value from one sample to the next, which is\n// the reason the bitcrusher carries code and the reason this does. What the\n// bitcrusher cannot be is *told when*: its holding runs on a rate of its own\n// that nothing else can tick, and it does not go below 200 Hz, so the one thing\n// a sample and hold is for — a value that lasts long enough to be a modulation\n// rather than a texture — is not a setting of it that anybody failed to find.\n//\n// Left alone this is the random source every synthesiser with an S&H on its LFO\n// has: a new number each step, unrelated to the last. There is no noise\n// generator here, deliberately. Reading a white noise source at one instant\n// gives a number unrelated to the one read before it, and a number unrelated to\n// the one before it is the whole of what reaches the output — so the sampling\n// is kept and the noise it would have sampled is not.\n//\n// Whatever is patched into In is summed with that number before it is taken,\n// and Random is how much of the number there is. At nothing it holds the input\n// and nothing else, which is the sample and hold a modular patch means; at\n// everything it holds its own dice; and in between it is a source with a\n// tremble on it. There is no switch between those because they are one\n// operation with a knob in it.\n//\n// Glide is a fraction of the step rather than a length of time, so it means the\n// same thing at every rate and follows an external clock that changes its mind.\n// At nothing the output steps; at a half it arrives half way through the step\n// and waits there; at one it is still travelling when the next value lands and\n// the line never sits still, which is the smooth random a filtered sample and\n// hold makes.\n//\n// Scale rounds the value to a note before it is held, reading the output as\n// octaves — one is an octave, a twelfth is a semitone — so that at a Detune\n// amount of 1200 cents each value is a note of the scale counted from the note\n// being played. It arrives as a position rather than a name, because a choice\n// reaching an AudioParam is given its place in the list, and SCALES is that\n// list in the same order. The rounding is done at the step and not at the\n// output, so Glide travels between notes rather than being rounded back into\n// a staircase.\n//\n// Length makes the dice repeat. Off, each step rolls afresh; at a number, the\n// steps go round that many values rolled when the processor was made. A\n// repeating signal is a pitched one, so this is what turns a clock at a note's\n// pitch from noise into a tone — its period is Length ticks of the clock, so it\n// sounds that many times below it. Every length reads the same table from its\n// start, which is what lets Length be moved while playing without the loop it\n// had turning into a different one. Like Scale, it arrives as a position in\n// the menu, and LENGTHS is that menu in the same order.\n\n// How many values each Length goes round, in the order the parameter lists\n// them. Off goes round none, and rolls every step.\nconst LENGTHS = [0, 2, 3, 4, 5, 6, 7, 8, 12, 16, 32, 64];\nconst LONGEST = 64;\n\n// Semitones above the root of each scale, in the order the parameter lists\n// them. Off rounds nothing, so it has no entry.\nconst SCALES = [\n  null,\n  [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11],\n  [0, 2, 4, 5, 7, 9, 11],\n  [0, 2, 3, 5, 7, 8, 10],\n  [0, 2, 4, 7, 9],\n  [0, 3, 5, 7, 10],\n  [0],\n];\n\n/** The note of the scale nearest a value counted in octaves. */\nfunction quantise(value, degrees) {\n  const semitones = value * 12;\n  const octave = Math.floor(semitones / 12);\n  const within = semitones - octave * 12;\n  // The root an octave up is the scale's last candidate, or a value just\n  // below it would round down to the scale's highest note instead.\n  let nearest = 12;\n  let distance = 12 - within;\n  for (let index = 0; index < degrees.length; index += 1) {\n    const away = Math.abs(within - degrees[index]);\n    if (away < distance) {\n      distance = away;\n      nearest = degrees[index];\n    }\n  }\n  return (octave * 12 + nearest) / 12;\n}\n\nclass AtmosynthSampleHold extends AudioWorkletProcessor {\n  static get parameterDescriptors() {\n    return [\n      { name: 'rate', defaultValue: 4, minValue: 0.01, maxValue: 40, automationRate: 'a-rate' },\n      { name: 'random', defaultValue: 1, minValue: 0, maxValue: 1, automationRate: 'a-rate' },\n      { name: 'glide', defaultValue: 0, minValue: 0, maxValue: 1, automationRate: 'a-rate' },\n      // Which of SCALES, by its position. Read only at a step, so once a block\n      // is enough.\n      { name: 'scale', defaultValue: 0, minValue: 0, maxValue: SCALES.length - 1, automationRate: 'k-rate' },\n      // Which of LENGTHS, by its position, read at a step as Scale is.\n      { name: 'length', defaultValue: 0, minValue: 0, maxValue: LENGTHS.length - 1, automationRate: 'k-rate' },\n      // What is patched into In, and what is patched into Clock. Both are read\n      // at audio rate, because a sample and hold that took its value from the\n      // start of a block would take it up to three milliseconds early.\n      { name: 'source', defaultValue: 0, minValue: -1000, maxValue: 1000, automationRate: 'a-rate' },\n      { name: 'clock', defaultValue: 0, minValue: -1000, maxValue: 1000, automationRate: 'a-rate' },\n    ];\n  }\n\n  constructor() {\n    super();\n    // Past its end, so the first sample of the first block is a step: a voice\n    // that has only just started still has a value to give away.\n    this.phase = 1;\n    this.lastClock = 0;\n    /** Samples left for which the Clock input still counts as driving, and\n     *  samples since it last ticked, which is what sets that. */\n    this.external = 0;\n    this.sinceClock = 0;\n    /** The value taken at the last step, the one before it, and how far the\n     *  glide between them has got in samples. */\n    this.held = 0;\n    this.from = 0;\n    this.travelled = 0;\n    /** Samples from one step to the next, which is what Glide is a fraction\n     *  of. Until there have been two steps it is what the rate implies. */\n    this.period = sampleRate / 4;\n    /** The values Length goes round, rolled once, and the next one due. */\n    this.dice = new Float32Array(LONGEST);\n    for (let index = 0; index < LONGEST; index += 1) this.dice[index] = Math.random() * 2 - 1;\n    this.next = 0;\n  }\n\n  process(inputs, outputs, parameters) {\n    const output = outputs[0];\n    const channel = output[0];\n    const at = (values, index) => (values.length > 1 ? values[index] : values[0]);\n\n    for (let index = 0; index < channel.length; index += 1) {\n      const rate = at(parameters.rate, index);\n\n      // The Clock input's rising zero crossings, which take the step over from\n      // the rate while anything is arriving there. Silence never crosses, so an\n      // input with nothing patched into it is not a clock — the same rule the\n      // sync oscillator's own Sync input answers to.\n      let step = false;\n      const level = at(parameters.clock, index);\n      this.sinceClock += 1;\n      if (this.lastClock < 0 && level >= 0) {\n        step = true;\n        // Long enough to cover the next tick, whatever this clock's tempo. A\n        // fixed hold would let the rate's own clock back in between the ticks\n        // of anything slower than it, which is most of what gets patched here,\n        // so it is three times the interval this clock just took — and the\n        // interval is measured from tick to tick rather than from step to\n        // step, or a rate ticking away in the gap would keep resetting it to\n        // its own and the hold would never learn how slow the clock was.\n        this.external = Math.max(sampleRate, 3 * this.sinceClock);\n        this.sinceClock = 0;\n      }\n      this.lastClock = level;\n\n      // The rate's own clock. It keeps turning while something else is driving,\n      // so giving the input up hands the step straight back to it.\n      this.phase += rate / sampleRate;\n      if (this.phase >= 1) {\n        this.phase -= 1;\n        if (this.external <= 0) step = true;\n      }\n      if (this.external > 0) this.external -= 1;\n\n      this.travelled += 1;\n      if (step) {\n        // Where the output actually is, read before anything is moved: a glide\n        // interrupted part way sets off from where it had got to and not from\n        // where it was going, or every step under a long glide would jump.\n        const reached = this.reached(index, parameters);\n        // Two steps make a period; one does not, so the first keeps the rate's\n        // own and nothing is divided by nothing.\n        if (this.travelled > 1) this.period = this.travelled;\n        this.travelled = 0;\n        this.from = reached;\n        const length = LENGTHS[Math.round(parameters.length[0])];\n        let roll;\n        if (length) {\n          roll = this.dice[this.next % length];\n          this.next = (this.next + 1) % length;\n        } else {\n          roll = Math.random() * 2 - 1;\n        }\n        const taken = at(parameters.source, index) + at(parameters.random, index) * roll;\n        const degrees = SCALES[Math.round(parameters.scale[0])];\n        this.held = degrees ? quantise(taken, degrees) : taken;\n      }\n\n      channel[index] = this.reached(index, parameters);\n    }\n\n    for (let other = 1; other < output.length; other += 1) output[other].set(channel);\n    return true;\n  }\n\n  /** Where the output has got to on its way from the last value to this one. */\n  reached(index, parameters) {\n    const glide = parameters.glide.length > 1 ? parameters.glide[index] : parameters.glide[0];\n    const over = glide * this.period;\n    if (!(over > 0)) return this.held;\n    const travelled = this.travelled / over;\n    return travelled >= 1 ? this.held : this.from + (this.held - this.from) * travelled;\n  }\n}\n\nregisterProcessor('atmosynth-sample-hold', AtmosynthSampleHold);\n",
        "declaredParams": [
          {
            "default": 4,
            "max": 40,
            "min": "0.01",
            "name": "rate"
          },
          {
            "default": 1,
            "max": 1,
            "min": 0,
            "name": "random"
          },
          {
            "default": 0,
            "max": 1,
            "min": 0,
            "name": "glide"
          },
          {
            "default": 0,
            "max": 6,
            "min": 0,
            "name": "scale"
          },
          {
            "default": 0,
            "max": 11,
            "min": 0,
            "name": "length"
          },
          {
            "default": 0,
            "max": 1000,
            "min": -1000,
            "name": "source"
          },
          {
            "default": 0,
            "max": 1000,
            "min": -1000,
            "name": "clock"
          }
        ],
        "numberOfInputs": 0,
        "numberOfOutputs": 1,
        "processorName": "atmosynth-sample-hold"
      }
    },
    {
      "id": "inin",
      "kind": "moduleInput",
      "options": {
        "input": "in"
      }
    },
    {
      "id": "clockin",
      "kind": "moduleInput",
      "options": {
        "input": "clock"
      }
    },
    {
      "id": "out",
      "kind": "moduleOutput",
      "options": {
        "output": "out"
      }
    }
  ],
  "connections": [
    {
      "from": {
        "node": "gen",
        "output": 0
      },
      "to": {
        "node": "out",
        "input": 0
      }
    },
    {
      "from": {
        "node": "inin",
        "output": 0
      },
      "to": {
        "node": "gen",
        "param": "source"
      }
    },
    {
      "from": {
        "node": "clockin",
        "output": 0
      },
      "to": {
        "node": "gen",
        "param": "clock"
      }
    }
  ],
  "automation": [],
  "parameters": [
    {
      "id": "rate",
      "label": "Rate (Hz)",
      "range": {
        "min": "0.01",
        "max": "40"
      },
      "default": "4",
      "targets": [
        {
          "node": "gen",
          "param": "rate"
        }
      ]
    },
    {
      "id": "random",
      "label": "Random",
      "range": {
        "min": "0",
        "max": "1"
      },
      "default": "1",
      "targets": [
        {
          "node": "gen",
          "param": "random"
        }
      ]
    },
    {
      "id": "length",
      "label": "Length",
      "values": [
        "off",
        "2",
        "3",
        "4",
        "5",
        "6",
        "7",
        "8",
        "12",
        "16",
        "32",
        "64"
      ],
      "default": "off",
      "targets": [
        {
          "node": "gen",
          "param": "length"
        }
      ]
    },
    {
      "id": "glide",
      "label": "Glide",
      "range": {
        "min": "0",
        "max": "1"
      },
      "default": "0",
      "targets": [
        {
          "node": "gen",
          "param": "glide"
        }
      ]
    },
    {
      "id": "scale",
      "label": "Scale",
      "values": [
        "off",
        "chromatic",
        "major",
        "minor",
        "major pentatonic",
        "minor pentatonic",
        "octaves"
      ],
      "default": "off",
      "targets": [
        {
          "node": "gen",
          "param": "scale"
        }
      ]
    }
  ],
  "sampleSlots": [],
  "createdAt": "2026-09-21T00:00:00.000Z"
}