Skip to content

Repository files navigation

SimpleSampler

A low-latency sample pad that lives in your terminal. Load any WAV, map it to a key or MIDI note, and play — no DAW required.

Built for live performance where every millisecond counts.

Features

  • 4x4 pad grid in the terminal via Textual — keyboard, mouse, or MIDI trigger
  • ~6ms audio latency — 256-sample buffer with lock-free voice mixing, no mutexes in the audio path
  • MIDI input — bind any note, CC, or program change to any pad with a simple string like note:36:ch9
  • Hot-swap MIDI devices from within the app — press m, pick a device, keep playing
  • Instant playback — samples preloaded into memory as float32 NumPy arrays (up to 10 MB cache)
  • Supports 8/16/24-bit WAV, mono or stereo, any sample rate (auto-resampled to 44.1kHz)

Requirements

  • Linux (this is all we care about)
  • Python >= 3.13
  • PortAudio — the system library that makes audio go fast
    # Debian / Ubuntu
    sudo apt install libportaudio2
    # Arch
    sudo pacman -S portaudio
    
  • uv — fast Python package manager

Installation

git clone https://github.com/yourusername/SimpleSampler.git
cd SimpleSampler
uv sync

That's it. No virtualenv juggling, no pip drama.

Usage

uv run simplesampler my_bank.json

The app launches in your terminal with a 4x4 pad grid. Smash keys, click pads, or send MIDI — audio fires instantly.

Controls

KeyAction
1 2 3 4Pads 0-3 (top row)
q w e rPads 4-7
a s d fPads 8-11
z x c vPads 12-15
mOpen MIDI device selector
Mouse clickTrigger any pad

Keybinds are customizable per-pad in the bank JSON (see below). The defaults above apply when no keybind is set.

Bank Configuration

A bank is a JSON file that defines your pad layout. Here's a minimal example:

{
"name": "My Kit",
"midi_device": "MPD218",
"pads": [
{
"id": 0,
"name": "Kick",
"sample_path": "/path/to/kick.wav",
"color": "red",
"keybind": "1",
"midibind": "note:36:ch9"
},
{
"id": 1,
"name": "Snare",
"sample_path": "/path/to/snare.wav",
"color": "green",
"keybind": "2",
"midibind": "note:38:ch9"
}
]
}

Bank fields

FieldTypeRequiredDescription
namestringyesDisplay name for the bank
midi_devicestringnoMIDI input device — name substring (e.g. "MPD218") or index (e.g. "0"). Omit to skip MIDI on startup.
padsarrayyesList of pad objects (up to 16)

Pad fields

FieldTypeRequiredDescription
idintyesPad position, 0-15 (4x4 grid, left-to-right, top-to-bottom)
namestringyesDisplay label on the pad
sample_pathstringyesAbsolute path to a WAV file
colorstringnoPad color — any Textual color (default: "blue")
keybindstringnoKeyboard key to trigger this pad (default: positional 1234qwerasdfzxcv)
midibindstringnoMIDI binding string (see below)

Midibind format

note:<number>:ch<channel> — MIDI note (e.g. note:36:ch9)
cc:<number>:ch<channel> — Control change (e.g. cc:1:ch0)
pc:<number>:ch<channel> — Program change (e.g. pc:5:ch0)

Don't know your MIDI note numbers? That's what midi_learn.py is for.

MIDI Learn

midi_learn.py is a standalone CLI tool that listens on a MIDI device and prints midibind strings to stdout as you hit pads/keys/knobs. Copy the output straight into your bank JSON.

# List available MIDI devices
uv run python midi_learn.py -l
# Listen on a specific device (by name)
uv run python midi_learn.py -d "MPD218"# Listen on a specific device (by index)
uv run python midi_learn.py -d 0
# Auto-select first available device
uv run python midi_learn.py

Hit a pad on your controller, get a string:

note:36:ch9
note:38:ch9
note:42:ch9
cc:1:ch0

Paste those into your bank JSON's midibind fields. Ctrl+C when you're done.

How It's Fast

A few deliberate choices that add up:

  • sounddevice + PortAudio — 256-sample blocks at 44.1kHz = ~5.8ms buffer. Reports actual achieved latency on startup.
  • Lock-free audio — voices are queued via collections.deque (atomic append in CPython). The audio callback drains its own local list. No mutex ever touches the hot path.
  • Direct key dispatchon_key fires audio through a pre-built dict lookup, completely bypassing Textual's async widget message queue. You hear the sound before the pad even flashes.
  • MIDI daemon thread — polls at 2ms intervals via iter_pending(), triggers audio directly from the thread. No round-trip through the UI event loop.
  • Preloaded samples — WAV files are decoded, resampled, and converted to float32 stereo NumPy arrays at startup. Zero file I/O during playback.

License

This project is licensed under the GNU General Public License v3.0 — see the LICENSE file for details.

About

A Simple no fuss TUI Based Audio Sampler Written in Python for me to Perform on Stage.

Resources

Stars

5 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages