feat(#14): mic record → speaker playback (M5StickS3 hardware bring-up) - #1
Open
junnncct1106 wants to merge 14 commits into
Open
feat(#14): mic record → speaker playback (M5StickS3 hardware bring-up)#1junnncct1106 wants to merge 14 commits into
junnncct1106 wants to merge 14 commits into
Conversation
… button-mode design Define the mic-record → speaker-playback feature from the requirements side before any code: - Purpose: first end-to-end proof that real captured audio data survives the full round trip (mic→ADC→I2S→RAM→I2S→DAC→amp→speaker), beyond thc1006#6 (meter) and thc1006#3 (synthesised tone). - Functional/quality requirements + platform constraints. - Quantified baseline: default vs audio-build memory; dram0 has ~314 KB free, so a 5 s mono clip (156 KB) fits with margin. Includes verified RMS refs. - Proposed modal REC-page button design (KEY1=action/long=re-record, KEY2=exit) with on-screen labels; short/long-press detection. Agreed: A=>=short phrase (5 s), C=intelligible (digital gain), D=baseline first. B (button/mode) proposed, under review. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…elpers Pure-logic primitives for the record->playback path (issue thc1006#14), the inverse pair / companion to the existing capture DSP: - audio_interleave_mono(): expand a recorded mono clip to interleaved stereo for the ES8311/I2S TX path (inverse of audio_deinterleave()). - audio_gain_clip_i16(): Q8 fixed-point gain with int16 saturation, to make a quiet mic recording audible on playback without wrapping (req QR-1). native_sim ztest extended (now 8/8): identity/2x/1.5x gain, +/- saturation, INT16_MIN, gain=0, in-place, mono->stereo round-trip vs deinterleave, plus NULL/0 no-op guards. No hardware; existing signatures unchanged. Part 1/5 of the feature/14-record-playback plan (docs/14_RECORD_PLAYBACK_DESIGN.md). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add the core mic-record -> speaker-playback engine (issue thc1006#14), all on the existing audio thread so the UI thread never touches I2S (HW-016e): - do_record(): full-duplex capture (amp OFF) of CONFIG_APP_AUDIO_REC_SECONDS into rec_buf, tracking peak RMS; bounded I/O, can't hang on a wedged clock. - do_play(): replays the held clip with the audio_beep() anti-pop amp sequence, applying CONFIG_APP_AUDIO_REC_GAIN_Q8 via audio_gain_clip_i16() and audio_interleave_mono(); short final block is zero-padded. - Thread refactored into a dispatcher: AUDIO-page meter (extracted to meter_session()) takes priority, else service one record/play request. Only one of {meter, record, play} runs at a time, sharing rx_buf/codec safely. - Public API for the UI: audio_record_request/audio_play_request/ audio_rec_get_state/audio_rec_peak/audio_rec_len_ms. UI only reads state. - Kconfig: APP_AUDIO_REC_SECONDS (1..8, default 5) and APP_AUDIO_REC_GAIN_Q8 (256..8192, default 1024 = 4.0x). Builds for m5stack_sticks3/esp32s3/procpu with overlay-audio.conf: dram0 60.1% (+160 KB clip buffer), 155 KB free. Not yet flashed; UI/buttons are Part 3. Part 2/5 of feature/14-record-playback (docs/14_RECORD_PLAYBACK_DESIGN.md). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…achine Wire the record->playback engine to a new modal PAGE_AUDIO_REC (issue thc1006#14): - pages.h: add PAGE_AUDIO_REC after PAGE_AUDIO (gated by CONFIG_APP_AUDIO). - main.c: on the REC page the two buttons drive a state machine instead of page nav (other pages unchanged): K1 short = record (IDLE) / play (REVIEW) / stop (RECORDING), K1 long-press (>=1 s, decided on release) in REVIEW = re-record, K2 = exit to the next page. A k1_armed guard stops the release of the press that navigated INTO the page from firing a spurious action. REC page also ticks fast (250 ms) for the live countdown/state. - ui.c: render_audio_rec_body() draws READY / RECORDING (SPEAK NOW + countdown) / REVIEW (length, peak, K1 play / hold re-rec) / PLAYING; the page clears on a mode change so screens start clean. UI only READS state, never touches I2S. - audio.c/.h: audio_record_stop_request() + rec_abort for the early-stop. ASCII font => English labels. Builds green both with overlay-audio.conf (dram0 60.1%) and the default no-audio config (unchanged). Hardware test is Part 4. Part 3/5 of feature/14-record-playback (docs/14_RECORD_PLAYBACK_DESIGN.md). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…hiccup The record loop stopped at the first esp32-i2s RX/TX hiccup, so recordings were ~256 ms of silence (peak_rms=0). The live meter never showed this because the thread re-enters its session each time, masking the same break. do_record now captures across session restarts (drop + restart, keep accumulating into rec_buf) until full or stopped, with a no-progress guard so a dead clock can't spin, and logs the first hiccup's errno + a restarts count. HW-verified on m5stack_sticks3 (MAC 70:04:1d:db:ab:b8): two recordings reach the full 5000 ms with real audio (peak_rms=684/717) and restarts=1 (a one-time startup hiccup, then stable); playback runs the full clip. Evidence: evidence/20260609-hw014-record-playback.log. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…page User feedback: the standalone AUDIO meter page is unnecessary (its only value is confirming the mic hears sound), and the REC screen looked frozen with no ready state. So: - Remove PAGE_AUDIO; fold the live '[####]' mic meter into the REC page's READY and REVIEW screens (draw_mic_meter()), so the device's hearing is always visible and the screen is never static. Pages: HOME/IMU/POWER/REC/DIAG. - Label every state 'state: ready|rec|review|play' (READY shows the live meter). - Run the meter on the REC page only in IDLE/REVIEW; the thread services a record/play request AHEAD of the meter and meter_session() yields on a pending rec_cmd, so pressing K1 on the meter screen records/plays without an I2S tug-of-war (no capture_on toggle race). Builds green: overlay-audio.conf (dram0 60.1%) and the default no-audio config. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…on hints Addresses hardware-test feedback: - No audible playback: the clip had real audio (peak_rms=1555) but was too quiet. Raise codec volume -20 -> -6 dB and the default digital gain 4x -> 8x (CONFIG_APP_AUDIO_REC_GAIN_Q8 1024 -> 2048); both stack for an audible replay. - No on-screen guidance on the PLAYING screen, and to reach the next page: every REC state now shows button hints (K2 labelled 'next page'); PLAYING shows '(auto-ends) / K2 next page'. - Cramped text: LINE_GAP 4 -> 8 px and all REC states lay out on consecutive rows (no skipped lines), so spacing is even. Builds green (overlay-audio.conf + default). Volume pending HW confirm. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Three hardware-level issues blocked record→playback on real M5StickS3 hardware (ES8311 + AW8737 + esp32-i2s), all found via serial bring-up: - ES8311 deferred-init: the codec shares the L3B rail (lcd_power, regulator-boot-on via the M5PM1 PMIC). With no supply/reset link on the node and CONFIG_DEVICE_DEPS off, the driver probed (chip-id read over I2C) at its POST_KERNEL priority BEFORE L3B was powered -> -EFAULT "Failed to read chip id" -> audio init failed. Mark the node zephyr,deferred-init and device_init() it from audio_init() (main context), where L3B is up and settled. - I2S TX mem_slab leak: the esp32 i2s driver does not return TX blocks to the slab on DROP from the ERROR state an underrun leaves, so tx_slab leaks empty and the next session's prequeue fails with -EAGAIN (-11), eventually wedging record too. Recover with I2S_TRIGGER_PREPARE (the documented ERROR->READY recovery that frees the queues) in loop_tx() and at do_record()/do_play() start. Bound do_play()'s TX writes via loop_tx() so a wedge times out instead of K_FOREVER-hanging the UI; play TX-only (full-duplex playback under-ran as the RX read stalled TX); BLOCK_COUNT 4->8 for underrun headroom. - The ES8311 mono ADC duplicates onto both I2S slots (slot index is a no-op); comment corrected. Codec DAC volume to 0 dB; playback make-up gain 4x.
rec_page_k2() only blocked page-exit while RECORDING; leaving mid-PLAYING let the main loop repaint another page's first frame while the audio thread still owned I2S/codec, wedging I2S (HW-016e). Per the thc1006#14 button map K2 is inert in both RECORDING and PLAYING -- guard both.
There was a problem hiding this comment.
Pull request overview
Implements issue thc1006#14’s end-to-end mic record → RAM → speaker playback bring-up for the M5StickS3, integrating it into the app’s UI as a modal “REC” page and addressing hardware sequencing/reliability issues encountered on real devices (codec power/probe ordering and ESP32 I2S TX queue recovery).
Changes:
- Adds a record/playback engine on the audio thread (fixed-length mono buffer, gain+clip on playback, bounded TX writes, ERROR→READY recovery via I2S PREPARE).
- Reworks the UI to a dedicated
PAGE_AUDIO_RECmodal recorder with short/long press behavior and safe navigation guards. - Adds new pure DSP helpers (mono↔stereo interleave + gain/clip) with ztests, plus design/evidence artifacts and new Kconfig knobs.
Reviewed changes
Copilot reviewed 12 out of 13 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
tests/drivers/audio_dsp/src/main.c |
Adds ztests for new DSP helpers (interleave + gain/clip). |
evidence/20260609-hw014-record-playback.log |
Adds a short serial log demonstrating repeatable record→play on hardware. |
docs/14_RECORD_PLAYBACK_DESIGN.md |
Adds a requirements/baseline/design write-up for thc1006#14. |
boards/m5stack/m5stack_sticks3/m5stack_sticks3_procpu.dts |
Marks ES8311 as zephyr,deferred-init to avoid probing before its rail is up. |
app/src/ui.c |
Replaces the old AUDIO page with a modal REC page UI (state-driven rendering + live meter). |
app/src/pages.h |
Renames the audio page enum to PAGE_AUDIO_REC. |
app/src/main.c |
Implements modal button handling for REC page + capture gating while rendering. |
app/src/audio.h |
Adds the public record/playback state machine API for the UI. |
app/src/audio.c |
Implements deferred codec init, slab recovery, record/playback engine, and updated capture behavior. |
app/src/audio_dsp.h |
Declares new DSP helpers used by playback (interleave + gain/clip). |
app/src/audio_dsp.c |
Implements audio_interleave_mono() and audio_gain_clip_i16(). |
app/overlay-audio.conf |
Sets a default record-playback make-up gain for the audio overlay. |
app/Kconfig |
Adds Kconfig options for clip length and playback gain. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+872
to
+875
| cmd = rec_cmd; | ||
| rec_cmd = REC_CMD_NONE; | ||
| if (cmd == REC_CMD_RECORD) { | ||
| do_record(); |
Comment on lines
+89
to
+93
| * Mic capture (issue #6). The ES8311 mono ADC duplicates its data onto both I2S | ||
| * slots, so either slot index works; AUDIO_MIC_SLOT picks one. AUDIO_MIC_FULL is | ||
| * the empirical full-scale RMS for the PAGE_AUDIO live bar: quiet ~70-100, a | ||
| * normal voice a few hundred to a few thousand, a loud clap >= 12000, so 2000 | ||
| * gives visible bars for speech without pinning the bar to 4 on every breath. |
Comment on lines
+160
to
+162
| } else if (evt->code == INPUT_KEY_1 && evt->value != 0) { | ||
| rec_page_k2(); | ||
| } |
Comment on lines
+3
to
+5
| Status: **requirements agreed; button/mode design proposed (under review)**; not yet | ||
| implemented. Tracks GitHub issue #14 ("Audio demo: record a short mic clip and | ||
| play it back"). Feature branch: `feature/14-record-playback`. |
Comment on lines
+44
to
+45
| - Mic is mono on I2S slot 0; codec fixed at 16 kHz / 16-bit; playback volume | ||
| −20 dB; the on-board speaker is small/quiet. |
Three correctness/robustness fixes from the PR thc1006#15 / #1 Copilot review: - rec_cmd is now an atomic_t consumed with a single atomic_set() exchange (returns the previous value), so a record/play command set by the UI thread can no longer be lost in the gap between the audio thread's separate volatile read and clear. Requests/meter-loop read it atomically. - fill_play_block() snapshots rec_samples and returns 0 when pos >= total, so the unsigned `rec_samples - pos` can never underflow into a huge n / out-of-bounds rec_buf read even if a future caller drops the pos<rec_samples invariant. - main.c disarms K1 long-press tracking when K2 exits the REC page, so a held-K1 release that lands off-page no longer leaves k1_armed set and fires a spurious REC action on the next visit. Also corrects the mic-slot comment in audio.c: the ES8311 mono ADC drives slot 0 with slot 1 silent (HW-016d) — it does not duplicate onto both slots, which contradicted the deinterleave test and could send someone to the silent slot. Builds clean for m5stack_sticks3/esp32s3/procpu with overlay-audio.conf. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Per the Copilot review, the design doc was stale against the merged implementation: - Status header said "not yet implemented"; it is implemented and HW-verified (2026-06-09, dell-0830) — point at the evidence log. - Constraints/baseline hard-coded playback volume at -20 dB; the thc1006#14 path runs the codec at 0 dB and makes up level digitally via CONFIG_APP_AUDIO_REC_GAIN_Q8. Noted the -20 dB figure as the legacy thc1006#3 tone baseline. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ate in meter gate Follow-up polish from a deeper self-review (no firmware behaviour change): - audio_dsp gain test now pins the exact contract a plausible rewrite could break: truncation toward zero (-3 * 1.5 -> -4, not floored -5), the exact saturation boundary (v==32767/-32768 not clamped; 32768/-32769 clamped), and INT16_MIN at the max uint16 gain (product stays inside int32, clamps instead of wrapping). 8/8 cases pass on native_sim. - main loop reads audio_rec_get_state() once into a local for the meter gate instead of calling it twice, so the audio thread transitioning state between the two reads can't produce a torn meter decision. Builds clean for m5stack_sticks3/esp32s3/procpu; twister audio_dsp green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Per maintainer review: do_play() ended with i2s_trigger(TX, DRAIN), which has no timeout and was also reached on the TX-timeout break path. If loop_tx() timed out because the TX clock was wedged, the break still hit DRAIN against the stuck clock, hanging the audio thread with the amp still on and rec_state stuck at PLAYING (K2 inert -> the REC page could not be left): a stuck speaker. Remove the DRAIN. The existing fixed k_msleep already waits a bounded time for the pre-queued DMA tail (<= BLOCK_COUNT blocks) to play out, then the stop: path does a wedge-safe I2S_TRIGGER_DROP — the same DROP-not-DRAIN teardown do_record() and meter_session() already use. Normal and error/break paths now both converge on bounded-wait -> DROP, so nothing can hang here. Builds clean for m5stack_sticks3/esp32s3/procpu with overlay-audio.conf. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Maintainer asked for a short phone clip of the playback to lock down QR-1 (intelligible playback), the one claim the serial log can't carry. Adds a 33s on-hardware record->play clip and cross-links it from the hw014 log. evidence/20260611-hw014-record-playback-demo.mp4 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
junnncct1106
force-pushed
the
feature/14-record-playback-clean
branch
from
June 10, 2026 20:27
a757e7b to
63de615
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Implements issue thc1006#14 (microphone record → speaker playback) on the M5StickS3, plus fixes three hardware-level blockers found during serial bring-up on real hardware (ES8311 codec + AW8737 amp + esp32-i2s).
Root causes fixed
ES8311 deferred-init — the codec shares the L3B rail (
lcd_power,regulator-boot-onvia the M5PM1 PMIC). With no supply/reset link on the node andCONFIG_DEVICE_DEPSoff, the driver probed the chip-id over I2C at its POST_KERNEL priority before L3B was powered →-EFAULT "Failed to read chip id"→ audio init failed. The node is nowzephyr,deferred-initanddevice_init()'d fromaudio_init()(main context, L3B up and settled).I2S TX
mem_slableak — the esp32 i2s driver does not return TX blocks to the slab onDROPfrom the ERROR state an underrun leaves, sotx_slableaks empty and the next session's prequeue fails with-EAGAIN (-11)(eventually wedging record too). Recovered withI2S_TRIGGER_PREPARE(the documented ERROR→READY recovery that frees the queues) inloop_tx()and atdo_record()/do_play()start; TX writes bounded vialoop_tx()(a wedge times out instead of K_FOREVER-hanging the UI); playback is TX-only (full-duplex playback under-ran because the RX read stalled TX);BLOCK_COUNT4→8 for underrun headroom.K2 mid-playback guard —
rec_page_k2()only blocked page-exit while RECORDING; leaving mid-PLAYING repainted another page's first frame while the audio thread still owned I2S/codec (HW-016e). Now inert in both RECORDING and PLAYING per the Audio demo: record a short mic clip and play it back thc1006/zephyr-m5stack-sticks3#14 button map.Verified on hardware
Codec init, recording, UI navigation, and I2S slab recovery (no more
-EAGAIN/ wedge across repeated record→play sessions).Pending
Final by-ear loudness tuning on the 2011 micro-speaker — gain/level only; the playback path itself is up.
Note for maintainers
The esp32 i2s driver not returning
mem_slabblocks on DROP-from-ERROR looks like an upstream Zephyr driver issue and may be worth a separate report.Test
On the REC page: KEY1 to record, KEY1 to play back; repeat to confirm no
-EAGAINand clean page navigation.