ADR-0010: Microphone and speaker share one I2S bus — every local sound relays the mic itself¶
Page en anglais
Cette page n'existe qu'en anglais.
Status: Accepted (confirmed on the device, 2026-08-05)
Date: 2026-09-06 (written retroactively from the incident and the [I2S-BUS] comments in Tab5/paquets/tab5-alarm.yaml / tab5-hardware.yaml)
Context¶
On the Tab5 the ES7210 microphone ADC and the ES8388 speaker DAC hang off a single i2s_audio bus (mic_bus, GPIO27/29/30 in tab5-hardware.yaml). ESPHome protects that bus with one mutex (i2s_audio.h, Mutex lock_): the first component that starts holds it, the other one fails with Parent bus is busy and retries once per second. microWakeWord listens permanently when "Ok Nabu" is on — the user's default state — so the microphone normally owns the bus.
The media_player already handles this for TTS: on_announcement stops microWakeWord, on_idle restarts it. Anything that plays sound outside the media player does not get that relay. The alarm clock's rtttl: melody writes to tab5_speaker directly (deliberately: it must ring without Home Assistant), and on 2026-08-05 it was completely silent — one Parent bus is busy per second, speaker stuck in STATE_STARTING. Cutting the wake-word switch stopped the errors at the same second; with the mic off the melody played and the microphone failed instead. Mutual exclusion demonstrated in both directions.
Decision¶
- Any audio path outside the media player performs the mic ↔ speaker relay itself, per sound:
micro_wake_word.stop, wait until!microphone.is_capturing(the mutex is only released instop_driver_(), not when the action returns), a 250 ms breath (is_capturingdrops inSTOPPING, a few ms before the mutex is freed), play, wait until!speaker.is_playing, thenmicro_wake_word.start. This is whattab5_alarm_ring_loopandtab5_alarm_previewdo. - The relay is unconditional — never "only if the mic is capturing": microWakeWord start/stop orders are deferred by one loop iteration (
pending_start_/pending_stop_), so testing the mic state at that instant is a coin toss against a command already in flight. media_player.on_idledoes not restart microWakeWord whilealarm_ringingis true: themedia_player.stopissued at ring start would otherwise reopen the mic behind the loop's back and mute the melody for good.- A 10 s
interval:reconciles the invariant "wake-word switch on ⇒ microWakeWord running" whenever nothing is playing (no ring, no assistant, no announcement, speaker idle) — a caller interrupted mid-relay (tab5_alarm_previewismode: restart) would otherwise leave "Ok Nabu" dead until the next TTS.
Consequences¶
- "Stop the alarm by voice" can only work during the 3 s silence window between two melody passes: while the melody plays the microphone has no bus at all. Shortening that window means revisiting the voice stop.
- The ring loop is slower to start than a naive
rtttl.play(stop + wait + 250 ms), accepted. - The games'
sfx_*()functions are stubs for the same reason: a local beep would need this whole relay. Any future local sound (game SFX, UI clicks) must go through it — there is no cheap "just play a sample" on this board while the wake word is armed. - The speaker's
on_idle-based restart of the mic is the only path that restores "Ok Nabu" after TTS; the alarm cleanup restores it explicitly in both directions (start if the switch is on, stop otherwise).