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.

ready
01

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

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

A wavetable with an envelope

One cycle, looped at any pitch and shaped by an ADSR envelope.

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

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

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

04

A drum machine on eight pads

Every drum synthesized in NumPy, on A S D F J K L ;. Try Genie here too.

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

Replay your performance

kb.render() re-synthesises what you played on the last keyboard.

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