{
  "$type": "at.atmosynth.module",
  "descriptionVersion": 1,
  "moduleId": "pulse",
  "versionKey": "1",
  "name": "Pulse oscillator",
  "description": "A square wave whose two halves need not be equal. Width is how much of each cycle is spent high: at a half it is the ordinary square, and away from a half it thins towards a reedy, hollow tone. Width is an input as well as a knob, and moving it with a slow wave is pulse width modulation — the shimmer a single oscillator makes when it sounds like more than one, and the thing the Juno-60 is built on. Detune works as it does on the other oscillators. This module carries code: the vocabulary's own square is fixed at half and its options cannot reach a width.",
  "audioInputs": [
    {
      "id": "width",
      "label": "Width"
    },
    {
      "id": "detune",
      "label": "Detune"
    }
  ],
  "audioOutputs": [
    {
      "id": "out",
      "label": "Out"
    }
  ],
  "midiInputs": [
    {
      "id": "midi",
      "label": "Note"
    }
  ],
  "nodes": [
    {
      "id": "note",
      "kind": "midiInput",
      "options": {
        "midiInput": "midi"
      }
    },
    {
      "id": "gen",
      "kind": "audioWorklet",
      "options": {
        "code": "// A pulse whose width can be moved, which the node vocabulary has no way to\n// make: its square is fixed at half and its options cannot reach a duty cycle.\n// The edges are softened with PolyBLEP, so a pulse played high does not alias\n// into a chord of frequencies nobody asked for.\nconst polyBlep = (t, dt) => {\n  if (t < dt) {\n    const x = t / dt;\n    return x + x - x * x - 1;\n  }\n  if (t > 1 - dt) {\n    const x = (t - 1) / dt;\n    return x * x + x + x + 1;\n  }\n  return 0;\n};\n\nclass AtmosynthPulse extends AudioWorkletProcessor {\n  static get parameterDescriptors() {\n    return [\n      { name: 'frequency', defaultValue: 0, minValue: 0, maxValue: 20000, automationRate: 'a-rate' },\n      { name: 'detune', defaultValue: 0, minValue: -4800, maxValue: 4800, automationRate: 'a-rate' },\n      // Never fully open or fully closed: at either the pulse is a flat line.\n      { name: 'width', defaultValue: 0.5, minValue: 0.02, maxValue: 0.98, automationRate: 'a-rate' },\n    ];\n  }\n\n  constructor() {\n    super();\n    this.phase = 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 hz =\n        at(parameters.frequency, index) * Math.pow(2, at(parameters.detune, index) / 1200);\n      const width = at(parameters.width, index);\n      const step = hz / sampleRate;\n\n      let value = this.phase < width ? 1 : -1;\n      value += polyBlep(this.phase, step);\n      value -= polyBlep((this.phase + 1 - width) % 1, step);\n      channel[index] = value;\n\n      this.phase += step;\n      if (this.phase >= 1) this.phase -= 1;\n    }\n\n    for (let other = 1; other < output.length; other += 1) output[other].set(channel);\n    return true;\n  }\n}\n\nregisterProcessor('atmosynth-pulse', AtmosynthPulse);\n",
        "declaredParams": [
          {
            "default": 0,
            "max": 20000,
            "min": 0,
            "name": "frequency"
          },
          {
            "default": 0,
            "max": 4800,
            "min": -4800,
            "name": "detune"
          },
          {
            "default": "0.5",
            "max": "0.98",
            "min": "0.02",
            "name": "width"
          }
        ],
        "numberOfInputs": 0,
        "numberOfOutputs": 1,
        "processorName": "atmosynth-pulse"
      }
    },
    {
      "id": "widthin",
      "kind": "moduleInput",
      "options": {
        "input": "width"
      }
    },
    {
      "id": "widthdepth",
      "kind": "gain",
      "options": {}
    },
    {
      "id": "detunein",
      "kind": "moduleInput",
      "options": {
        "input": "detune"
      }
    },
    {
      "id": "detunedepth",
      "kind": "gain",
      "options": {}
    },
    {
      "id": "out",
      "kind": "moduleOutput",
      "options": {
        "output": "out"
      }
    }
  ],
  "connections": [
    {
      "from": {
        "node": "note",
        "output": 0
      },
      "to": {
        "node": "gen",
        "param": "frequency"
      }
    },
    {
      "from": {
        "node": "gen",
        "output": 0
      },
      "to": {
        "node": "out",
        "input": 0
      }
    },
    {
      "from": {
        "node": "widthin",
        "output": 0
      },
      "to": {
        "node": "widthdepth",
        "input": 0
      }
    },
    {
      "from": {
        "node": "widthdepth",
        "output": 0
      },
      "to": {
        "node": "gen",
        "param": "width"
      }
    },
    {
      "from": {
        "node": "detunein",
        "output": 0
      },
      "to": {
        "node": "detunedepth",
        "input": 0
      }
    },
    {
      "from": {
        "node": "detunedepth",
        "output": 0
      },
      "to": {
        "node": "gen",
        "param": "detune"
      }
    }
  ],
  "automation": [],
  "parameters": [
    {
      "id": "width",
      "label": "Width",
      "range": {
        "min": "0.02",
        "max": "0.98"
      },
      "default": "0.5",
      "targets": [
        {
          "node": "gen",
          "param": "width"
        }
      ]
    },
    {
      "id": "widthAmount",
      "label": "Width amount",
      "range": {
        "min": "0",
        "max": "0.48"
      },
      "default": "0.3",
      "targets": [
        {
          "node": "widthdepth",
          "param": "gain"
        }
      ]
    },
    {
      "id": "detune",
      "label": "Detune (cents)",
      "range": {
        "min": "-1200",
        "max": "1200"
      },
      "default": "0",
      "targets": [
        {
          "node": "gen",
          "param": "detune"
        }
      ]
    },
    {
      "id": "detuneAmount",
      "label": "Detune amount (cents)",
      "range": {
        "min": "0",
        "max": "1200"
      },
      "default": "50",
      "targets": [
        {
          "node": "detunedepth",
          "param": "gain"
        }
      ]
    }
  ],
  "sampleSlots": [],
  "createdAt": "2026-08-29T00:00:00.000Z"
}