From ee08c38e784d53201c277354c70f5e60da9777a6 Mon Sep 17 00:00:00 2001 From: ainyan03 <205502311+ainyan03@users.noreply.github.com> Date: Wed, 7 Oct 2026 00:11:01 +0000 Subject: [PATCH] Speaker: keep the DMA fed at the hold level while waiting for new data After the last samples of a sound, spk_task slept dma_buf_count times, for a time estimated from dma_buf_len, before writing the fade-out ramp. With the ESP-IDF 5 I2S driver (Arduino core 3.x) the ramp was observed to arrive after the DMA had reached the auto-cleared region: the DAC output dropped to the zero level for about half a DMA buffer and then jumped back to the bias level at the end of every tone, heard as noise on the ESP32 internal DAC (e.g. M5Stack Basic, #371). The legacy driver has the same buffer layout and auto-clear, so the timed wait was fragile there as well, although the drop was not observed with Arduino core 2.x. Write one buffer of the hold level per step instead of sleeping. The blocking write paces the loop by the DMA itself, so the hold data and the ramp follow the last samples without a gap, and no timing estimate is needed. The hold level is the current DAC bias for use_dac and 0 otherwise. The buzzer output keeps the timed wait, to leave its behavior unchanged in this fix. A new sound requested during this hold now starts after the queued hold data, about one DMA ring later (dma_buf_count * dma_buf_len samples), instead of being packed right after the previous sound. Measured on M5Stack Basic (Arduino core 3.3.x, sample_rate 96 kHz, dma_buf_len 256, dma_buf_count 8, GPIO25 with a logic analyzer, 12 tones of 500 ms with 50 ms gaps): drops at the tone end 12 -> 0. The interval from completion of the final audio write to submission of the fade-out ramp is about 21 ms (previously about 15 ms). --- src/utility/Speaker_Class.inl | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/utility/Speaker_Class.inl b/src/utility/Speaker_Class.inl index cc8e0ef..05982e4 100644 --- a/src/utility/Speaker_Class.inl +++ b/src/utility/Speaker_Class.inl @@ -574,8 +574,22 @@ namespace m5 if (buf_cnt) { // (no data... wait for new data) --buf_cnt; - uint32_t wait_msec = 1 + (self->_cfg.dma_buf_len / (spk_sample_rate_x256 >> 17)); - flg_nodata = (0 == ulTaskNotifyTake( pdFALSE, wait_msec )); + if (self->_cfg.buzzer) + { + uint32_t wait_msec = 1 + (self->_cfg.dma_buf_len / (spk_sample_rate_x256 >> 17)); + flg_nodata = (0 == ulTaskNotifyTake( pdFALSE, wait_msec )); + } + else + { // Keep the DMA fed at the hold level instead of sleeping. A timed wait can let the + // DMA reach the auto-cleared buffers, which output 0 (on the DAC, a drop to the zero + // level and a jump back to the bias). The blocking write paces this loop. + uint32_t hold = self->_cfg.use_dac ? (uint32_t)(dac_offset | dac_offset << 16) : 0u; + size_t words = dma_buf_len >> 1; + for (size_t i = 0; i < words; ++i) { sound_buf32[i] = hold; } + size_t write_bytes; + _spk_i2s_write(i2s_port, sound_buf32, words * sizeof(int32_t), &write_bytes, portMAX_DELAY); + flg_nodata = (0 == ulTaskNotifyTake( pdFALSE, 0 )); + } } if (flg_nodata && 0 == buf_cnt)