ZMod

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.

Basic Usage

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)

Parameters

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()

Shorthand

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)

Notes

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)

Envelopes

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')

Using busses

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)

API

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.

Signals

sig(value: number): ControlSignal

Creates a new ControlSignal with the specified value.

Signal Operators

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 than
  • greaterthanzero
  • negate
  • gaintoaudio
  • audiotogain
  • pow
  • scale
  • scaleexp
sine(lfo(0.5,100,200).multiply(2))

LFOs

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

Envelope

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.

Oscillators

Basic Waveforms

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)

FM Oscillators

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)
)

AM Oscillators

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)
)

Pulse

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)

PWM

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

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

Noise generators from bright to dark:

(): AudioSignal
  • white
  • pink
  • brown

Modifiers

amp

AudioSignal.amp(value: ControlSignal): Gain

Controls the amplitude of the signal. The value can be a ControlSignal or a number.

sine(100).amp(lfo())

Filters

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')

hpf

High-pass filter.

AudioSignal.hpf(frequency?: ControlSignal, q?: ControlSignal, rolloff?: FilterRollOff): AudioSignal

lpf

Low-pass filter.

AudioSignal.lpf(frequency?: ControlSignal, q?: ControlSignal, rolloff?: FilterRollOff): AudioSignal

bpf

Band-pass filter.

AudioSignal.bpf(frequency?: ControlSignal, q?: ControlSignal, rolloff?: FilterRollOff): AudioSignal

fbf

Feedback comb filter.

AudioSignal.fbf(delayTime?: ControlSignal, resonance?: ControlSignal): AudioSignal

Effects

reverb

Reverb effect.

AudioSignal.reverb(wet?: ControlSignal, decay?: ControlSignal): AudioSignal

delay

Feedback delay effect.

AudioSignal.delay(wet?: ControlSignal, delayTime?: ControlSignal, feedback?: ControlSignal): AudioSignal

dist

Distortion effect.

AudioSignal.dist(wet?: ControlSignal, distortion?: ControlSignal): AudioSignal

chorus

Chorus effect.

AudioSignal.chorus(wet?: ControlSignal, frequency?: ControlSignal, feedback?: ControlSignal, depth?: ControlSignal): AudioSignal

Metering

follow

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),
)

Routing

pan

AudioSignal.pan(value?: ControlSignal): AudioSignal

Stereo panner. Values between 0 and 1. Converts the signal to stereo.

fm(100).pan(lfo())

input

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)

bus

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

Recording

loop

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)