{
  "$type": "at.atmosynth.module",
  "descriptionVersion": 1,
  "moduleId": "bitcrusher",
  "versionKey": "1",
  "name": "Bitcrusher",
  "description": "A converter with not enough of either thing a converter needs. Bits is how many levels are left to describe the sound with: at sixteen the steps are far too small to hear, at eight the quiet parts start to grow a fizz that follows the note, and at three or four what is left is a shape rather than a sound. Rate is how often a new sample is taken at all, and everything between one and the next is held flat — that is where most of the sound comes from, because every partial above half the rate folds back down under it and arrives at a pitch that has nothing to do with the note. Bringing the rate down is the metallic ringing tail; bringing the bits down is the grit underneath it. At the top of its range the rate holds every sample and does nothing at all, so the bits can be heard on their own. Rate is an input as well as a knob, and Rate amount is how far, in hertz. Dry/wet keeps the sound that went in beside the crushed one rather than replacing it. This module carries code: nothing in the node vocabulary holds a value from one sample to the next, and a curve that could round one is fixed when it is built, so neither knob here could turn.",
  "audioInputs": [
    {
      "id": "in",
      "label": "In"
    },
    {
      "id": "rate",
      "label": "Rate"
    }
  ],
  "audioOutputs": [
    {
      "id": "out",
      "label": "Out"
    }
  ],
  "midiInputs": [],
  "nodes": [
    {
      "id": "in",
      "kind": "moduleInput",
      "options": {
        "input": "in"
      }
    },
    {
      "id": "dry",
      "kind": "gain",
      "options": {}
    },
    {
      "id": "gen",
      "kind": "audioWorklet",
      "options": {
        "code": "// A bitcrusher: the two things a converter does badly, both of them on\n// purpose. Neither is reachable from the node vocabulary.\n//\n// The quantisation half nearly is: a WaveShaper's curve is a lookup, and a\n// staircase written into one rounds every sample to the nearest step exactly\n// as this does. But a curve is a construction option rather than an\n// AudioParam, so a shaper's bit depth is fixed the moment the node is built —\n// it could never be turned, let alone modulated, and a bit depth that cannot\n// move is not what anybody wants a bitcrusher for.\n//\n// The other half is not reachable at all. Nothing in the vocabulary holds a\n// value: every node there answers each sample from the sample that arrived,\n// and sample-and-hold — carry this one forward until the next one is due — is\n// the whole of what a decimator is. It is also where most of the sound comes\n// from. Holding at eight kilohertz folds everything above four back down\n// under it, and the ringing metallic tail that puts under the note is the\n// noise a converter with no filter in front of it makes.\n//\n// Held first and rounded once, in that order: rounding every sample and then\n// throwing most of them away would do the arithmetic the listener never hears\n// and leave the same result.\nclass AtmosynthCrusher extends AudioWorkletProcessor {\n  static get parameterDescriptors() {\n    return [\n      { name: 'bits', defaultValue: 6, minValue: 1, maxValue: 16, automationRate: 'a-rate' },\n      { name: 'rate', defaultValue: 8000, minValue: 200, maxValue: 96000, automationRate: 'a-rate' },\n    ];\n  }\n\n  constructor() {\n    super();\n    this.held = 0;\n    // Due immediately, so the first sample of a note is one the listener hears\n    // rather than the silence a fresh processor starts at.\n    this.phase = 1;\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\n    for (let index = 0; index < channel.length; index += 1) {\n      // Above the rate this context runs at there is nothing to decimate: a\n      // sample is due every sample, and the hold passes everything through.\n      this.phase += Math.min(at(parameters.rate, index), sampleRate) / sampleRate;\n      if (this.phase >= 1) {\n        this.phase -= Math.floor(this.phase);\n        // The gap between two representable levels across the range −1 to 1.\n        // At one bit that is 1 and the signal is left with its sign; at\n        // sixteen it is thirty microvolts of full scale and inaudible, which\n        // is what the top of the knob is for.\n        const step = Math.pow(2, 1 - at(parameters.bits, index));\n        this.held = step * Math.round((source ? source[index] : 0) / step);\n      }\n      channel[index] = this.held;\n    }\n\n    for (let other = 1; other < output.length; other += 1) output[other].set(channel);\n    return true;\n  }\n}\n\nregisterProcessor('atmosynth-crusher', AtmosynthCrusher);\n",
        "declaredParams": [
          {
            "default": 6,
            "max": 16,
            "min": 1,
            "name": "bits"
          },
          {
            "default": 8000,
            "max": 96000,
            "min": 200,
            "name": "rate"
          }
        ],
        "numberOfInputs": 1,
        "numberOfOutputs": 1,
        "processorName": "atmosynth-crusher"
      }
    },
    {
      "id": "wet",
      "kind": "gain",
      "options": {}
    },
    {
      "id": "ratein",
      "kind": "moduleInput",
      "options": {
        "input": "rate"
      }
    },
    {
      "id": "ratedepth",
      "kind": "gain",
      "options": {}
    },
    {
      "id": "mixsrc",
      "kind": "constantSource",
      "options": {}
    },
    {
      "id": "mixdepth",
      "kind": "gain",
      "options": {}
    },
    {
      "id": "mixinvert",
      "kind": "waveShaper",
      "options": {
        "curve": [
          1,
          -1
        ]
      }
    },
    {
      "id": "out",
      "kind": "moduleOutput",
      "options": {
        "output": "out"
      }
    }
  ],
  "connections": [
    {
      "from": {
        "node": "in",
        "output": 0
      },
      "to": {
        "node": "dry",
        "input": 0
      }
    },
    {
      "from": {
        "node": "dry",
        "output": 0
      },
      "to": {
        "node": "out",
        "input": 0
      }
    },
    {
      "from": {
        "node": "in",
        "output": 0
      },
      "to": {
        "node": "gen",
        "input": 0
      }
    },
    {
      "from": {
        "node": "gen",
        "output": 0
      },
      "to": {
        "node": "wet",
        "input": 0
      }
    },
    {
      "from": {
        "node": "wet",
        "output": 0
      },
      "to": {
        "node": "out",
        "input": 0
      }
    },
    {
      "from": {
        "node": "ratein",
        "output": 0
      },
      "to": {
        "node": "ratedepth",
        "input": 0
      }
    },
    {
      "from": {
        "node": "ratedepth",
        "output": 0
      },
      "to": {
        "node": "gen",
        "param": "rate"
      }
    },
    {
      "from": {
        "node": "mixsrc",
        "output": 0
      },
      "to": {
        "node": "mixdepth",
        "input": 0
      }
    },
    {
      "from": {
        "node": "mixdepth",
        "output": 0
      },
      "to": {
        "node": "mixinvert",
        "input": 0
      }
    },
    {
      "from": {
        "node": "mixinvert",
        "output": 0
      },
      "to": {
        "node": "dry",
        "param": "gain"
      }
    }
  ],
  "automation": [],
  "parameters": [
    {
      "id": "bits",
      "label": "Bits",
      "range": {
        "min": "1",
        "max": "16"
      },
      "default": "6",
      "targets": [
        {
          "node": "gen",
          "param": "bits"
        }
      ]
    },
    {
      "id": "rate",
      "label": "Sample rate (Hz)",
      "range": {
        "min": "200",
        "max": "48000"
      },
      "default": "8000",
      "targets": [
        {
          "node": "gen",
          "param": "rate"
        }
      ]
    },
    {
      "id": "rateAmount",
      "label": "Rate amount (Hz)",
      "range": {
        "min": "0",
        "max": "12000"
      },
      "default": "4000",
      "targets": [
        {
          "node": "ratedepth",
          "param": "gain"
        }
      ]
    },
    {
      "id": "mix",
      "label": "Dry/wet",
      "range": {
        "min": "0",
        "max": "1"
      },
      "default": "1",
      "targets": [
        {
          "node": "wet",
          "param": "gain"
        },
        {
          "node": "mixdepth",
          "param": "gain"
        }
      ]
    }
  ],
  "sampleSlots": [],
  "createdAt": "2026-09-02T00:00:00.000Z"
}