{
  "$type": "at.atmosynth.module",
  "descriptionVersion": 1,
  "moduleId": "sample-hold",
  "versionKey": "1",
  "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 one before it and held flat until the step after. It makes no sound of its own. Patch it into a filter's Cutoff for the bubbling the whole thing 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 and a second sample and hold steps it at random intervals. 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 before the value is taken. At nothing it holds the input and nothing else, which is the sample and hold a modular patch means — an envelope sampled by an LFO, an LFO sampled by another one, a slow wave turned into a staircase. At everything it holds its own number and pays the input no attention, which is the random LFO. Between the two it is whatever came in with a tremble on it.\n\nGlide is how much of each step is spent travelling to the new value rather than sitting on it. At nothing the output steps, which is what makes it a sample and hold; at a half it arrives half way through and waits; at one it is still on its way when the next value lands and the line never sits still at all — the smooth random a filtered S&H makes, and the one setting that will not click whatever it is patched into. It is a fraction of the step rather than a length of time, so it means the same thing at every rate and follows a clock that changes its mind.\n\nDeclared shared it wanders once for the whole patch and every note hears the same dice; 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, and is the near miss worth naming: its holding runs on a clock of its own that nothing can tick, and it does not go below 200 Hz — so a value that lasts long enough to be a modulation rather than a texture is not a setting of it that anybody failed to find.",
  "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\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      // 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  }\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        this.held =\n          at(parameters.source, index) + at(parameters.random, index) * (Math.random() * 2 - 1);\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": 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": "glide",
      "label": "Glide",
      "range": {
        "min": "0",
        "max": "1"
      },
      "default": "0",
      "targets": [
        {
          "node": "gen",
          "param": "glide"
        }
      ]
    }
  ],
  "sampleSlots": [],
  "createdAt": "2026-09-05T00:00:00.000Z"
}