Write a Modulation Source
When the built-in LFOs and envelopes don’t do what you need, write a dsp signal. It is compiled
like an effect, returns one value per sample, and binds with << wherever a built-in signal does.
Write a signal and bind it
Section titled “Write a signal and bind it”A signal has one entry point, fn process() -> out:
use "std/instruments" { Sampler, Kit };use "std/effects" { Lowpass };
dsp signal Ramp { range: 0..1 state phase: 0.0;
fn process() -> out { phase = __native("fract", phase + CPS * INV_SR); return phase; }}
let drums = AudioTrack("drums");drums.set_instrument(Sampler(Kit("CR-78")));drums << [bd sd bd sd];
let filter = Lowpass("lowpass", 2000);drums.set_effects(#[filter]);filter.parameter("Cutoff") << Ramp().range_exp(300, 4000);
PLAY;phase moves on by CPS * INV_SR in each sample — one whole turn per cycle. Ramp() makes an
instance, and every signal method works on it: .range_exp maps it onto the cutoff.
A signal’s output is taken to go from -1 to 1. range: 0..1 says this one goes from 0 to
1, so .range and .range_exp map the right span.
Give it parameters
Section titled “Give it parameters”param works as in an effect. A signal’s params are the arguments of its constructor:
dsp signal Wobble { range: 0..1 param speed: 4.0 range(0.1, 40); param depth: 0.5 range(0, 1); state phase: 0.0;
fn process() -> out { phase = __native("fract", phase + speed * CPS * INV_SR); return 0.5 + depth * 0.5 * __native("sin", phase * TWOPI); }}
let slow = Wobble(0.5);let shallow = Wobble(speed: 8, depth: 0.2);let wobbly = Wobble(Wobble(0.25).range(2, 8)); // speed moved by another signalA param given a signal follows it sample by sample.
Lock it to the beat
Section titled “Lock it to the beat”The clock constants follow the transport, so you don’t need a phase of your own. CYCLE_PHASE
and BEAT_PHASE ramp from 0 to 1 over each cycle or beat:
dsp signal CycleRamp { range: 0..1
fn process() -> out { return CYCLE_PHASE; }}BEAT_EDGE and CYCLE_EDGE are 1 on the one sample where a beat or cycle starts. Use them to
sample and hold — here, a new random value on every beat:
dsp signal BeatRandom { range: 0..1 state held: 0.0;
fn process() -> out { if BEAT_EDGE > 0.0 { held = __native("noise") * 0.5 + 0.5; } else { held = held; } return held; }}Every constant is listed in the DSP builtins.
Process another signal
Section titled “Process another signal”Give process arguments, and the signal takes other signals as inputs. They come first in the
constructor, before the params:
use "std/signals" { Sine };
dsp signal Quantize { param steps: 4 range(1, 32);
fn process(source) -> out { return __native("round", source * steps) / steps; }}
let stepped = Quantize(Sine(2)); // input, positionallylet finer = Quantize(source: Sine(2), steps: 16);let still = Quantize(0.5); // a number is a constant signalFollow a MIDI controller
Section titled “Follow a MIDI controller”cc, pitchbend and aftertouch read the live MIDI input:
dsp signal CcSmooth { range: 0..1 state value: 0.0;
fn process() -> out { value = value + (__native("cc", 74) - value) * 0.001; return value; }}This follows CC 74 on any channel, and glides to each new value instead of stepping. To read one
channel only, give it first: __native("cc", 0, 74).
Related
Section titled “Related”- Sweep a Parameter with an LFO — the built-in LFOs, and what a signal can be bound to
- Signals & Time — when a bound signal starts and restarts
- DSP Blocks — everything a
dsp signalcan declare