Skip to content

Commit cff8162

Browse files
feat(audio): 32-voice polyphony, smooth retriggering, and silent panic
- increase `Audio::kVoiceCount` and `StreamManager::kMaxStreams` to 32 - change retrigger behavior to start a new voice while fading out older voices in the same group - make panic clear the trigger queue and quickly fade out active voices instead of hard-cut stopping - remove all `Serial` logging from firmware code - delete `todo.md` - update `README.md` and `docs/documentation.md` to match current behavior
1 parent fb3ed5a commit cff8162

22 files changed

Lines changed: 252 additions & 354 deletions

README.md

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,11 @@ Architecture note: `src/main.cpp` is a thin entrypoint, while runtime orchestrat
1515
## Current Features
1616

1717
- WAV sample playback from `/samples` on SD.
18-
- 8-voice polyphony with deterministic oldest-voice stealing.
18+
- 32-voice polyphony with deterministic oldest-voice stealing.
19+
- Retrigger starts a new voice instance and softly fades older voices from the same sample group.
1920
- Playback format for assigned samples is enforced to WAV PCM16, 44.1kHz, mono.
2021
- Audio playback runs in a dedicated FreeRTOS task/core with trigger events passed by queue.
21-
- SD stream refill runs in a dedicated task; audio-domain stream reads are served from RAM read-ahead buffers.
22+
- SD and RAM playback share the same voice engine and mixer pipeline.
2223
- OLED UI with 2 encoders (via MCP23017).
2324
- On-device MIDI note to sample assignment.
2425
- Per-sample `SHOT/LOOP` playback mode (stored in config).
@@ -37,8 +38,6 @@ Architecture note: `src/main.cpp` is a thin entrypoint, while runtime orchestrat
3738
- optionally add `sampler_config.json` (if missing, firmware starts with defaults).
3839
3. Flash firmware:
3940
- `make upload-main`
40-
4. Open serial log:
41-
- `make monitor`
4241

4342
## Flashing Prebuilt Firmware (from GitHub Releases)
4443

docs/documentation.md

Lines changed: 14 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ Minimal format:
100100
Notes:
101101

102102
- `note`: `0..127`
103-
- `panic_note`: optional `0..127`; when received as MIDI NOTE ON, all active voices are stopped immediately
103+
- `panic_note`: optional `0..127`; when received as MIDI NOTE ON, all active voices are quickly faded out
104104
- `playback_mode`: optional `"shot"` or `"loop"` stored per assignment/sample
105105
- `volume`: clamped to `0..100`
106106
- `volume = 100`: full per-voice level (before dynamic mixer headroom)
@@ -124,14 +124,13 @@ Current pipeline (recently updated):
124124

125125
Playback engine behavior:
126126

127-
- fixed `8`-voice playback pool (`Audio::kVoiceCount`),
127+
- fixed `32`-voice playback pool (`Audio::kVoiceCount`),
128128
- each trigger allocates a free voice slot when available,
129-
- retriggering the same sample reuses its active voice slot when possible (previous instance is stopped and restarted from frame `0`, so the sample does not layer with itself),
129+
- retriggering the same sample starts a new voice instance and requests short fade-out on already active voices in the same retrigger group,
130130
- if all voices are active, the incoming trigger steals the oldest active voice (deterministic `oldest-voice` policy),
131-
- if incoming MIDI NOTE ON matches configured panic note, all currently active voices are stopped,
131+
- if incoming MIDI NOTE ON matches configured panic note, all currently active voices are quickly faded out and pending trigger backlog is cleared,
132132
- works for both SD-streamed and RAM-backed sample playback,
133-
- SD-streamed voices use per-voice read-ahead RAM buffers that are refilled by a dedicated `stream_refill`
134-
task; the audio update loop consumes already buffered data (no direct SD read in the critical mix loop),
133+
- voice update loop applies bounded per-voice decode budget (`kVoiceLoopSampleBudget`) to keep scheduling predictable,
135134
- 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.
136135
- trigger events are sent through a queue from UI/MIDI domain to dedicated audio task (no direct playback calls from UI code path).
137136

@@ -170,21 +169,16 @@ Assignment rules:
170169

171170
### `include/audio.h`
172171

173-
- Audio voice count: `Audio::kVoiceCount = 8`
172+
- Audio voice count: `Audio::kVoiceCount = 32`
174173

175174
### `include/boot_screen_flow.h`
176175

177176
- Boot screen dismiss timeout: `BootScreenFlow::kDefaultDismissTimeoutMs = 5000`
178177

179178
### `include/debug_flags.h`
180179

181-
- Global debug logs switch: `DebugFlags::kEnableDebugLogs`
182-
- Per-trigger playback logs switch: `DebugFlags::kEnablePerTriggerPlaybackLogs`
183-
- Input event logs switch: `DebugFlags::kEnableInputEventLogs`
184-
- Runtime audio diagnostics log switch: `DebugFlags::kEnableRuntimeAudioDiagLogs`
185-
- Runtime RAM diagnostics log switch: `DebugFlags::kEnableRuntimeRamUsageLogs`
186-
- Runtime RAM diagnostics interval: `DebugFlags::kRuntimeRamUsageLogIntervalMs = 5000`
187-
- To enable/disable a log group, edit `include/debug_flags.h` and set the given flag to `true`/`false`, then rebuild and flash firmware (`make upload-main`).
180+
- Debug flag constants are still defined in code, but main firmware no longer emits diagnostic logs to `Serial`.
181+
- `DebugFlags::kRuntimeRamUsageLogIntervalMs = 5000` remains as a timing constant used by gated runtime checks.
188182

189183
### `include/input.h`
190184

@@ -209,10 +203,9 @@ Assignment rules:
209203

210204
### `src/audio.cpp`
211205

212-
- Voice fade-in to reduce trigger click: `kVoiceFadeInUs` (default `0`, feature inactive to avoid transient chopping on percussive sounds; set non-zero microseconds to enable)
213206
- Audio mixer buffer size: `kMixerBufferSamples = 512`
214-
- `AUDIO_DIAG` includes trigger handling timing: `play_max_us`, `slow_plays`, `play_count`
215-
- `AUDIO_DIAG` includes I2S rate-set counters: `rate_set_calls`, `rate_set_skipped`, `rate_set_applied`
207+
- Retrigger fade-in (new voice): `kRetriggerFadeInUs = 800`
208+
- Retrigger fade-out (older voices in same group): `kRetriggerFadeOutUs = 1200`
216209

217210
### `src/sampler_app.cpp`
218211

@@ -222,7 +215,6 @@ Assignment rules:
222215
- Trigger queue length: `32`
223216
- Loader command queue length: `12`
224217
- UI status queue length: `16`
225-
- `RAM_DIAG` includes heap/psram occupancy: `heap_free`, `heap_min`, `heap_size`, `psram_free`, `psram_size`
226218

227219
### `src/settings_store.cpp`
228220

@@ -244,7 +236,7 @@ Assignment rules:
244236
- `make upload-debug`
245237
- `make build-debug-midi`
246238
- `make upload-debug-midi`
247-
- `make monitor`
239+
- `make monitor` (optional raw UART monitor; firmware does not print diagnostic logs by default)
248240
- `make convert-samples SAMPLES_DIR=/path/to/samples` (requires `ffmpeg`, converts `*.wav` in that folder to PCM16/44.1kHz/mono in-place)
249241
- `make convert-samples /path/to/samples` (equivalent positional form)
250242
- conversion runs `ffmpeg` with `-nostdin` to avoid stdin conflicts in batch processing.
@@ -255,7 +247,7 @@ Assignment rules:
255247
- `pio run -e esp-wrover-kit -t upload`
256248
- `pio run -e esp-wrover-kit-debug-input -t upload`
257249
- `pio run -e esp-wrover-kit-debug-midi -t upload`
258-
- `pio device monitor -b 115200`
250+
- `pio device monitor -b 115200` (optional raw UART monitor)
259251

260252
## 10. Module Map (Code Orientation)
261253

@@ -278,5 +270,5 @@ Assignment rules:
278270
- `src/sample_classifier.cpp`: assigned sample classification (RAM/STREAM/error modes)
279271
- `src/sample_ram_manager.cpp`: pool allocation and WAV data preload into RAM
280272
- `src/active_sample_registry.cpp`: final active-sample registry after fallback handling
281-
- `src/audio.cpp`: 8-voice playback engine (SD + RAM), mixer, and oldest-voice stealing
282-
- `src/stream_manager.cpp`: buffered SD stream sources and background refill task for streamed voices
273+
- `src/audio.cpp`: 32-voice playback engine (SD + RAM), mixer, retrigger fade behavior, and oldest-voice stealing
274+
- `src/stream_manager.cpp`: per-voice SD stream source wrappers and stream diagnostics

include/audio.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
class Audio {
77
public:
8-
static constexpr uint8_t kVoiceCount = 8;
8+
static constexpr uint8_t kVoiceCount = 32;
99
static constexpr uint16_t kWaveformPointCount = 128;
1010

1111
struct RuntimeStats {
@@ -31,6 +31,7 @@ class Audio {
3131
int16_t retriggerGroupId = -1,
3232
bool loopEnabled = false);
3333
void stopAllVoices();
34+
void fadeOutAllVoices(uint32_t fadeOutUs);
3435
void stopLoopingVoicesForGroup(int16_t retriggerGroupId);
3536
void setLoopEnabledForGroup(int16_t retriggerGroupId, bool loopEnabled);
3637
bool playSampleRam(const uint8_t *pcmData,

include/stream_manager.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ class AudioFileSource;
88

99
class StreamManager {
1010
public:
11-
static constexpr uint8_t kMaxStreams = 8;
11+
static constexpr uint8_t kMaxStreams = 32;
1212

1313
struct Diagnostics {
1414
uint32_t sourceReadCount = 0;

0 commit comments

Comments
 (0)