ZMod (Zen Modular) lets you build your own modular synth patches using a simple JavaScript API. It works like a rack of connected sound modules—oscillators, filters, effects, etc.
Set the inst
to zmod
and provide a patch string using ZMod syntax:
s0.set({ inst: 'zmod', patch: 'sine(100).amp(0.5).pan(0.5)' })
s0.e.every(16)
The example above plays a constant drone. You can control its parameters with signals. sig
takes a name and an optional default value:
s0.set({ inst: 'zmod', patch: "sine(sig('freq', 100)).amp(sig('amp')).pan(sig('pan'))" })
Now you can update parameters live on the stream:
s0.freq.set('100 200 300 400')
s0.amp.set(0.5)
s0.pan.saw()
Use a #
prefix instead of sig
for simpler code:
s0.set({ inst: 'zmod', patch: 'sine(#freq).amp(#amp).pan(#pan)' })
s0.freq.set('100 200 300 400')
s0.amp.set(0.5)
s0.pan.saw()
s0.e.every(4)
Use #n
for note values wherever you would supply a frequency. It will automatically convert midi note numbers to Hz:
s0.set({ inst: 'zmod', patch: 'sine(#n).amp(0.5).pan()' })
s0.n.set('Ddor%16..*16')
s0.e.every(1)
Use #e
to create an envelope, which will be triggered by the stream's e
parameter:
s0.set({ inst: 'zmod', patch: 'sine(#n).amp(#e).pan()'})
s0.n.set('Ddor%16..*16')
s0.e.set('3:8')
This automatically maps the a
, d
, s
, and r
parameters to the envelope's attack, decay, sustain, and release times:
s0.set({ inst: 'zmod', patch: 'sine(#n).amp(#e).pan()'})
s0.n.set('Ddor%16..*16')
s0.a.set('100')
s0.d.set('100')
s0.s.set('0.5')
s0.r.set('1000')
s0.e.set('3:8')
Need more envelopes? Use #e1
, #e2
, etc. to create additional envelopes. These map to a1
, d1
, s1
, r1
, and a2
, d2
, s2
, and r2
, etc.
const patch = `
saw(#n).amp(#e).pan()
.reverb(0.75,5000)
.lpf(#e1.scale(50,5000),.9)
`
s0.set({ inst: 'zmod', patch, a: 10, a1: 500, s1:0.1, r1:0})
s0.n.set('Ddor%8..*16')
s0.e.set('3:8')
ZMod supports routing signals to and from busses, allowing you to create complex patches with multiple signal paths. Use the bus
method to route a signal to a bus, and the bus
function to retrieve a signal from a bus. You can also route audio from non-ZMod instruments to a bus, for use in ZMod patches.
s0.set({
inst: 1, bank: 'bd', // play a kick drum
bus0: 1, // route to bus 0
})
s0.e.set('3:8')
// use the bus to modulate frequency and filter cutoff
let patch = `
saw(#n.add(bus(0).follow().scale(10,500)))
.amp(#e)
.dist()
.lpf(bus(0).follow().scale(1000,5000))
.pan()
`
s1.set({inst: 'zmod', patch, n: 36, s: 1, a: 50, dur: $(4).btms()})
s1.e.every(16)
There are two main types of signal in ZMod: ControlSignal
and AudioSignal
. ControlSignals
, including LFOs and Envelopes, can usually be passed to any AudioSignal
method where a number is expected.
sig(value: number): ControlSignal
Creates a new ControlSignal
with the specified value.
These methods can be applied to a ControlSignal
, and passed one or more number
arguments:
ControlSignal.fn(...args: number[]): ControlSignal
Available functions:
abs
add
mul
sub
gt
// greater thangreaterthanzero
negate
gaintoaudio
audiotogain
pow
scale
scaleexp
sine(lfo(0.5,100,200).multiply(2))
Generates a ControlSignal
that oscillates between the specified minimum and maximum values at the specified frequency.
(frequency: ControlSignal, min: number = 0, max: number = 1): ControlSignal
lfo
lfosine
lfotri
lfosquare
lfosaw
Creates a ControlSignal
controlled by an ADSR envelope (all time values in milliseconds).
adsr(attack?: number, decay?: number, sustain?: number, release?: number): ControlSignal
sine(100).amp(adsr())
Envelopes must be triggered from outside the patch. See Basic Usage above.
All oscillators generate an AudioSignal
at the specified frequency. The frequency can be a ControlSignal
or a number.
(freq: ControlSignal = 220): AudioSignal
sine
tri
square
saw
sine(100)
Frequency Modulation (FM) oscillators allow for complex timbres by modulating the frequency of a carrier wave with another signal. The harm
parameter controls the harmonicity ratio (pitch relationship between the two oscillators), and modi
controls the modulation index (amplitude of the modulator oscillator).
(freq: ControlSignal = 220, harm: ControlSignal = 1, modi: ControlSignal = 1): AudioSignal
fm
fmsine
fmtri
fmsquare
fmsaw
fmsaw(
100,
lfosaw(0.5,0.5,10),
lfosine(0.25,1,10)
)
Amplitude Modulation (AM) oscillators modulate the amplitude of a carrier wave with another signal. The harm
parameter controls the harmonicity ratio, similar to FM oscillators.
(freq: ControlSignal = 220, harm: ControlSignal = 1): AudioSignal
am
amsine
amtri
amsquare
amsaw
fmsaw(
lfosaw(0.125,100,1000),
lfosaw(0.5,0.5,10),
lfosine(0.25,1,10)
)
A pulse wave is a square wave with a variable duty cycle, controlled by the width
parameter (0 to 1). A width of 0.5 produces a standard square wave.
pulse(freq: ControlSignal, width: ControlSignal): AudioSignal
pulse(100, 0.5)
Pulse Width Modulation (PWM) oscillators modulate the width of a pulse wave with another signal, typically an LFO. The modFreq
parameter controls the frequency of the modulation.
pwm(freq: ControlSignal, modFreq: ControlSignal): AudioSignal
pwm(100, lfo(0.125,0.5,2))
Fat oscillators are designed to produce a richer sound by combining multiple oscillators. The spread
parameter controls the detuning between the oscillators, creating a thicker sound.
(freq: ControlSignal = 220, spread: number = 10): AudioSignal
fat
fatsine
fattri
fatsquare
fatsaw
Noise generators from bright to dark:
(): AudioSignal
white
pink
brown
AudioSignal.amp(value: ControlSignal): Gain
Controls the amplitude of the signal. The value can be a ControlSignal
or a number.
sine(100).amp(lfo())
Control the frequency content of the signal. All filters take a frequency
parameter, which can be a ControlSignal
or a number. The q
parameter controls the resonance, and rolloff
controls the filter's roll-off rate.
saw(100).lpf(lfo(0.5,100,2000), 0.5, '12')
High-pass filter.
AudioSignal.hpf(frequency?: ControlSignal, q?: ControlSignal, rolloff?: FilterRollOff): AudioSignal
Low-pass filter.
AudioSignal.lpf(frequency?: ControlSignal, q?: ControlSignal, rolloff?: FilterRollOff): AudioSignal
Band-pass filter.
AudioSignal.bpf(frequency?: ControlSignal, q?: ControlSignal, rolloff?: FilterRollOff): AudioSignal
Feedback comb filter.
AudioSignal.fbf(delayTime?: ControlSignal, resonance?: ControlSignal): AudioSignal
Reverb effect.
AudioSignal.reverb(wet?: ControlSignal, decay?: ControlSignal): AudioSignal
Feedback delay effect.
AudioSignal.delay(wet?: ControlSignal, delayTime?: ControlSignal, feedback?: ControlSignal): AudioSignal
Distortion effect.
AudioSignal.dist(wet?: ControlSignal, distortion?: ControlSignal): AudioSignal
Chorus effect.
AudioSignal.chorus(wet?: ControlSignal, frequency?: ControlSignal, feedback?: ControlSignal, depth?: ControlSignal): AudioSignal
Envelope follower. In this example, we use the amplitude of the signal in the left channel to modulate the pitch of signal in the right channel.
stack(
sine(100).amp(lfo()).out(0),
sine(out(0).follow().multiply(1000).add(100)).amp(0.5).out(1),
)
AudioSignal.pan(value?: ControlSignal): AudioSignal
Stereo panner. Values between 0 and 1. Converts the signal to stereo.
fm(100).pan(lfo())
AudioSignal.input(index: number): AudioSignal
Routes the signal from an input. Due to the limitation of the Web Audio API, only the first two inputs on your audio device are available.
input(0)
When used as method, routes the signal to a bus.
AudioSignal.bus(index: number): AudioSignal
When used as a function, routes the signal from a bus. A 10ms delay is applied to prevent feedback.
bus(index: number): AudioSignal
A looper - needs a separate chapter as it's such a useful feature!
AudioSignal.loop(gain: ControlSignal, length: ControlSignal, rate: ControlSignal, clear: ControlSignal): AudioSignal
s0.patch.set(`
fm(#_n)
.amp(#e)
.pan(lfo())
.loop(#record,#length,#rate,#clear)
`)
s0.record.set('0') // 0 stops recording
s0.length.set('4') // in beats
s0.rate.set('1') // buffer speed - 1 is normal speed, 0.5 is half speed, 2 is double speed, negative numbers are reverse. Best to manipulate when recording is off
s0.clear.set(0) // 1 clears the loop
s0._n.set('Ddor%16..?*16')
s0.e.every(7).or(every(5))
s0.m.not(s0.e)