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