import numpy as np
from browseraudio import keyboard, play
sr = 22050
t = np.arange(int(1.2 * sr)) / sr # 1.2 s per note
def pluck(freq):
# a few harmonics, the higher ones dying faster — an electric-piano-ish tone
tone = sum(np.sin(2 * np.pi * k * freq * t) * np.exp(-(1.5 + 2.5 * k) * t) / k
for k in range(1, 6))
return 0.5 * tone * np.minimum(1, t / 0.004) # 4 ms attack, no click
kb = keyboard(pluck, sr, release=0.3)
Feature · Synth
Play a NumPy synth live
keyboard(render, sample_rate) turns your computer keyboard into an
instrument. You design the sound in NumPy; the page plays it in real time.
Runs the real browseraudio on an in-browser Python kernel
(thebe + Pyodide). Cells are editable; the first
Run downloads ~25 MB. Reload to reset.
Design a sound, then play it
A function from frequency to samples, rendered once per note and played in the page:
no round trip, any number of notes at once. Click the keyboard and play
(Z/X change octave).
A wavetable with an envelope
One cycle, looped at any pitch and shaped by an ADSR envelope.
cycle = np.linspace(-1, 1, 256) # one sawtooth cycle
kb = keyboard(wavetable=0.5 * cycle,
attack=0.02, decay=0.2, sustain=0.6, release=0.4)
Plucked strings, played by the genie
Karplus–Strong: noise circulating in a lossy delay line one period long.
Starts in genie mode: play 1–8 or
A S D F J K L ;.
def karplus_strong(freq, seconds=1.4, decay=1.0):
"""A plucked string: a burst of noise circulating in a short, lossy delay line."""
period = sr / freq # the loop must be one period long:
M = int(period) # whole samples...
w = period - M # ...plus a fraction (tap weighting)
g = 10 ** (-3 * period / (sr * decay)) # loop gain: -60 dB after `decay` s
n = int(seconds * sr)
y = np.zeros(n + 1) # y[i + 1] is sample i; y[0] = 0
noise = np.random.default_rng(M).uniform(-1, 1, M)
noise -= noise.mean() # no DC offset: no thump
y[1:M + 1] = np.convolve(noise, [0.5, 0.5], "same") # a softer pluck
for s in range(M, n, M): # a period at a time, vectorized
e = min(s + M, n)
y[s + 1:e + 1] = g * ((1 - w) * y[s - M + 1:e - M + 1] + w * y[s - M:e - M])
return 0.4 * y[1:]
kb = keyboard(karplus_strong, sr, release=0.5, input="genie")
Genie mode is Piano Genie — Chris Donahue, Ian Simon & Sander Dieleman, Piano Genie, IUI 2019 (arXiv:1810.03226) — running the trained model published by Magenta. Please cite it if you use genie mode in your work.
A drum machine on eight pads
Every drum synthesized in NumPy, on A S D F J K L ;. Try
Genie here too.
from browseraudio import KeyMap
n = np.arange(sr) / sr # 1 s per hit
noise = np.random.default_rng(0).uniform(-1, 1, sr)
def sweep(f0, f1, rate): # a pitch falling from f0 to f1
f = f1 + (f0 - f1) * np.exp(-rate * n)
return np.sin(2 * np.pi * np.cumsum(f) / sr)
hiss = np.diff(noise, prepend=0) # noise with the lows taken out
DRUMS = { # General MIDI drum numbers
36: sweep(160, 50, 30) * np.exp(-7 * n), # kick
38: 0.5 * noise * np.exp(-18 * n)
+ 0.5 * sweep(240, 180, 20) * np.exp(-25 * n), # snare
42: 0.35 * hiss * np.exp(-60 * n), # closed hi-hat
46: 0.3 * hiss * np.exp(-9 * n), # open hi-hat
39: 0.5 * noise * sum(np.exp(-80 * np.clip(n - d, 0, None)) * (n >= d)
for d in (0, 0.012, 0.024)), # clap
45: sweep(180, 110, 12) * np.exp(-9 * n), # low tom
48: sweep(260, 170, 12) * np.exp(-10 * n), # high tom
56: 0.25 * np.sign(np.sin(2 * np.pi * 540 * n) + np.sin(2 * np.pi * 800 * n))
* np.exp(-12 * n), # cowbell
}
DRUMS = {midi: 0.8 * hit / np.abs(hit).max() # even levels
for midi, hit in DRUMS.items()}
def drum(freq):
return DRUMS[round(69 + 12 * np.log2(freq / 440))] # frequency → MIDI number
pads = KeyMap(["KeyA", "KeyS", "KeyD", "KeyF", "KeyJ", "KeyK", "KeyL", "Semicolon"],
[[(midi, 0) for midi in DRUMS]],
labels=["kick", "snare", "hat", "open hat",
"clap", "low tom", "hi tom", "bell"])
kb = keyboard(drum, sr, keymap=pads, one_shot=True, max_voices=32)
Replay your performance
kb.render() re-synthesises what you played on the last keyboard.
print(len(kb.events), "note events")
play(kb.render(), kb.sample_rate)
Want your own voice as the instrument? Record it →, then build a render function that resamples rec.samples.