Repository navigation
P4 panel: playback and capture cannot run together — both directions open I2S(0) #23
Description
Reported by the camera session while verifying an unrelated I2C change, and passed to me to judge whether it is a bug or intended arbitration. It is a real limitation, and it is not the audio session contract refusing.
Symptom
On the Waveshare P4 panel board: open the microphone, then call play(), and it raises. Play first and then open the microphone and it sometimes succeeds and sometimes does not.
Not the session contract
AudioSession arbitrates owners of one shared codec, and this board correctly gives each direction its own session, since it has two codecs:
_SESSION = AudioSession(codec_factory=lambda: _codec(), duplex=False) # ES8311, playback
_INPUT_SESSION = AudioSession(codec_factory=lambda: _input_codec(), duplex=False) # ES7210, captureTwo separate sessions, so acquire() never sees a second owner and never raises. The contract permits simultaneous play and record.
What actually blocks it
Both directions construct the same I2S peripheral, board_peripherals.py around lines 91 and 108:
return I2S(0, sck=Pin(_SCLK), ws=Pin(_LRCK), sd=Pin(_DSDIN), mode=I2S.TX, ...) # audio_out
return I2S(0, sck=Pin(_SCLK), ws=Pin(_LRCK), sd=Pin(_ASDOUT), mode=I2S.RX, ...) # audio_inmachine.I2S takes a direction at construction and one id cannot be both, so the second open fails. The order-dependence and the intermittency are about whether the first instance has been released, not about arbitration.
The wiring is genuinely full duplex: the two codecs share the bit and word clocks and have separate data lines (_DSDIN into the codec, _ASDOUT out of it). So this is one peripheral that should be running duplex, not two peripherals.
Why it matters
Under design principle 1, a pairing the hardware permits that the design does not deliver is a defect in the design rather than a feature request. The board has a speaker and a microphone on one bus, the API exposes both, and the two cannot be used together. Anything wanting monitoring, a loopback, an effects path, or a voice interface hits this.
It also cost real time this week: an attempt to prove speaker output by acoustic loopback was inconclusive, and could not be run at all in the obvious way for this reason.
Options, none free
- Duplex
machine.I2S. Correct, and a port-level change: the ESP32 port would need to expose one id in both directions. Upstream question. - A native duplex path in audiodev's I2S backend, bypassing
machine.I2Sfor boards whose codecs share a bus. - Document the exclusion on the board and in
audiodev, so it fails with a clear message naming the cause instead of anOSErrorfrom the second open. This is the cheap half and should happen regardless of which of the above is chosen.
Third option first, in any case: today the failure names neither the peripheral nor the reason.
Activity
bdbarnett commentedon Sep 4, 2026
The failure is worse than the
OSError, and MicroPython's own source says how.ports/esp32/machine_i2s.c:402, in the pinned v1.28.0:Constructing
I2S(0)whileI2S(0)is already in use does not raise. It deinitialises the existing instance and hands back the same object, reinitialised in the new direction. Both Python-side handles are the one object, so the caller's playback handle silently becomes a capture instance.That accounts for the reported asymmetry exactly:
OSErrorthat was seen.So the danger is not that one direction is refused. It is that the direction opened second wins by silently killing the first, and only the reverse order produces a diagnostic.
This also supplies a third explanation for an acoustic loopback on this board reading rms 71.2 during a tone against 69.2 in silence: not that the microphone cannot hear the speaker, and not only that the capture path was unavailable, but that opening the capture is what stopped the tone. That test could not have worked in any ordering.
Consequence for the options above: documenting the exclusion is no longer the cheap half, it is the urgent half, because the current behaviour is silent data loss rather than a refused call. Whatever else happens,
audiodevshould refuse the second direction itself, with a message naming the peripheral, rather than lettingmachine.I2Sreassign it.Not run on hardware — this is read from the pinned source and matches both observed behaviours. The board test that would confirm it: start a tone, open the microphone, and check whether the tone stops.
bdbarnett commentedon Sep 4, 2026
Confirmed on hardware, by ear. 440 Hz tone playing, microphone opened at the three-second mark. By ear, the tone stopped exactly when the mic opened. In the same run the software reported:
Three things that follow:
out.playingreportsTrueacross the reassignment. The flag outlives the hardware it describes, so a caller cannot detect this even afterwards. Whatever is done about refusing the second direction, that flag needs to learn it lost its peripheral.So the fix is a design call between two places:
audiodevrefuses the second direction and reports which peripheral is held, ormachine.I2Sstops silently reassigning an occupied id. The first is ours and cheap; the second is upstream and correct. They are not exclusive.bdbarnett commentedon Sep 4, 2026
Restating this as three stacked findings, because they have different owners and only one of them is ours.
1. The board models one duplex peripheral as two devices. (ours)
_output_streamand_input_streamboth constructI2S(0)with the samesck=Pin(12)andws=Pin(10), differing only insd(9 out, 11 in) and mode. TwoAudioSession(..., duplex=False)objects sit on top, so the session layer never sees a conflict either. Collapsing this to one audio device is the fix we can make — but see 2: it buys a loud refusal instead of silent data loss, not duplex. That is an honest exclusion, and worth having.2.
machine.I2Scannot express duplex at all. (upstream) Onesdpin,mode=TXorRX. The capability exists one layer down:i2s_new_channel(&cfg, &tx_handle, &rx_handle)takes both handles in a single call, andmachine_i2s.c:366calls it twice, passing NULL for the unused half. This board's wiring — shared bit and word clocks, separate data lines — is exactly what that call expects. A duplex mode takingsd_out/sd_inis upstream-shaped work, not a port hack.And the obvious workaround is closed too:
machine_i2s.c:360hardcodesI2S_ROLE_MASTER, so capturing onI2S(1)as a slave sharing the clocks is not reachable from Python either, despite the P4 having three I2S blocks. There is no Python-level path to duplex on this board today, by any arrangement.3. Silent reassignment on re-construct. (upstream, and rank it first)
machine_i2s.c:402: occupied slot,machine_i2s_deinit(self), same object handed back, no error. This is what turns 1 from a refused call into audible data loss, and it is independent of duplex — any code that constructsI2S(n)twice silently steals a live peripheral and leaves the first handle reportingplaying == True. Confirmed on hardware by ear.So: 1 is a board-config fix here, 2 is a feature request upstream, 3 is a plain upstream bug. 3 stands alone and is the one to report first; nobody has to want duplex to be bitten by it.
bdbarnett commentedon Sep 26, 2026
The pin move this was waiting on is done, so I rechecked it against 1.29. Nothing upstream changed.
ports/esp32/machine_i2s.cat v1.29.0 still deinitialises a liveI2S(0)when it's constructed again (mp_machine_i2s_make_new_instance, lines 424–431). It still opens one direction per instance, passing NULL for the other handle toi2s_new_channel(lines 378 and 380), and it still hardcodesI2S_ROLE_MASTER(line 372).Our half is #109: the board now refuses the second direction by name, so it no longer silently stops the first. It's tested with fakes both ways and not yet on the P4, which is busy with the casting session. Duplex itself is firmware work and is tracked in micropython-pydevices#5.
One question for you: do you want an upstream MicroPython issue drafted for the silent takeover? That's finding 3 above, and it bites anyone who constructs
I2S(n)twice, duplex or not. Under the current posture I'd draft it for you to post, as with the WebREPL Ctrl-C case.P4 panel: refuse the second direction on I2S(0) instead of losing the…
bdbarnett commentedon Oct 8, 2026
Checked on the P4 panel tonight (MicroPython 1.29, ESP32_GENERIC_P4 C6_WIFI).
The refusal from #109 works on the board, both ways. With the speaker open and playing,
pcm_in().open()raisesOSError: I2S(0) is in use by speaker: this board's speaker and microphone share one I2S port, so close the speaker before opening the microphone, and the speaker keeps taking writes. With the microphone open, the first speaker write raises the mirror message. Nothing is silently taken over any more.The hardware can do both at once on one port. Through the audio pump's driver,
_audioif.i2s_start(0, 12, 10, 9, 24000, din=11, mclk=13, channels=2)opens TX and RX as one channel pair, and_audioif.rt_probeplayed a quiet step on TX while RX captured 24000 bytes (250 ms) on the same clock, with no error.What's missing is a way to use that from Python.
pcm_in()opensmachine.I2S, which takes one direction per id. The pump'sInputis an audiosample the pump pulls on its own thread, so Python can't read it as PCM. Recording while playing needs aPCMInputover the pump's RX: C work in the pump's driver to hand RX blocks to Python, plus a board path that opens the duplex pair for both roles. That's beyond a board-config change, so this stays open for it.Found on the way: the board's package didn't install
es7210.py, sopcm_in()failed with ImportError on a board installed with mip. That's #147.The earlier question about drafting an upstream MicroPython issue for the silent takeover of a live
I2S(n)is still open.bdbarnett commentedon Oct 9, 2026
Recording while playing on one port is now two PRs, both waiting for review: PyDevices/audioif#25 lets the pump's driver hand its RX half to Python and lets an output and a recorder share the channel, and #153 makes
pcm_in()on both P4 boards read from that channel. On the panel, a tone played throughaudio_out()came back throughpcm_in()78 dB above the noise, with no dropouts. The measurements are in audioif#25.pcm_outis stillmachine.I2S, so it still refuses to open alongside a recording.P4 boards record while they play: pcm_in reads the audio pump's RX ha…
bdbarnett commentedon Oct 9, 2026
Playback and capture now run together on one I2S port on both ESP32-P4 boards: audioif#25 and #153. On the P4 panel a 1 kHz tone played through
audio_out()came back throughpcm_in()78 dB above the spectrum's median, with no lost frames. On the ESP32-P4-WIFI6-DEV-KIT it came back 58 dB above the noise, falling to the noise floor with the amplifier off, with no dips and no dropped frames. The ES8311's left slot (take="left") is the right one there. Still open separately:pcm_out(machine.I2S) can't play alongside a recording; routing it through the pump would fix that.