This document collects the information needed to build, run, and maintain the device.
platformio.ini defines three environments:
esp-wrover-kit: main firmware (src/main.cpp)esp-wrover-kit-debug-input: input test firmware (src/debug_input_main.cpp)esp-wrover-kit-debug-midi: MIDI test firmware (src/debug_midi_main.cpp)
PSRAM is enabled in all environments (board_build.psram = enabled, -DBOARD_HAS_PSRAM).
Source of truth: include/pins.h.
CS: GPIO13MISO: GPIO2MOSI: GPIO15SCK: GPIO14
BCLK: GPIO27LRC/WS: GPIO25DOUT: GPIO26PA_EN: GPIO21
SDA: GPIO33SCL: GPIO32- I2C address:
0x10
SDA: GPIO23SCL: GPIO18- OLED: auto-detected address
0x3C/0x3D - MCP23017: default address
0x27 MCP_INTA: GPIO5MCP_INTB: GPIO0 (currently not used by main firmware)
MIDI_IN: GPIO22- UART:
Serial2,31250 bps
Current hardware revision includes additional isolation elements to reduce audible noise caused by ground loops between Samplotron and an external mixer:
- audio output path:
600:600audio isolation transformer inserted between one output channel and the output jack, - output jack: isolated from chassis,
- power path: Hi-Link
B0505S-3WR3DC/DC isolator.
GPA0: Encoder 1 AGPA1: Encoder 1 BGPA2: Encoder 1 switchGPA3: Encoder 2 AGPA4: Encoder 2 BGPA5: Encoder 2 switch
- samples:
/samples/*.wav - configuration:
/sampler_config.json
/samplesis scanned (non-recursive),.wavand.WAVare recognized,- file list is sorted alphabetically,
- UI sample limit:
32.
Location and parser: src/settings_store.cpp.
Minimal format:
{
"version": "1.0",
"global_settings": {
"sample_ram_budget_bytes": 1048576,
"panic_note": 24
},
"midi_assignments": [
{
"note": 60,
"sample_path": "/samples/kick.wav",
"volume": 100,
"playback_mode": "shot"
}
]
}Notes:
note:0..127panic_note: optional0..127; when received as MIDI NOTE ON, all active voices are quickly faded outplayback_mode: optional"shot"or"loop"stored per assignment/samplevolume: clamped to0..100volume = 100: full per-voice level (before dynamic mixer headroom)sample_path: full SD path, for example/samples/snare.wav- maximum assignments:
128
Current pipeline (recently updated):
- MIDI-assigned samples are classified as
RAMorSTREAM, RAMis used only for WAV files that meet all conditions:- PCM format (
audioFormat = 1), 16-bit,44100 Hz,mono,- duration
<= 5.0 s, - fit into the RAM budget,
- PCM format (
- if preload fails, the entry falls back to
STREAM. - if assigned sample format is unsupported/missing, playback for that note is blocked (
UNAVAILABLE) instead of trying to decode anyway.
Playback engine behavior:
- fixed
32-voice playback pool (Audio::kVoiceCount), - each trigger allocates a free voice slot when available,
- retriggering the same sample starts a new voice instance and requests short fade-out on already active voices in the same retrigger group,
- if all voices are active, the incoming trigger steals the oldest active voice (deterministic
oldest-voicepolicy), - if incoming MIDI NOTE ON matches configured panic note, all currently active voices are quickly faded out and pending trigger backlog is cleared,
- works for both SD-streamed and RAM-backed sample playback,
- voice update loop applies bounded per-voice decode budget (
kVoiceLoopSampleBudget) to keep scheduling predictable, - per-voice mixer gain uses dynamic headroom (
kDynamicMixMinGain..kDynamicMixMaxGain) based on active voice count; master output passes through a limiter with make-up gain to increase loudness while controlling clipping. - trigger events are sent through a queue from UI/MIDI domain to dedicated audio task (no direct playback calls from UI code path).
Important behavior:
- RAM pool budget is "locked" after the first
prepare()(sample_ram_manager.cpp), - changing
sample_ram_budget_bytesin the same runtime session is reported asfixedBudgetMismatch, - a real budget change requires a device reboot.
UI states:
MainLibraryAssignNoteSaving
Flow:
- in
Library, you select a sample and trigger preview, - preview requests are sent as
PreviewSamplecommand tosample_loaderqueue before playback routing, - in
Main,VOLandSHOT/LOOPare shown only when an active sample exists, - in
Main,SHOT/LOOPtoggles one-shot vs loop for the currently active sample, L rotateinLibrarytoggles assignment mode betweenSampleandPanic,- long-pressing the right button enters
AssignNotefor the current mode, - the first received MIDI note assigns either the current sample or the global panic note (depending on selected mode),
SAVEwrites configuration (/sampler_config.json) and refreshes classification + RAM preload.
Assignment rules:
- one sample can be assigned to only one note at a time (new assignment clears older one),
- volume is stored per sample (
0..100), not per note.
- Audio voice count:
Audio::kVoiceCount = 32
- Boot screen dismiss timeout:
BootScreenFlow::kDefaultDismissTimeoutMs = 5000
- Debug flag constants are still defined in code, but main firmware no longer emits diagnostic logs to
Serial. DebugFlags::kRuntimeRamUsageLogIntervalMs = 5000remains as a timing constant used by gated runtime checks.
- Button debounce:
35 ms - Encoder detent:
4ticks - Long press (right encoder):
700 ms
- RAM preload threshold:
kFixedPreloadThresholdSeconds = 5.0f
- Default RAM budget:
kDefaultSampleRamBudgetBytes = 1 MB
- UI sample limit:
Ui::kMaxSamples = 32 - Minimum saving screen:
1000 ms - "Saved" feedback duration:
1000 ms - MIDI pulse indicator duration:
100 ms
- Audio mixer buffer size:
kMixerBufferSamples = 512 - Retrigger fade-in (new voice):
kRetriggerFadeInUs = 800 - Retrigger fade-out (older voices in same group):
kRetriggerFadeOutUs = 1200
- Audio task core/priority: core
1, priority6 - Sample loader task core/priority: core
0, priority4 - UI task core/priority: core
0, priority2 - Trigger queue length:
32 - Loader command queue length:
12 - UI status queue length:
16
- Config path:
"/sampler_config.json" - JSON document capacity:
12288
- Volume change step in UI:
5per right-encoder tick onVOL
make helpmake build-mainmake upload-mainmake build-debugmake upload-debugmake build-debug-midimake upload-debug-midimake monitor(optional raw UART monitor; firmware does not print diagnostic logs by default)make convert-samples SAMPLES_DIR=/path/to/samples(requiresffmpeg, converts*.wavin that folder to PCM16/44.1kHz/mono in-place)make convert-samples /path/to/samples(equivalent positional form)- conversion runs
ffmpegwith-nostdinto avoid stdin conflicts in batch processing.
- conversion runs
pio run -e esp-wrover-kitpio run -e esp-wrover-kit -t uploadpio run -e esp-wrover-kit-debug-input -t uploadpio run -e esp-wrover-kit-debug-midi -t uploadpio device monitor -b 115200(optional raw UART monitor)
src/main.cpp: thin Arduino entrypoint delegating toSamplerAppsrc/sampler_app.cpp: high-level orchestration (boot sequence, module wiring, task startup)src/sampler_loader_ipc.h: cross-task command/status types forsample_loaderand UI/audio domainssrc/boot_screen_flow.cpp: boot screen render model and dismiss/timeout flowsrc/sampler_callback_binder.cpp: callback wiring for UI/MIDI/input routingsrc/input_ui_bridge.cpp:Input::Event->Ui::Eventmappingsrc/sampler_playback_router.cpp: preview and MIDI-triggered playback routingsrc/sampler_save_service.cpp: save flow orchestration (wait idle -> collect -> rebuild -> persist)src/trigger_engine.cpp: trigger queue and dedicated audio task loopsrc/sample_library.cpp: SD sample discovery, sorting, and path lookupsrc/sampler_runtime.cpp: settings <-> UI mapping, classification, RAM preload, active registrysrc/ui.cpp: UI state, navigation, assignment logic, save flowsrc/display_ssd1309.cpp: OLED screen renderingsrc/input.cpp: MCP23017 reading, encoder decode, click/long-press handlingsrc/midi.cpp: MIDI parser (running status), NOTE_ON -> UI + trigger callbacksrc/settings_store.cpp: JSON load/save on SDsrc/sample_classifier.cpp: assigned sample classification (RAM/STREAM/error modes)src/sample_ram_manager.cpp: pool allocation and WAV data preload into RAMsrc/active_sample_registry.cpp: final active-sample registry after fallback handlingsrc/audio.cpp: 32-voice playback engine (SD + RAM), mixer, retrigger fade behavior, and oldest-voice stealingsrc/stream_manager.cpp: per-voice SD stream source wrappers and stream diagnostics