โŒ˜K

Synth blocks

Oscillators, voices, envelopes, LFOs, meters, and random sources.

The synth and modulation parts ship in @msh/device-sdk/device/assembly. Build them once in prepare, drive them per sample in process.

let osc: Oscillator;
let env: Envelope;
prepare(sampleRate: f64, maxBlock: i32): void {
  osc = new Oscillator(sampleRate as f32);
  osc.setWaveform(Waveform.Saw);
  env = new Envelope(sampleRate as f32);
}

Warning

Construct these in prepare, never in process. The why is worth a read.

Oscillators and voices

BlockBuildUse
Oscillatornew Oscillator(sr)setWaveform(Waveform.Saw), process(freqHz) per sample. Band-limited (PolyBLEP).
VoiceAllocatornew VoiceAllocator(count)noteOn(note) / noteOff(note) return a voice index (steals oldest when full); noteOf(voice), reset().
VoiceBank<T>new VoiceBank<T>(count, (i) => new T(...))get(i), count, norm (= 1/count, multiply a summed mix by it).
AudioBuffernew AudioBuffer(maxFrames, channels?)push(l, r?) to record, read(ch, fractionalFrame) cubic-interpolated playback; at, set, length, reset.
KarplusStrongnew KarplusStrong(sr, minFreqHz?)Plucked string / resonator: setPitch(hz), setDamping(0..1), setFeedback(0..1), process(x) (excite with a noise burst or live audio). setDispersion(-1..1): positive = stiff-string inharmonicity (piano bass, bells), negative = amplitude-dependent curved-bridge buzz (sitar). Re-call setPitch after changing damping or dispersion.

Waveform values: Sine, Saw, Square, Triangle.

A polyphonic synth is a VoiceAllocator paired with a VoiceBank of per-voice DSP:

voices = new VoiceBank<Oscillator>(
  8,
  (_i) => new Oscillator(sampleRate as f32),
);
alloc = new VoiceAllocator(8);
// in process, on a note-on: const v = alloc.noteOn(m.note);
// then sum voices: sum += voices.get(v).process(freq); out = sum * voices.norm;

Envelopes and LFOs

BlockBuildUse
Envelopenew Envelope(sr)AHDSR. noteOn(peak, attackS, holdS, attackCurve?), noteOff(releaseS, releaseCurve?), tick(holdS, decayS, sustain, decayCurve?) per sample, isActive().
Lfonew Lfo(sr)setShape(LfoShape.Sine), setFreq(hz) or setTempoSync(beats, bpm), setPhase(p), retrigger(), process() (-1..1), processUnipolar() (0..1).
RandomLfonew RandomLfo(sr, seed?, smoothingMs?)process(rateHz, sr): a smoothed random LFO. setSmoothing(sr, timeMs).
LowPassGatenew LowPassGate(sr)process(x, gate): one control opens amplitude AND a gentle low-pass together through a vactrol-style lag (fast attack, slow decay): the west-coast pluck. setColor(bleed), setRange(minHz, maxHz), setResponse(attackMs, decayMs), gateValue(). Feed a raw gate or an envelope.

Stage times pass to tick each sample, so they modulate freely; noteOn latches attack, hold, and peak. attackCurve / releaseCurve / decayCurve are in -1..1 (0 = linear).

LfoShape values: Sine, Triangle, Saw, Ramp, Square, SampleHold, Random, Exp.

Metering and detection

BlockBuildUse
EnvelopeFollowernew EnvelopeFollower(sr, attackMs, releaseMs)process(x) tracks the peak envelope; value(), setTimes(a, r), reset().
RmsMeternew RmsMeter(sr, windowMs)process(x) returns sliding-window RMS; reset().
PeakMeternew PeakMeter(sr, releaseMs)process(x) instant attack, exponential release; setRelease(ms), reset().
Triggernew Trigger()process(x, threshold) true on the rising edge; reset().
Gatenew Gate()process(x, on, off) Schmitt gate with hysteresis (off < on); isOpen(), reset().
StrumDetectornew StrumDetector(sr, minIntervalMs?)process(x) true on the sample a pluck/hit lands: 3-band energy analysis, AGC, adaptive threshold, minimum inter-onset spacing. Pair it with KarplusStrong / ModalBank to strum a resonator from live audio.
HysteresisQuantizernew HysteresisQuantizer()process(value, numSteps, hysteresis?) quantizes a 0..1 value to an integer step without flutter at the boundaries: modes, wavetable frames, or scale degrees driven by a modulated param. value() reads the last step.

Random sources

BlockBuildUse
Rngnew Rng(seed?)uniform() ([0,1)), bipolar() ([-1,1)), nextU32(), reseed(seed). Seedable xorshift32.
Noisenew Noise(seed?)white() (-1..1), pink() (Paul Kellet, roughly -3 dB/oct).
SampleHoldnew SampleHold(seed?)process(rateHz, sr) re-rolls when the phase wraps; trigger() forces a new value now; value().

All random sources are deterministic for a given seed, so a patch sounds the same on reload.

Stereo helpers

HelperDoes
panGainL(pan) / panGainR(pan)Equal-power pan gain for the left / right channel; pan in -1..1.
MidSideencode(l, r) then mid() / side(); recombine with decodeL(m, s) / decodeR(m, s).

For audio filters, delays, and drive, see DSP blocks. For pure math and curve helpers, see Helpers.