Sound

Instruments played from Python, and the audio stream.

Instrument

Instrument(polyphony=_shared.DEFAULT_POLYPHONY)

Something you play with notes and shape with named controls.

Subclasses (Synth) choose the sound; this base class carries what every instrument shares: note_on, note_off, control_change and the volume control.

Creating an instrument loads the sound engine and, unless KILN_AUDIO is offline, opens the default audio device (see SoundStream). It sounds for as long as the object exists: when the last reference goes, its voice stops. Notes and controls are written to shared memory and picked up by the audio thread at its next buffer, within about 5 ms.

Named controls are attributes, and every write glides over about 5 ms, so turning a knob every frame never clicks. control_change(name, value) is the same write by name. Values outside a control’s range are clamped.

Notes are queued in order and never dropped: ten note_on calls in one frame play ten notes, and a note_on with its note_off in the same update() still sounds for one buffer (about 5 ms).

An instrument holds up to polyphony notes at once (8 by default), so several note_on calls make a chord. A note beyond that releases the oldest held note to make room (voice stealing). With polyphony=1 the instrument is mono and legato: a note_on while a note is held moves that note to the new pitch, gliding, without starting it again. That is how you bend a note: call note_on with each new pitch while the mouse is down. After a note_off, the next note starts fresh.

A voice budget keeps the sound from overrunning the audio thread: at most 16 voices sound per instrument (held notes and fading release tails together; polyphony is clamped to 16) and at most 24 synth voices’ worth across all instruments (a drum hit costs a fraction of one). Past that, the quietest release tail, else the oldest held note, fades out within 5 ms (less than one voice over, as drums make it, is let be), and a burst of new synth notes beyond the total waits one buffer (about 5 ms) for room; drum hits never wait. A release tail stops once it falls below -90 dBFS.

Parameters

Name Type Description Default
polyphony int How many notes it holds at once, 1 to 16 (more is clamped to 16). 1 is mono legato. _shared.DEFAULT_POLYPHONY

Attributes

Name Type Description
volume Output level, 0 to 1 (default 0.5).
pan Stereo position, -1 (left) to 1 (right), default 0 (centre). Equal power: a sound panned to one side is as loud as in the centre.
polyphony int How many notes it holds at once, 1 to 16; changing it applies from the next note.
fx list This instrument’s effects, in order (a list of Effect; empty by default). Assign a new list to change them: effects that leave fade out and new ones fade in, over 20 ms, so it never clicks. kiln.master.fx processes every instrument.

Methods

Name Description
control_change Sets a named control; the sound glides to it over about 5 ms.
note_off Releases held notes.
note_on Starts a note.

control_change

Instrument.control_change(name, value)

Sets a named control; the sound glides to it over about 5 ms.

Controls are named, not numbered. synth.control_change("cutoff", 800) is synth.cutoff = 800.

Parameters
Name Type Description Default
name str A control of this instrument, such as "cutoff" or "volume". required
value float | str The new value, clamped to the control’s range (or an option’s name). required
Raises
Name Type Description
ValueError name is not a control of this instrument, or value is NaN.

note_off

Instrument.note_off(freq=None, velocity=0.0)

Releases held notes.

Parameters
Name Type Description Default
freq float | None Release the held notes at this pitch (the value given to note_on, matched within a quarter semitone); None releases every held note. A pitch that is not held does nothing. With polyphony=1 only the latest pitch is held: there is no note stack, so releasing it is silence even if an earlier key is still down. None
velocity float Release velocity, accepted and unused for now. 0.0

note_on

Instrument.note_on(freq, velocity=1.0)

Starts a note.

With polyphony=1 and a note held, moves that note to freq instead (legato; its velocity stays).

Parameters
Name Type Description Default
freq float Pitch in Hz, above 0. Pitches above half the sample rate (the highest the stream can play) are clamped to it. required
velocity float Loudness, 0 to 1 (clamped). 1.0
Raises
Name Type Description
ValueError freq is not a positive, finite number, or velocity is not finite.

Synth

Synth(polyphony=_shared.DEFAULT_POLYPHONY)

A classic analog synth voice: two oscillators, a resonant filter, two envelopes, an LFO.

Built the way a Minimoog or a Prophet is, from virtual-analog models (see kiln.sound.dsp):

osc1, osc2, sub, noise -> mixer -> drive -> filter -> amp

Two band-limited oscillators (osc1_wave, osc2_wave: "sine", "triangle", "saw", "pulse"), osc2 detune cents above osc1, a square sub-oscillator an octave down and noise are mixed and driven into a filter: a resonant 24 dB "ladder" low-pass, or a 12 dB state-variable "lp", "bp" or "hp". An amp envelope shapes the loudness of each note; a filter envelope moves the cutoff (and, with pitch_env_amount, the pitch); an LFO wobbles pitch, cutoff or pulse width. Each oscillator drifts slightly, as analog ones do.

synth = kiln.Synth()
synth.filter_type = "ladder"
synth.cutoff = 800.0          # darker, glided over about 5 ms
synth.resonance = 0.7         # a whistling peak at the cutoff
synth.note_on(110)
synth.note_off()

Every attribute below is a control: set it at any time, or use control_change(name, value). Numbers outside a range are clamped.

Attributes

Name Type Description
osc1_wave Oscillator 1 waveform: “sine”, “triangle”, “saw” (default) or “pulse”.
osc2_wave Oscillator 2 waveform, the same choices (default “saw”).
detune Oscillator 2’s pitch above oscillator 1, cents, -1200 to 1200 (default 7: a slow, thick beating; 700 is a fifth, 1200 an octave).
pulse_width Duty cycle of the “pulse” waveform, 0.05 to 0.95 (default 0.5: square).
drift How far each oscillator wanders off pitch, cents, 0 to 50 (default 3).
osc1_level Oscillator 1 in the mixer, 0 to 1 (default 1).
osc2_level Oscillator 2 in the mixer, 0 to 1 (default 0.7).
sub_level Square sub-oscillator one octave below oscillator 1, 0 to 1 (default 0).
noise_level Noise in the mixer, 0 to 1 (default 0).
noise_color “white” (default, hiss) or “pink” (rumble, -3 dB per octave).
filter_type “ladder” (default; 24 dB low-pass), “lp”, “bp” or “hp” (12 dB).
cutoff Filter cutoff, Hz, 20 to 20000 (default 2000).
resonance Emphasis at the cutoff, 0 to 1 (default 0). The ladder whistles on its own (self-oscillates) above about 0.95.
keytrack How much the cutoff follows the note, 0 to 1 (default 0; at 1 a note an octave up gets a cutoff an octave up, around middle C).
drive Filter input level, 0 to 1 (default 0.2): from warm to fuzzy.
attack Amp envelope attack, seconds, 0 to 20 (default 0.005).
decay Amp envelope decay to the sustain level, seconds, 0 to 20 (default 0.3).
sustain Amp level while the note is held, 0 to 1 (default 1). At 0 the voice frees itself once it falls silent, so a later note_off does nothing.
release Amp envelope release after note_off, seconds, 0 to 20 (default 0.15).
filter_attack Filter envelope attack, seconds, 0 to 20 (default 0.005).
filter_decay Filter envelope decay, seconds, 0 to 20 (default 0.4).
filter_sustain Filter envelope sustain level, 0 to 1 (default 0.3).
filter_release Filter envelope release, seconds, 0 to 20 (default 0.2).
filter_env_amount How far the filter envelope moves the cutoff at its peak, octaves, -8 to 8 (default 0).
pitch_env_amount How far the filter envelope bends the pitch at its peak, semitones, -48 to 48 (default 0).
lfo_rate LFO speed, Hz, 0.01 to 50 (default 5).
lfo_shape “sine” (default), “triangle”, “saw”, “square” or “sample_hold” (random steps).
lfo_to_pitch Vibrato depth, semitones, -24 to 24 (default 0).
lfo_to_cutoff Wah depth, octaves, -8 to 8 (default 0).
lfo_to_pwm Pulse-width modulation depth, -0.45 to 0.45 (default 0).
glide Portamento with polyphony=1: how long the pitch takes to slide to a note played while another is held, seconds, 0 to 10 (a time constant; default 0, with a 5 ms minimum).
volume Output level, 0 to 1 (default 0.5).

SoundStream

SoundStream()

The audio device stream.

Optional: the first instrument opens the default device with the default settings on its own. Call open to choose other settings or another device. Kiln has one stream: open replaces the one already open, and every SoundStream object reports its settings. After close, existing instruments are silent until open (or a new instrument) opens it again.

If the device goes away while playing (headphones pulled, a Bluetooth speaker switched off), Kiln reopens the stream on its own within about a second, on the default device if the one chosen is gone.

There is no audioOut callback: sound comes from instruments, which run in compiled code on the audio thread, so a busy draw() never interrupts it.

Under KILN_AUDIO=offline nothing opens the device: open only records the settings, and kiln.sound.offline.render renders the sound into an array with them.

stream = kiln.SoundStream()
stream.open(sample_rate=48000, buffer_size=256)

Attributes

Name Type Description
sample_rate int Samples per second, per channel.
buffer_size int Frames per buffer; smaller is lower latency and more work.
output_channels int Output channels; the mix’s left and right go to channels 0 and 1, and any further channels are silent.
input_channels int Input channels (the first one is recorded for analysis).
device int | str | None The device asked for: None (the default), an index, or part of a name.

Methods

Name Description
close Stops and closes the stream.
open Opens the audio device and starts the stream.
start Starts the stream again after stop.
stop Pauses the stream; start resumes it.

close

SoundStream.close()

Stops and closes the stream.

The next instrument created opens the default stream again.

open

SoundStream.open(
    sample_rate=48000,
    buffer_size=256,
    output_channels=2,
    input_channels=0,
    device=None,
)

Opens the audio device and starts the stream.

Parameters
Name Type Description Default
sample_rate int Samples per second, per channel. 48000
buffer_size int Frames per buffer, 1 to 4096. 256 frames at 48 kHz is 5.3 ms. 256
output_channels int Output channels, at least 1. 2
input_channels int Input channels; 0 opens no input. 0
device int | str | None None for the default device, a PortAudio device index, or part of a device name (case-insensitive). None
Raises
Name Type Description
ImportError The “sound” extra is not installed.
ValueError A setting is out of range, or no device matches device.
RuntimeError PortAudio could not open the stream.

start

SoundStream.start()

Starts the stream again after stop.

stop

SoundStream.stop()

Pauses the stream; start resumes it.

Sequencer

Sequencer(bpm=120.0, swing=0.0, steps_per_beat=4)

Plays instruments in steps, on the beat to the sample, and keeps time for visuals.

Give each instrument a pattern of hits, a line of notes or a progression of chords, one per step, then start(). Steps are 16th notes by default (steps_per_beat=4) and bars have four beats. Each instrument’s steps loop on their own length, so a 3-step pattern against a 16-step one makes a polyrhythm.

seq = kiln.Sequencer(bpm=122)
seq.pattern(kick, "x...x...x...x...")
seq.pattern(hat, "..x...x...x...xX")
seq.notes(bass, ["A1", "-", None, "A1", "C2", None, "E2", "-"])
seq.chords(pad, [["A3", "C4", "E4"], "-", "-", "-", ["F3", "A3", "C4"], "-", "-", "-"])
seq.ramp(bass, "cutoff", 200, 2000, bars=8, at_bar=4)
seq.start()

In draw(), beat, bar and phase tell where the music is, as it is heard: 1 - seq.phase is a pulse that peaks on every beat.

Every note, and the start of every ramp, is on its exact sample. The sequencer works ahead of the music (LOOKAHEAD_S, 0.1 s): kiln’s frame loop asks it, once per frame, for the notes due in the next 0.1 s, which go to the audio thread with their sample time. So it plays while a sketch runs (kiln.run) or while kiln.sound.offline.render renders, and frames may come late by up to 0.1 s without a note moving.

A Sequencer plays inside a kiln sketch, or with offline.render. In a plain script, the REPL or a notebook nothing asks it for the next steps: on an audio device it plays 0.1 s and stops, and start() warns. A started sequencer plays until stop() or the end of the sketch’s run, even if you keep no reference to it.

If the main thread stalls for longer than that, the steps whose time has already passed are skipped, not played late in a burst; the sequencer carries on in time from the next step. Changes (a new pattern, bpm, swing, stop) apply to the steps not scheduled yet, about 0.1 s ahead.

Parameters

Name Type Description Default
bpm float Tempo, beats per minute, 20 to 999. 120.0
swing float How late every second step of a beat comes (steps 1, 3, … counted from the beat), as a share of a step, 0 (straight) to below 1. 0.2 delays the off-beat 16ths by a fifth of a step; about 0.33 is a triplet shuffle. The beat itself is never moved, so with 3 steps a beat only the middle one swings. 0.0
steps_per_beat int Steps in a beat: 4 for 16th notes, 2 for 8ths, 3 for triplets. 4

Attributes

Name Type Description
bpm float Tempo, beats per minute; a change applies from the next step.
swing float The swing amount; a change applies from the next step.
playing bool True from start() until stop() (read-only).

Methods

Name Description
chords Gives instrument a chord progression, one step at a time.
notes Gives instrument a line of notes, one per step.
pattern Gives instrument a rhythm: one character per step.
ramp Moves a control of instrument from start to end over bars bars.
start Starts playing from step 0, bar 0. Does nothing while it plays.
stop Stops scheduling steps.

chords

Sequencer.chords(instrument, steps, gate=DEFAULT_GATE)

Gives instrument a chord progression, one step at a time.

A step is a list of notes (names such as "A3", "C#4", or MIDI numbers), None for a rest, or "-" to hold the chord before it for one more step (a tie). All the notes of a chord start on the same sample and are released together, after gate of the chord’s last step. chords is to progressions what notes is to melodies, and replaces the instrument’s previous pattern, notes or chords, from the next step; an empty list removes the instrument.

seq.chords(pad, [["A3", "C4", "E4"], "-", "-", "-",
                 ["F3", "A3", "C4"], "-", "-", None])

A chord that ends rings out its release while the next chord starts, so the instrument needs a voice for each: set its polyphony to at least twice the biggest chord (8 for triads and four-note chords, the default). With fewer, the new chord steals the voices still releasing and cuts that tail short; a warning says so.

Parameters
Name Type Description Default
instrument The instrument to play, usually a Synth with a slow release. required
steps list The progression, such as [["A3", "C4", "E4"], "-", None]. required
gate float How much of its last step a chord is held, above 0 to 1 (default 0.5). DEFAULT_GATE
Raises
Name Type Description
ValueError A step that is not a list of notes, None or "-", an empty chord, or a bad gate.

notes

Sequencer.notes(instrument, notes, gate=DEFAULT_GATE)

Gives instrument a line of notes, one per step.

Each step is a note name ("A1", "C#4", "Eb3"; A4 is 440 Hz), a MIDI number (45), None for a rest, or "-" to hold the note before it for one more step (a tie). A note is released after gate of its last step. The line replaces the instrument’s previous pattern or notes, from the next step; an empty list removes the instrument.

Parameters
Name Type Description Default
instrument The instrument to play. required
notes list The steps, such as ["A1", "-", None, "C2"]. required
gate float How much of its last step a note is held, above 0 to 1 (default 0.5). DEFAULT_GATE
Raises
Name Type Description
ValueError A step that is not a note, None or "-", or a bad gate.

pattern

Sequencer.pattern(instrument, steps, note=None, gate=DEFAULT_GATE)

Gives instrument a rhythm: one character per step.

x is a hit, X an accented (louder) hit, . a rest; spaces and | are ignored, so "x... x... | x... x..." reads in beats. A hit is a note_on at note (drums, which are one-shots, get no note_off); synths are released after gate of the step. The pattern replaces the instrument’s previous pattern or notes, from the next step; an empty pattern removes the instrument.

Parameters
Name Type Description Default
instrument The instrument to play (a Synth, a drum, …). required
steps str The rhythm, such as "x...x...X..x....". required
note The pitch of every hit: a name (“A1”, “C#4”) or a MIDI number. By default a drum plays at its tune (as hit() does), anything else at C4. None
gate float How much of a step a hit is held, above 0 to 1 (default 0.5). DEFAULT_GATE
Raises
Name Type Description
ValueError An unknown character in steps, or a bad note or gate.

ramp

Sequencer.ramp(instrument, control, start, end, bars, at_bar=None)

Moves a control of instrument from start to end over bars bars.

Long automation, such as a 16-bar filter opening. The ramp begins on a bar line, exactly on that bar’s first sample, and reaches end on the bar line bars later, then stays there. Its start is on the sample because the whole ramp goes to the audio thread as timed control events, like notes: the first value is queued up to RAMP_ARM_S (2 s) before its bar, the rest ahead of the music within LOOKAHEAD_S, a value about every 5 ms along a straight line (the control’s own glide smooths them), and nothing depends on a frame arriving near the bar. Issue it many bars early, or call it from setup() with at_bar; a main thread that stalls through the bar still gets the ramp started on time. A stall longer than 0.1 s, once the ramp is under way, pauses it where it is, and when frames resume it jumps (one step, the control’s glide smoothing it) to the place it should have reached.

Ramps on one control queue in time order: a later ramp takes over from its own start, and the earlier one runs until then (two asked for the same bar: the later wins). A tempo change after the first value is queued (the 2 s before the bar) does not move that start.

Parameters
Name Type Description Default
instrument The instrument whose control moves. required
control str The control’s name, such as "cutoff" or "volume". required
start float The value on the first bar line. required
end float The value bars bars later. required
bars float How long the ramp lasts, in bars (above 0). required
at_bar float | None The bar it starts on, counted from 0 at start(). By default the next bar line (bar 0 before start()). A bar that has already passed starts the ramp at once, at the value it would have by now. None
Raises
Name Type Description
ValueError control is not a control of instrument, bars is not above 0, or at_bar is negative.

start

Sequencer.start()

Starts playing from step 0, bar 0. Does nothing while it plays.

The first step sounds LOOKAHEAD_S (0.1 s) after the call on an audio device, at once when rendering offline. It plays on a kiln sketch’s frames (kiln.run) or in offline.render; started on an audio device with no sketch running, it warns, because nothing would play past 0.1 s.

stop

Sequencer.stop()

Stops scheduling steps.

The steps already scheduled (up to LOOKAHEAD_S, 0.1 s) still play, and a note held across a tie is released where the next step would have started. beat stops there too. start() plays again from the beginning. Every sequencer still playing stops when the sketch’s run ends.

note_to_freq

note_to_freq(note)

The pitch of a note in Hz, from a name (“A1”, “C#4”, “Eb3”; A4 is 440 Hz) or a MIDI number.

note_to_freq("A4") and note_to_freq(69) are both 440.0, and a MIDI number may be fractional (69.5 is a quarter tone up). It is what synth.note_on wants::

synth.note_on(kiln.note_to_freq("C#3"))

Raises

Name Type Description
ValueError Not a note name or a finite number.