A playable piano in the terminal. Notes are held and released with the computer keyboard, and a small keyboard drawn with the kitty graphics protocol shows which key is which. It draws inline, in eight rows under the prompt that launched it, and erases itself on exit.
- Polyphonic additive synthesis, 16 voices, rendered directly in the CoreAudio callback
- Seven voices — piano, felt, epiano, clav, vibes, organ, pad — switchable while playing
- Shared hall reverb, eight damped combs into four allpasses
- True note-off through the kitty keyboard protocol: a held key sustains
- Sustain pedal
- Two key layouts, tracker and GarageBand-style; transposable ±2 octaves
- Kitty graphics keyboard with the pressed key highlighted; falls back to coloured cells where graphics are unavailable
- Tune files: play one back, or practise it a note at a time
- Offline rendering to WAV and PPM
- Works under tmux (graphics and keyboard both use DCS passthrough)
Two layouts, selected with layout in the config or --layout NAME. They
disagree about z and x — notes in one, octave shift in the other.
FastTracker II, Impulse Tracker, Renoise, OpenMPT, MilkyTracker, LMMS. Two rows of white keys, each with its black keys on the row above. C3–E5.
black: 2 3 5 6 7 9 0
white: q w e r t y u i o p C4 ............. E5
black: s d g h j l ;
white: z x c v b n m , . / C3 ......... B3
, l . ; / repeat the notes of q 2 w 3 e. Only the upper row's key is drawn
as the label. f, k, 1, 4 and 8 are unmapped — the gaps where E–F and
B–C have no black key between them.
Apple's Musical Typing, and the same shape in Ableton Live and FL Studio. C4–E5, and the drawn keyboard shrinks to match.
black: w e t y u o p
white: a s d f g h j k l ; C4 ............. E5
| Key | Action |
|---|---|
space |
sustain pedal (held; toggles without note-off) |
[ / ] |
previous / next voice |
tab |
switch layout |
- / = |
octave down / up |
z / x |
octave down / up (garageband layout only) |
↑ / ↓ |
volume up / down |
' |
play the tune / stop |
\ |
practise the tune / stop |
` |
restart the tune from the top |
esc, ctrl-c, ctrl-q |
quit |
| Option | Effect |
|---|---|
--volume N |
0–100 |
--octave N |
-2..2 |
--layout NAME |
tracker (default) or garageband |
--voice NAME |
piano felt epiano clav vibes organ pad |
--tune FILE |
load a tune |
--autoplay |
start the tune playing at once |
--practice |
start the tune in practice mode |
--device NAME |
output device name substring (--probe lists them) |
--unmute |
unmute that device while running, restore it on exit |
--no-graphics |
draw with coloured cells instead of kitty graphics |
--no-kbd |
skip the kitty keyboard protocol |
--tmux-kbd |
inside tmux, reach past it for note-off (the default) |
--no-tmux-kbd |
leave tmux's keyboard alone, giving up sustain |
--cell WxH |
override the terminal's cell pixel size |
--probe |
report terminal and audio capabilities, then exit |
--fix-terminal |
pop a stranded keyboard protocol mode and exit |
--dump-wav FILE |
render --play, or --tune, offline to a WAV and exit |
--dump-ppm FILE |
render the keyboard image to a PPM and exit |
--play KEYS |
keys to render for --dump-wav, e.g. zxcvb |
--pressed KEYS |
keys drawn as held for --dump-ppm |
--chord |
strike all of --play at once |
--repeat N |
repeat the --play sequence N times |
--hold-ms N |
note length for --dump-wav (default 300) |
--gap-ms N |
gap between notes (default 100) |
--tail-ms N |
silence after the last note (default 3000) |
--help |
usage |
[ and ] cycle while playing; voice = NAME sets the starting one.
| Voice | |
|---|---|
piano |
struck string: strike-point comb, stiffness, detuned unison rank |
felt |
soft hammers, dark, wide unison beat, heavy room send |
epiano |
Rhodes: near-pure fundamental with a 9.2× tine |
clav |
struck near the end of the string; bright, short |
vibes |
bar modes at 1 : 4 : 10.7, long ring |
organ |
drawbars at 8' 4' 5⅓' 2' 1⅗' 1⅓' 1'; holds until released |
pad |
bowed strings: slow attack, full harmonic series, detuned second rank |
brightness, strike, inharmonicity, detune_cents and decay_scale apply
to piano; decay_scale also scales the other decaying voices. reverb = 0
turns the room off, reverb = 1.5 makes it cavernous.
--tune FILE loads a tune. ' plays it, \ practises it, ` restarts.
--tune FILE --dump-wav OUT.wav renders it offline instead of playing it.
tunes/ode-to-joy.tune is included as an example.
A tune is a text file. # starts a comment, | is an ignored barline, and
whitespace separates events:
tempo 120
voice felt
gate 0.85
E4 E4 F4 G4 | G4 F4 E4 D4
C4 C4 D4 E4 | E4/4. D4/8 D4/2
| Token | |
|---|---|
C4 |
a quarter note; octave 4 is middle C |
F#3 Bb5 Cs4 |
sharp and flat; s also means sharp |
G4/2 |
half note — /1 whole, /4 quarter, /8 eighth, /16 sixteenth |
A4/4. |
dotted, so half as long again; dots stack |
r r/2 |
a rest |
C4+E4+G4 |
a chord, struck together, up to 8 notes |
| Directive | |
|---|---|
tempo 120 |
quarter notes per minute (10–400, default 100) |
voice felt |
which voice to play it with |
gate 0.85 |
fraction of each note's slot that sounds (0.05–1, default 0.9) |
In play mode the tune runs on the clock. In practice mode it waits at each event and advances only once you are holding exactly the notes it asked for; rests pass on their own. Practice matches the note you sounded rather than the key you pressed, so transposing or switching layout mid-tune keeps working.
The drawn keys colour as:
| Colour | |
|---|---|
| amber | the tune's note — waiting for it in practice, sounding it in play |
| blue | a key you are holding |
| green | both at once: you played the note it asked for |
Notes outside the drawn range are named in the status line rather than marked.
~/.config/piano/config, written with commented defaults on first run.
| Key | Default | Effect |
|---|---|---|
volume |
80 | 0–100 |
sample_rate |
0 | 0 follows the output device |
buffer_frames |
256 | lower is less latency, more risk of crackle |
velocity |
100 | 0–127; louder and brighter |
device |
output device name substring; empty follows the system default | |
unmute |
false | clear that device's mute while running |
voice |
piano | starting voice |
release_ms |
150 | damper time after key-up |
decay_scale |
1.0 | multiplies every string decay time |
brightness |
1.0 | above 1 brighter, below 1 warmer |
strike |
0.125 | hammer position; notches out the 1/strike'th harmonic |
inharmonicity |
0.0002 | string stiffness; 0 is perfectly harmonic |
detune_cents |
0.6 | unison detune, the source of the beating |
stereo_spread |
0.15 | 0 is mono |
reverb |
1.0 | scales every voice's room send; 0 is completely dry |
layout |
tracker | tracker or garageband |
octave |
0 | starting transpose, -2..2 |
kbd |
auto | auto or off — kitty keyboard protocol |
tmux_keyboard |
true | reach past tmux for real key releases |
graphics |
auto | auto, on or off |
transfer |
auto | hand pixels to the terminal as a temp file; auto is off over ssh |
cell |
override cell pixels, e.g. 8x17 |
Without the kitty keyboard protocol there is no note-off: notes decay on their
own and the sustain pedal toggles instead of being held. --probe reports
whether the terminal negotiated it.
The keyboard mode belongs to the terminal running tmux, not to any one pane, and
tmux does not carry key releases itself — extended-keys on and always were
both tested against tmux 3.7 and neither delivers them, nor does tmux report
pane focus that could scope the mode. Reaching past tmux is therefore the only
way to get note-off there, and it is on by default:
tmux_keyboard = true(default) — holding a key sustains. While piano runs, every other pane receives keys as escape sequences and tmux stops seeing its own bindings, prefix included.tmux_keyboard = false, or--no-tmux-kbd— nothing touches the shared terminal. Notes decay on their own and the pedal toggles.
Outside tmux none of this applies.
If a run is killed before it can pop the mode, the session stays that way. Fix
it with piano --fix-terminal, or by hand:
printf '\033Ptmux;\033\033[<1u\033\\' # inside tmux
printf '\033[<1u' # outside
Most keyboards register at most about six simultaneous keys.
piano --probe reports the figure and lists every output with its latency:
output device External Headphones
buffer frames 256 requested, 256 granted (5.3 ms)
callback 9472 frames in 300 ms ok
output latency 165.3 ms (buffer + driver safety + device)
available outputs (latency at 256 frames):
External Headphones 165.3 ms vol 100% (system default)
MacBook Air Speakers 8.4 ms MUTED
Bluetooth adds 100–200 ms that no buffer size removes. Pin a wired or built-in
output with device = speakers in the config, or --device speakers, and
everything else keeps using the system default.
macOS keeps volume and mute per device, so an output you are not otherwise using
may be sitting muted. --probe shows each device's level, and piano says so in
the status line rather than going silently quiet. --unmute clears it for the
run and restores it on exit.
make
make install # to ~/.local/bin
Requires macOS (CoreAudio). No dependencies beyond the system frameworks;
src/term.h and src/kitty.h are vendored single-header libraries.
MIT
