Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -78,13 +78,19 @@
channels=(1, 2),
native_channels=_MAX_CHANNELS,
bits=(16,),
wire=I2SWire(0, sck=_SCLK, ws=_LRCK, sd=_DSDIN, mck=_MCLK, mck_fs=_MCLK_FS),
# sd_in is the ES7210's data pin. Publishing it is what lets audio_out and
# pcm_in run at once on firmware whose audio pump can read the RX half of
# its own channel: the port opens as one TX/RX pair on one clock.
wire=I2SWire(
0, sck=_SCLK, ws=_LRCK, sd=_DSDIN, mck=_MCLK, mck_fs=_MCLK_FS, sd_in=_ASDOUT
),
)

# Capture is the ES7210 on the SAME I2S port and the same clocks as playback,
# with AudioSession(duplex=False) below. That is why this board cannot run the
# acoustic loopback in tools/audio_rig/ and the T-Embed can: there, output and
# input are separate peripherals on separate pins.
# Capture is the ES7210 on the SAME I2S port and the same clocks as playback.
# With the audio pump (audiodev.pump.duplex()), pcm_in reads the RX half of
# the pump's channel, so it records while audio_out plays. Without it, pcm_in
# is machine.I2S, which takes one direction per port, so it and the speaker
# refuse each other (see _refuse_if_held).
AUDIO_IN = AudioCapability(
_FORMAT,
rates=None,
Expand Down Expand Up @@ -135,13 +141,29 @@ def _i2c():
return _i2c_own


def _pump_clock_running():
try:
from audiodev import pump
except ImportError:
return False
return pump.clock_running()


def _ensure_mclk(multiplier=_MCLK_FS, rate=None):
"""PWM-bootstrap the shared codec MCLK on GPIO13 until I2S takes the pin."""
"""PWM-bootstrap the shared codec MCLK on GPIO13 until I2S takes the pin.

Never while the audio pump's channel is running: it is already driving
GPIO13, and a PWM started now would take the pin from it, so both codecs
would lose their clock mid-stream. The idle PWM is released instead.
"""
global _mclk, _mclk_rate
from machine import PWM, Pin

if rate is not None:
_mclk_rate = int(rate)
if _pump_clock_running():
_stop_mclk()
return None
freq = _mclk_rate * multiplier
if _mclk is None:
_mclk = PWM(Pin(_MCLK), freq=freq, duty_u16=32768)
Expand Down Expand Up @@ -208,11 +230,17 @@ def _refuse_if_held(session, wanted, holder):
during playback silenced the speaker with no error anywhere, and
``playing`` stayed True (pydevices#23, heard on this board). So the second
direction is refused here, by name, instead.

This still applies with the audio pump, to pcm_out: it is machine.I2S
whatever the firmware, so it and the pump's channel cannot share the port.
audio_out plays through the pump and records alongside pcm_in.
"""
if session._owners:
raise OSError(
"I2S(0) is in use by %s: this board's speaker and microphone "
"share one I2S port, so close the %s before opening the %s"
"I2S(0) is in use by the %s: this board's speaker and microphone "
"share one I2S port, and pcm_out and machine.I2S take the whole "
"port, so close the %s before opening the %s. audio_out can play "
"while pcm_in records, at the recording's rate"
% (holder, holder, wanted)
)

Expand Down Expand Up @@ -314,9 +342,17 @@ def _pcm_out(format=None, *, latency=None, queue_ms=None):
def _pcm_in(format=None, *, latency=None, queue_ms=None):
"""ES7210 capture: a raw ``PCMInput`` with hardware ADC gain.

Shares I2S(0) and its clocks with playback, so this board cannot record
itself: opening it while the speaker is open raises OSError, and so does
the reverse.
Shares I2S(0) and its clocks with playback. On firmware with the audio
pump, this reads the RX half of the pump's channel, so it records while
``audio_out`` plays (an acoustic loopback through the panel's own speaker
and microphones works), in either order. The recording must then be at
the rate the speaker is clocked at. The two microphones are averaged into
the one channel.

Without the pump it is ``machine.I2S``, which has one direction per port:
opening it while the speaker is open raises OSError, and so does the
reverse. ``pcm_out`` is always ``machine.I2S``, so it refuses alongside
a recording either way.
"""
global _mclk_rate

Expand All @@ -334,6 +370,18 @@ def _pcm_in(format=None, *, latency=None, queue_ms=None):
)
_mclk_rate = wire.rate
ibuf = queue_bytes(wire, latency, queue_ms, default=_IBUF, minimum=_MIN_IBUF)
pump = _duplex_pump()
if pump is not None:
return pump.PumpPCMInput(
wire,
wire=AUDIO_OUT.wire,
ring_bytes=ibuf,
take="mix",
before_open=lambda: _refuse_if_held(_SESSION, "microphone", "speaker"),
session=_INPUT_SESSION,
set_hardware_gain=lambda value: _INPUT_SESSION.get_codec().set_gain(value),
power=lambda enable: _input_power(enable, wire.rate),
)
return I2SPCMInput(
lambda: _input_stream(ibuf, wire),
wire,
Expand All @@ -343,6 +391,15 @@ def _pcm_in(format=None, *, latency=None, queue_ms=None):
)


def _duplex_pump():
"""``audiodev.pump`` when this firmware records while it plays, else None."""
try:
from audiodev import pump
except ImportError:
return None
return pump if pump.duplex() else None


def _audio_out(format=None, *, latency=None, queue_ms=None, **kwargs):
"""ES8311 sample player: ``play(sample, loop=)``/``stop()``/``pause()``/
``resume()``/``playing`` over any audiosample (``synthio.Synthesizer``,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,10 +107,18 @@
channels=(1, 2),
native_channels=1,
bits=(16,),
wire=I2SWire(0, sck=_SCLK, ws=_LRCK, sd=_DSDIN, mck=_MCLK, mck_fs=_MCLK_FS),
# sd_in is the ES8311's ADC output. Publishing it is what lets audio_out
# and pcm_in run at once on firmware whose audio pump can read the RX
# half of its own channel: the port opens as one TX/RX pair on one clock.
wire=I2SWire(
0, sck=_SCLK, ws=_LRCK, sd=_DSDIN, mck=_MCLK, mck_fs=_MCLK_FS, sd_in=_ASDOUT
),
)

# The microphone is the ES8311's ADC on the same I2S port and clocks.
# The microphone is the ES8311's ADC on the same I2S port and clocks. With the
# audio pump (audiodev.pump.duplex()), pcm_in reads the RX half of the pump's
# channel, so it records while audio_out plays. Without it, pcm_in is
# machine.I2S, and it and the speaker refuse each other.
AUDIO_IN = AudioCapability(
_FORMAT,
rates=None,
Expand Down Expand Up @@ -154,12 +162,37 @@ def i2c():
return _i2c_own


def _pump_clock_running():
try:
from audiodev import pump
except ImportError:
return False
return pump.clock_running()


def _duplex_pump():
"""``audiodev.pump`` when this firmware records while it plays, else None."""
try:
from audiodev import pump
except ImportError:
return None
return pump if pump.duplex() else None


def _ensure_mclk(multiplier=_MCLK_FS, rate=None):
"""PWM-bootstrap MCLK on GPIO13 until I2S takes the pin.

Never while the audio pump's channel is running: it already drives GPIO13,
and a PWM started now would take the pin from it mid-stream.
"""
global _mclk, _mclk_rate
from machine import PWM, Pin

if rate is not None:
_mclk_rate = int(rate)
if _pump_clock_running():
_stop_mclk()
return None
freq = _mclk_rate * multiplier
if _mclk is None:
_mclk = PWM(Pin(_MCLK), freq=freq, duty_u16=32768)
Expand Down Expand Up @@ -233,12 +266,16 @@ def _refuse_if_held(session, wanted, holder):

Constructing machine.I2S(0) while it is in use doesn't raise on esp32:
it deinitialises the live instance and hands the same object back in the
new direction, so the speaker would just go quiet.
new direction, so the speaker would just go quiet. pcm_out is always
machine.I2S, so this holds with the audio pump too; audio_out plays
through the pump and records alongside pcm_in.
"""
if session._owners:
raise OSError(
"I2S(0) is in use by %s: this board's speaker and microphone "
"share one I2S port, so close the %s before opening the %s"
"I2S(0) is in use by the %s: this board's speaker and microphone "
"share one I2S port, and pcm_out and machine.I2S take the whole "
"port, so close the %s before opening the %s. audio_out can play "
"while pcm_in records, at the recording's rate"
% (holder, holder, wanted)
)

Expand Down Expand Up @@ -312,8 +349,12 @@ def _pcm_out(format=None, *, latency=None, queue_ms=None):
def _pcm_in(format=None, *, latency=None, queue_ms=None):
"""The onboard microphone through the ES8311's ADC: a raw ``PCMInput``.

Shares I2S(0) and its clocks with playback, so opening it while the
speaker is open raises OSError, and so does the reverse.
Shares I2S(0) and its clocks with playback. On firmware with the audio
pump, this reads the RX half of the pump's channel, so it records while
``audio_out`` plays, in either order, at the rate the speaker is clocked
at. Without the pump it is ``machine.I2S``: opening it while the speaker
is open raises OSError, and so does the reverse. ``pcm_out`` is always
``machine.I2S``, so it refuses alongside a recording either way.
"""
global _mclk_rate

Expand All @@ -326,6 +367,19 @@ def _pcm_in(format=None, *, latency=None, queue_ms=None):
)
_mclk_rate = wire.rate
ibuf = queue_bytes(wire, latency, queue_ms, default=_IBUF, minimum=_MIN_IBUF)
pump = _duplex_pump()
if pump is not None:
# The ES8311 has one ADC channel; this records the left slot.
return pump.PumpPCMInput(
wire,
wire=AUDIO_OUT.wire,
ring_bytes=ibuf,
take="left",
before_open=lambda: _refuse_if_held(_SESSION, "microphone", "speaker"),
session=_INPUT_SESSION,
set_hardware_gain=lambda value: _INPUT_SESSION.get_codec().set_gain(value),
power=lambda enable: _INPUT_SESSION.get_codec().enable_input(enable),
)
return I2SPCMInput(
lambda: _input_stream(ibuf, wire),
wire,
Expand Down
30 changes: 28 additions & 2 deletions docs/audio.md
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,31 @@ Codec-specific features remain reachable through `device.codec`. Shared
half-duplex hardware uses `AudioSession`; opening the opposite direction while
one direction owns the session raises `OSError`.

### Recording while you play, on one I2S port

Some boards put the speaker codec and the microphone on one I2S port (the
ESP32-P4 boards do). `machine.I2S` opens one direction per port, so there you
can't record and play at the same time through it. On firmware with the audio
pump you can: `audio_out` opens the port as a TX/RX pair, and `pcm_in` returns
an `audiodev.pump.PumpPCMInput` that reads the RX half of the same channel.
Open them in either order; closing one leaves the other running.

```python
out = board_peripherals.audio_out()
mic = board_peripherals.pcm_in()
out.play(sample, loop=True)
mic.open()
buf = bytearray(48000) # one second at 24 kHz mono
mic.readinto(buf) # the speaker keeps playing
print(mic.stats()) # {'captured': ..., 'dropped': 0, 'waiting': ...}
```

The two share one clock, so they share one rate. A recorder at another rate
than the speaker is refused, and so is a sample at another rate while a
recorder is open. `pcm_out` is `machine.I2S`, so it still can't open
alongside a recording. A board joins in by publishing `sd_in=`, the
microphone's pin, on its `I2SWire`.

## Host backends (transports under `AudioOut`, or usable raw)

`audiodev.sdl2_audio` is the reference playback and real-microphone backend for
Expand Down Expand Up @@ -322,7 +347,8 @@ needed. See [`tests/test_audiodev.py`](../tests/test_audiodev.py).

## ESP32-P4 status

The Waveshare ESP32-P4 configuration uses one half-duplex session for its I2S
peripheral and ES8311. Playback exposes hardware DAC volume/mute and controls
The Waveshare ESP32-P4 configuration shares one I2S port between the ES8311
and the ES7210; with the audio pump, `audio_out` and `pcm_in` run at once (see
"Recording while you play" above), and without it the two refuse each other. Playback exposes hardware DAC volume/mute and controls
the speaker amplifier through `AudioOut`; capture exposes hardware ADC gain.
Register, stream, session, and GPIO behavior is covered by host simulations.
11 changes: 8 additions & 3 deletions lib/audiodev/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,18 +135,23 @@ class I2SWire:
such a consumer stop reaching into a board module's private names.
"""

def __init__(self, port, *, sck, ws, sd, mck=None, mck_fs=256):
def __init__(self, port, *, sck, ws, sd, mck=None, mck_fs=256, sd_in=None):
self.port = int(port)
self.sck = int(sck)
self.ws = int(ws)
self.sd = int(sd)
self.mck = None if mck is None else int(mck)
self.mck_fs = int(mck_fs)
# The microphone's data pin, when the microphone shares this wire's
# clocks. A board that publishes it can record while it plays: the
# output opens the port as a TX/RX pair and the input reads the RX
# half (``audiodev.pump.PumpPCMInput``).
self.sd_in = None if sd_in is None else int(sd_in)

def __repr__(self):
return (
"I2SWire(port=%d, sck=%d, ws=%d, sd=%d, mck=%r, mck_fs=%d)"
% (self.port, self.sck, self.ws, self.sd, self.mck, self.mck_fs)
"I2SWire(port=%d, sck=%d, ws=%d, sd=%d, mck=%r, mck_fs=%d, sd_in=%r)"
% (self.port, self.sck, self.ws, self.sd, self.mck, self.mck_fs, self.sd_in)
)


Expand Down
Loading
Loading