diff --git a/CHANGELOG.md b/CHANGELOG.md index d23d885..5cbf663 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog +## Unreleased + +- **Record while you play, on one I2S port.** `_audioif.rx_open()` reads the RX + half of the channel pair while `audiobusio.I2SOut` plays on the TX half, so a + board whose speaker codec and microphone share a port (the ESP32-P4 boards) + can do both at once. The output and the recorder each hold the channel and it + closes when the last one lets go. `I2SOut` takes `data_in=` to open the pair, + keeps the channel stereo while it can carry a recording, and refuses a sample + at another rate while a recorder is open instead of retuning the bus under + it. `retarget()` now also refuses a tail with another channel count, which + used to play at twice or half speed. + ## v0.1.1 (2026-09-24) - **`audiobusio.I2SOut.starved()` reports silence on the wire, in bytes.** It diff --git a/README.md b/README.md index 357c752..a4839bf 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ clocking zeros rather than disabling the channel, and `left_justified` and `external_clock` raise `NotImplementedError` rather than half-shifting a bus in silence. -### The six keywords and methods that are ours +### The seven keywords and methods that are ours | | what it is for | |---|---| @@ -63,6 +63,7 @@ in silence. | `sample_rate=` | The rate the bus is opened at, before anything plays. The first `play()` retunes it to whatever the sample asks for. | | `main_clock_fs=` | Bit clocks of MCLK per frame. 256 is what every board in this workspace straps and what CircuitPython's espressif port hard-codes, so it is the default — but a codec wanting 384 has nowhere else to say so, and `audiodev`'s `I2SWire` has carried the number since before this class existed. Dropping it silently is a half-shifted clock that sounds like a bad cable. | | `sink=` | Desktop only: the file the blocks are written to, which is what makes the whole lifecycle testable with no board. | +| `data_in=` | The microphone's data pin, on a board whose microphone shares this output's clocks. The channel opens as a TX/RX pair so `_audioif.rx_open()` can record while this plays (see below). The channel then stays stereo, so a mono sample is copied to both slots on the pump's thread, and while a recorder is open a sample at another rate is refused rather than retuning the bus under the recording. | | `retarget(sample, loop=…)` | Swap the tail **without a gap** — no `stop()`, no join, no channel close and re-open between two blocks a listener is in the middle of. | | `.status` | The pump's status `bytearray`, which is the only way a caller that spawned through `play()` can reach the two words that say why the pump stopped. | @@ -137,7 +138,12 @@ audiopump.shutdown() # closes the channel with the task | `i2s_start(port, bclk, ws, dout, rate, …)` | Opens a TX channel the pull loop writes every block into, and returns the bytes the DMA holds when full — the block-to-wire latency floor. `bits`, `channels`, `mclk`, `mclk_fs`, `dma_desc`, `dma_frame` are keywords. | | `i2s_start(…, din=PIN)` | Opens the RX half of the **same** channel pair, so one clock tree drives capture and playback and the two DMAs cannot drift. | | `i2s_start(…, din=, in_port=, in_bclk=, in_ws=, in_mclk=)` | Opens RX on a **different** peripheral, for a board whose microphone is not on the speaker's port. See the warning below. | -| `i2s_stop()` | Closes both channels. | +| `i2s_stop()` | Closes both channels, a recording included. | +| `rx_open(port, bclk, ws, dout, din, rate, mclk=, mclk_fs=, ring=, take=)` | Records from the RX half while an output plays on the same port. Opens the channel pair if nothing has (TX then clocks zeros until an output joins), or joins the one `I2SOut(..., data_in=)` opened; the output and the recorder each hold the channel and it closes when the last lets go. The RX interrupt copies every DMA block into a ring of `ring` bytes, taking the left slot, the right, their average or both (`take` 0, 1, 2, 3). A recorder at another rate than the open channel is refused by name. | +| `rx_read(buf)` | Copies whole frames from that ring into `buf` and returns the bytes copied, 0 when nothing is waiting. Never blocks. | +| `rx_close()` | Stops recording. The channel closes only if no output holds it. | +| `rx_stats()` | `(frames captured, frames dropped, bytes waiting)`. Dropped frames are a reader that fell further behind than `ring`. | +| `i2s_state()` | `None` with no channel open, else `(rate, channels, has_rx, holders)`, holders 1 the output, 2 the recorder, 3 both. A board asks before touching a clock pin the channel may be driving. | | `i2s_dma_bytes()`, `i2s_rx_bytes()` | What the two DMAs actually clocked, from their ISRs. Bytes the DMA sent that the pump never wrote *are* the silence a listener heard, so these are how starvation gets measured at all. | | `Input(sample_rate=, channel_count=, frames=, timeout_ms=)` | An audiosample whose `get_buffer` is an I2S read, so a live microphone is a source like any other and `audioeffects.create(name, Input(…), rate)` builds a graph on it. The read blocks, and that is the pacing. `stats()` reports what it has read and what it has missed. | | `rt_probe(capture, frames=, lead=, prime=, level=, channels=, timeout_ms=)` | Round-trip latency in frames rather than in host time: it preloads a click into the TX DMA *before* either channel is enabled and captures RX from the same instant. Runs with no pump spawned — two owners of one channel is the failure that sounds like silence. | diff --git a/_audioif.c b/_audioif.c index f131c08..2a72f75 100644 --- a/_audioif.c +++ b/_audioif.c @@ -938,6 +938,128 @@ static volatile uint32_t audiopump_byte_rate; static volatile uint64_t audiopump_starved_t0; static volatile uint64_t audiopump_starved_fed0; +// Who holds the open channel (AUDIOPUMP_I2S_HOLD_*), and what it was opened +// with. A second holder joins only if it asks for the same wire. +static uint8_t audiopump_i2s_holds; +static audiopump_i2s_cfg_t audiopump_i2s_live; + +// --- capture: the RX half, handed to the interpreter ------------------------ +// +// The capture half of the channel pair, as PCM Python can read. `Input` above +// it is an audiosample the PUMP pulls on its own task; a recorder on the +// interpreter thread has no way to reach that. This is the other way out: the +// RX DMA's interrupt copies every finished descriptor into a ring, and +// `rx_read()` empties it. +// +// Why the interrupt, and not a bigger DMA. The channel pair is allocated with +// one config, so the RX DMA is exactly as deep as TX's -- six descriptors of +// 5 ms, which is the output's latency floor and is chosen for the output. A +// recorder that reads every 50 ms from Python would lose most of what that +// holds. The ring decouples the two: TX keeps its 30 ms, RX gets as much +// history as the reader asks for. +// +// It takes one slot, the other, their average, or both, because a codec with +// two microphones on one wire (the ES7210 puts MIC1 left and MIC2 right) and a +// mono recorder want a choice the hardware does not make. The conversion is +// done here, in the interrupt, so the ring holds what the reader will get. +// +// Single producer (the interrupt) and single consumer (the interpreter), so +// the ring itself needs no lock. The spinlock below is only for taking the +// ring away while an interrupt on the other core might be inside it. +#define AUDIOPUMP_TAKE_LEFT (0) +#define AUDIOPUMP_TAKE_RIGHT (1) +#define AUDIOPUMP_TAKE_MIX (2) +#define AUDIOPUMP_TAKE_BOTH (3) + +typedef struct { + uint8_t *buf; + uint32_t len; // bytes, a whole number of output frames + uint32_t wpos; // the interrupt's + uint32_t rpos; // the interpreter's + volatile uint32_t written; // bytes ever written; differences are exact + volatile uint32_t taken; // bytes ever read + volatile uint32_t frames; // frames that reached the ring + volatile uint32_t dropped; // frames the ring had no room for + uint8_t take; + uint8_t out_frame; // 2 for one channel out, 4 for two + volatile bool armed; +} audiopump_cap_t; + +static audiopump_cap_t audiopump_cap; +static portMUX_TYPE audiopump_cap_mux = portMUX_INITIALIZER_UNLOCKED; + +// In the interrupt. `in` is the finished descriptor: signed 16-bit stereo, +// because the channel is opened stereo whenever there is an RX half to read. +// A full ring keeps what it has and counts the rest, so a slow reader loses +// the newest audio rather than corrupting the oldest. +static IRAM_ATTR void audiopump_cap_push(const int16_t *in, uint32_t frames) { + audiopump_cap_t *c = &audiopump_cap; + const uint32_t taken = __atomic_load_n(&c->taken, __ATOMIC_ACQUIRE); + const uint32_t room = (c->len - (c->written - taken)) / c->out_frame; + const uint32_t n = frames < room ? frames : room; + uint8_t *buf = c->buf; + uint32_t pos = c->wpos; + for (uint32_t f = 0; f < n; f++) { + const int16_t l = in[2 * f]; + const int16_t r = in[2 * f + 1]; + int16_t *out = (int16_t *)(void *)(buf + pos); + if (c->out_frame == 4) { + out[0] = l; + out[1] = r; + } else if (c->take == AUDIOPUMP_TAKE_LEFT) { + out[0] = l; + } else if (c->take == AUDIOPUMP_TAKE_RIGHT) { + out[0] = r; + } else { + out[0] = (int16_t)(((int32_t)l + (int32_t)r) >> 1); + } + pos += c->out_frame; + if (pos >= c->len) { + pos = 0; + } + } + c->wpos = pos; + c->frames += n; + c->dropped += frames - n; + __atomic_store_n(&c->written, c->written + n * c->out_frame, + __ATOMIC_RELEASE); +} + +// On the interpreter. Whole frames only; returns bytes copied. +static uint32_t audiopump_cap_pull(uint8_t *dst, uint32_t want) { + audiopump_cap_t *c = &audiopump_cap; + if (c->buf == NULL) { + return 0; + } + want -= want % c->out_frame; + const uint32_t written = __atomic_load_n(&c->written, __ATOMIC_ACQUIRE); + const uint32_t have = written - c->taken; + const uint32_t n = want < have ? want : have; + const uint32_t first = n < c->len - c->rpos ? n : c->len - c->rpos; + memcpy(dst, c->buf + c->rpos, first); + memcpy(dst + first, c->buf, n - first); + uint32_t pos = c->rpos + n; + if (pos >= c->len) { + pos -= c->len; + } + c->rpos = pos; + __atomic_store_n(&c->taken, c->taken + n, __ATOMIC_RELEASE); + return n; +} + +// Take the ring away. The interrupt checks `armed` inside the same spinlock, +// so once this returns nothing is writing into the buffer it frees. +static void audiopump_cap_release(void) { + portENTER_CRITICAL(&audiopump_cap_mux); + audiopump_cap.armed = false; + uint8_t *buf = audiopump_cap.buf; + audiopump_cap.buf = NULL; + portEXIT_CRITICAL(&audiopump_cap_mux); + if (buf != NULL) { + heap_caps_free(buf); + } +} + static IRAM_ATTR bool audiopump_on_sent(i2s_chan_handle_t handle, i2s_event_data_t *event, void *user_ctx) { (void)handle; @@ -951,10 +1073,23 @@ static IRAM_ATTR bool audiopump_on_recv(i2s_chan_handle_t handle, (void)handle; (void)user_ctx; audiopump_rx_bytes += event->size; + if (audiopump_cap.armed) { + portENTER_CRITICAL_ISR(&audiopump_cap_mux); + if (audiopump_cap.armed && audiopump_cap.buf != NULL) { + audiopump_cap_push((const int16_t *)event->dma_buf, + (uint32_t)(event->size / 4)); + } + portEXIT_CRITICAL_ISR(&audiopump_cap_mux); + } return false; } static void audiopump_i2s_close(void) { + // Everything goes, whoever was holding it: this is the teardown a soft + // reset and `i2s_stop()` take. The ring first, so no interrupt is inside + // it when the channel stops. + audiopump_cap_release(); + audiopump_i2s_holds = 0; if (audiopump_i2s_rx != NULL) { i2s_channel_disable(audiopump_i2s_rx); } @@ -1166,7 +1301,58 @@ const audiodsp_port_ops_t *audiodsp_port_driver(void) { // beside every other board decision -- exactly as usbif_i2s.c does it. #if AUDIOIF_DRV_ESP +static uint32_t audiopump_i2s_open_raw(const audiopump_i2s_cfg_t *cfg); + +static uint32_t audiopump_i2s_depth(void) { + return audiopump_dma_depth; +} + +// The pins and the clock, which two holders of one channel have to agree on. +// Raises naming the first thing that differs, because "Peripheral in use" +// would send a caller looking for a second owner that does not exist. +static void audiopump_i2s_check_join(const audiopump_i2s_cfg_t *want, + bool want_rx) { + const audiopump_i2s_cfg_t *live = &audiopump_i2s_live; + if (want->port != live->port || want->bclk != live->bclk + || want->ws != live->ws + || (want->dout >= 0 && want->dout != live->dout) + || (want->din >= 0 && want->din != live->din)) { + mp_raise_msg(&mp_type_RuntimeError, MP_ERROR_TEXT("Peripheral in use")); + } + if (want_rx && live->din < 0) { + mp_raise_ValueError(MP_ERROR_TEXT( + "the I2S channel was opened without its microphone pin; open the " + "output with data_in= to record while it plays")); + } + if (want->rate != live->rate) { + mp_raise_msg_varg(&mp_type_ValueError, MP_ERROR_TEXT( + "the I2S channel is clocked at %d Hz and this asks for %d Hz; " + "playback and capture share the clock, so they must share the rate"), + live->rate, want->rate); + } + if (want->bits != live->bits || want->channels != live->channels) { + mp_raise_ValueError(MP_ERROR_TEXT( + "the I2S channel is open with another sample width or slot count")); + } +} + +// The OUTPUT's open. Joins a channel the capture reader holds; refuses a +// second output, as it always has. uint32_t audiopump_i2s_open(const audiopump_i2s_cfg_t *cfg) { + if (audiopump_i2s_tx != NULL) { + if (audiopump_i2s_holds & AUDIOPUMP_I2S_HOLD_OUT) { + mp_raise_ValueError(MP_ERROR_TEXT("i2s already open")); + } + audiopump_i2s_check_join(cfg, false); + audiopump_i2s_holds |= AUDIOPUMP_I2S_HOLD_OUT; + return audiopump_i2s_depth(); + } + const uint32_t depth = audiopump_i2s_open_raw(cfg); + audiopump_i2s_holds = AUDIOPUMP_I2S_HOLD_OUT; + return depth; +} + +static uint32_t audiopump_i2s_open_raw(const audiopump_i2s_cfg_t *cfg) { if (audiopump_i2s_tx != NULL) { mp_raise_ValueError(MP_ERROR_TEXT("i2s already open")); } @@ -1298,6 +1484,7 @@ uint32_t audiopump_i2s_open(const audiopump_i2s_cfg_t *cfg) { audiopump_i2s_close(); mp_raise_OSError(MP_EIO); } + audiopump_i2s_live = *cfg; // Bytes the DMA holds when full: the block-to-wire latency floor. return chan_config.dma_desc_num * chan_config.dma_frame_num * (uint32_t)(bits / 8) * (uint32_t)(cfg->channels == 1 ? 1 : 2); @@ -1396,6 +1583,189 @@ static mp_obj_t audiopump_i2s_starved_bytes(void) { static MP_DEFINE_CONST_FUN_OBJ_0(audiopump_i2s_starved_bytes_obj, audiopump_i2s_starved_bytes); +// --- rx_open / rx_read: recording while the output plays ------------------- +// +// _audioif.rx_open(0, 12, 10, 9, 11, 24000, mclk=13, ring=20000, take=2) +// n = _audioif.rx_read(buf) # never blocks; whole frames +// _audioif.rx_close() +// +// Opens the channel pair if nothing has (TX then clocks zeros until an output +// joins), or joins the one `audiobusio.I2SOut(..., data_in=)` opened. Either +// way the output and the recorder are the two halves of one channel and one +// clock, so neither can take the port away from the other: closing the +// output leaves the recording running, and closing the recorder leaves the +// output playing. +// +// `dout` is the speaker's pin even for a recorder that never plays, because +// the pair is allocated together and an output arriving later has to find +// its half already wired. +static mp_obj_t audiopump_rx_open(size_t n_args, const mp_obj_t *pos_args, + mp_map_t *kw_args) { + enum { ARG_port, ARG_bclk, ARG_ws, ARG_dout, ARG_din, ARG_rate, ARG_mclk, + ARG_mclk_fs, ARG_ring, ARG_take }; + static const mp_arg_t allowed[] = { + { MP_QSTR_port, MP_ARG_REQUIRED | MP_ARG_INT, { .u_int = 0 } }, + { MP_QSTR_bclk, MP_ARG_REQUIRED | MP_ARG_INT, { .u_int = -1 } }, + { MP_QSTR_ws, MP_ARG_REQUIRED | MP_ARG_INT, { .u_int = -1 } }, + { MP_QSTR_dout, MP_ARG_REQUIRED | MP_ARG_INT, { .u_int = -1 } }, + { MP_QSTR_din, MP_ARG_REQUIRED | MP_ARG_INT, { .u_int = -1 } }, + { MP_QSTR_rate, MP_ARG_REQUIRED | MP_ARG_INT, { .u_int = 48000 } }, + { MP_QSTR_mclk, MP_ARG_INT, { .u_int = -1 } }, + { MP_QSTR_mclk_fs, MP_ARG_INT, { .u_int = 256 } }, + // Bytes of history the reader may fall behind by before audio is + // dropped. In the reader's format, so 20000 is 416 ms of one channel + // at 24 kHz. + { MP_QSTR_ring, MP_ARG_INT, { .u_int = 20000 } }, + // 0 left slot, 1 right, 2 their average (one channel out), 3 both. + { MP_QSTR_take, MP_ARG_INT, { .u_int = AUDIOPUMP_TAKE_MIX } }, + }; + mp_arg_val_t args[MP_ARRAY_SIZE(allowed)]; + mp_arg_parse_all(n_args, pos_args, kw_args, MP_ARRAY_SIZE(allowed), + allowed, args); + + if (audiopump_i2s_holds & AUDIOPUMP_I2S_HOLD_IN) { + mp_raise_ValueError(MP_ERROR_TEXT("capture already open")); + } + const int take = args[ARG_take].u_int; + if (take < AUDIOPUMP_TAKE_LEFT || take > AUDIOPUMP_TAKE_BOTH) { + mp_raise_ValueError(MP_ERROR_TEXT("take must be 0, 1, 2 or 3")); + } + if (args[ARG_din].u_int < 0 || args[ARG_dout].u_int < 0) { + mp_raise_ValueError(MP_ERROR_TEXT("rx_open needs both dout and din")); + } + const uint8_t out_frame = take == AUDIOPUMP_TAKE_BOTH ? 4 : 2; + uint32_t len = (uint32_t)args[ARG_ring].u_int; + len -= len % out_frame; + if (len < 64u * out_frame) { + mp_raise_ValueError(MP_ERROR_TEXT("ring is too small")); + } + + const int rate = args[ARG_rate].u_int; + const audiopump_i2s_cfg_t cfg = { + .port = args[ARG_port].u_int, + .bclk = args[ARG_bclk].u_int, + .ws = args[ARG_ws].u_int, + .dout = args[ARG_dout].u_int, + .rate = rate, + .bits = 16, + .channels = 2, + .mclk = args[ARG_mclk].u_int, + .mclk_fs = args[ARG_mclk_fs].u_int, + .dma_desc = 6, + .dma_frame = audiopump_i2s_dma_frame(rate), + .din = args[ARG_din].u_int, + .in_port = -1, + .in_bclk = -1, + .in_ws = -1, + .in_mclk = -1, + }; + if (audiopump_i2s_tx != NULL) { + audiopump_i2s_check_join(&cfg, true); + } + + // The ring before the channel, so a refusal for memory leaves nothing + // open. Internal RAM first: the interrupt copies into it. + uint8_t *buf = heap_caps_malloc_prefer(len, 2, + MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT, MALLOC_CAP_8BIT); + if (buf == NULL) { + mp_raise_msg(&mp_type_MemoryError, + MP_ERROR_TEXT("no memory for the capture ring")); + } + portENTER_CRITICAL(&audiopump_cap_mux); + audiopump_cap.buf = buf; + audiopump_cap.len = len; + audiopump_cap.wpos = 0; + audiopump_cap.rpos = 0; + audiopump_cap.written = 0; + audiopump_cap.taken = 0; + audiopump_cap.frames = 0; + audiopump_cap.dropped = 0; + audiopump_cap.take = (uint8_t)take; + audiopump_cap.out_frame = out_frame; + audiopump_cap.armed = audiopump_i2s_tx != NULL; + portEXIT_CRITICAL(&audiopump_cap_mux); + + if (audiopump_i2s_tx == NULL) { + nlr_buf_t nlr; + if (nlr_push(&nlr) == 0) { + (void)audiopump_i2s_open_raw(&cfg); + nlr_pop(); + } else { + audiopump_cap_release(); + nlr_jump(nlr.ret_val); + } + audiopump_i2s_holds = AUDIOPUMP_I2S_HOLD_IN; + audiopump_cap.armed = true; + } else { + audiopump_i2s_holds |= AUDIOPUMP_I2S_HOLD_IN; + } + return mp_obj_new_int_from_uint(len); +} +static MP_DEFINE_CONST_FUN_OBJ_KW(audiopump_rx_open_obj, 6, audiopump_rx_open); + +// Copy what has been captured into `buf`, up to its length in whole frames. +// Returns the bytes copied, 0 when nothing is waiting. Never blocks: a +// recorder decides for itself how long to wait, on its own thread. +static mp_obj_t audiopump_rx_read(mp_obj_t buf_in) { + if (!(audiopump_i2s_holds & AUDIOPUMP_I2S_HOLD_IN) + || audiopump_cap.buf == NULL) { + mp_raise_OSError(MP_ENODEV); + } + mp_buffer_info_t buf; + mp_get_buffer_raise(buf_in, &buf, MP_BUFFER_WRITE); + return mp_obj_new_int_from_uint( + audiopump_cap_pull((uint8_t *)buf.buf, (uint32_t)buf.len)); +} +static MP_DEFINE_CONST_FUN_OBJ_1(audiopump_rx_read_obj, audiopump_rx_read); + +// Stop recording. The channel closes only if no output holds it. Safe to +// call twice. +static mp_obj_t audiopump_rx_close(void) { + if (!(audiopump_i2s_holds & AUDIOPUMP_I2S_HOLD_IN)) { + return mp_const_none; + } + audiopump_cap_release(); + audiopump_i2s_holds &= (uint8_t)~AUDIOPUMP_I2S_HOLD_IN; + if (audiopump_i2s_holds == 0) { + audiopump_i2s_close(); + } + return mp_const_none; +} +static MP_DEFINE_CONST_FUN_OBJ_0(audiopump_rx_close_obj, audiopump_rx_close); + +// (frames captured, frames dropped because the ring was full, bytes waiting). +// `dropped` is the number a recorder watches: anything but 0 is a hole in +// the recording, and it means the reader fell further behind than `ring`. +static mp_obj_t audiopump_rx_stats(void) { + audiopump_cap_t *c = &audiopump_cap; + const uint32_t waiting = c->buf == NULL ? 0 + : __atomic_load_n(&c->written, __ATOMIC_ACQUIRE) - c->taken; + mp_obj_t items[3] = { + mp_obj_new_int_from_uint(c->frames), + mp_obj_new_int_from_uint(c->dropped), + mp_obj_new_int_from_uint(waiting), + }; + return mp_obj_new_tuple(3, items); +} +static MP_DEFINE_CONST_FUN_OBJ_0(audiopump_rx_stats_obj, audiopump_rx_stats); + +// None when no channel is open, else (rate, channels, has_rx, holders): +// holders is 1 for the output, 2 for the recorder, 3 for both. A board asks +// this before it touches a clock pin the channel may already be driving. +static mp_obj_t audiopump_i2s_state(void) { + if (audiopump_i2s_tx == NULL) { + return mp_const_none; + } + mp_obj_t items[4] = { + mp_obj_new_int(audiopump_i2s_live.rate), + mp_obj_new_int(audiopump_i2s_live.channels), + mp_obj_new_bool(audiopump_i2s_rx != NULL), + mp_obj_new_int(audiopump_i2s_holds), + }; + return mp_obj_new_tuple(4, items); +} +static MP_DEFINE_CONST_FUN_OBJ_0(audiopump_i2s_state_obj, audiopump_i2s_state); + // --- audiopump.Input: the capture side, as an audiosample ------------------ // // An audiosample whose get_buffer is an i2s_channel_read. That makes the live @@ -1788,6 +2158,11 @@ static const mp_rom_map_elem_t audioif_driver_globals_table[] = { MP_ROM_PTR(&audiopump_i2s_sink_bytes_obj) }, { MP_ROM_QSTR(MP_QSTR_i2s_starved_bytes), MP_ROM_PTR(&audiopump_i2s_starved_bytes_obj) }, + { MP_ROM_QSTR(MP_QSTR_i2s_state), MP_ROM_PTR(&audiopump_i2s_state_obj) }, + { MP_ROM_QSTR(MP_QSTR_rx_open), MP_ROM_PTR(&audiopump_rx_open_obj) }, + { MP_ROM_QSTR(MP_QSTR_rx_read), MP_ROM_PTR(&audiopump_rx_read_obj) }, + { MP_ROM_QSTR(MP_QSTR_rx_close), MP_ROM_PTR(&audiopump_rx_close_obj) }, + { MP_ROM_QSTR(MP_QSTR_rx_stats), MP_ROM_PTR(&audiopump_rx_stats_obj) }, { MP_ROM_QSTR(MP_QSTR_Input), MP_ROM_PTR(&audiopump_input_type) }, { MP_ROM_QSTR(MP_QSTR_rt_probe), MP_ROM_PTR(&audiopump_rt_probe_obj) }, { MP_ROM_QSTR(MP_QSTR_lock_probe), @@ -1826,7 +2201,26 @@ bool audiopump_i2s_is_open(void) { void audiopump_i2s_shutdown(void) { #if AUDIOIF_DRV_ESP - audiopump_i2s_close(); + audiopump_i2s_holds &= (uint8_t)~AUDIOPUMP_I2S_HOLD_OUT; + if (audiopump_i2s_holds == 0) { + audiopump_i2s_close(); + } + #endif +} + +bool audiopump_i2s_input_held(void) { + #if AUDIOIF_DRV_ESP + return (audiopump_i2s_holds & AUDIOPUMP_I2S_HOLD_IN) != 0; + #else + return false; + #endif +} + +uint32_t audiopump_i2s_live_rate(void) { + #if AUDIOIF_DRV_ESP + return audiopump_i2s_tx != NULL ? (uint32_t)audiopump_i2s_live.rate : 0; + #else + return 0; #endif } diff --git a/audiobusio.c b/audiobusio.c index 9b825a5..e50ec68 100644 --- a/audiobusio.c +++ b/audiobusio.c @@ -83,6 +83,8 @@ typedef struct _audiobusio_i2sout_obj_t { // buffer, allocated once per play(). See "the converted path" below. mp_obj_t conv_buf; uint32_t conv_len; + bool converting; // the pump converts every block into conv_buf + uint8_t tail_channels; // channel count of what the pump is pulling mp_obj_t ring; mp_obj_t ring_write; // bound methods, resolved once: no allocation mp_obj_t ring_space; // per fill, which happens a few hundred times a @@ -184,14 +186,7 @@ static int audiobusio_pin_gpio(mp_obj_t pin_or_int, qstr what) { // Sized in time it is zero at every rate, and 48 kHz still lands on the 240 // that is already proven. static int audiobusio_dma_frame(int rate) { - int frames = rate / 200; // one descriptor, 5 ms - if (frames < 64) { - frames = 64; // a floor: tiny descriptors cost IRQs - } - if (frames > 240) { - frames = 240; - } - return frames; + return audiopump_i2s_dma_frame(rate); } static audiobusio_i2sout_obj_t *audiobusio_owner(void) { @@ -207,6 +202,27 @@ static void audiobusio_check(audiobusio_i2sout_obj_t *self) { } } +// --- sharing the wire with a microphone ---------------------------------- +// +// On a board whose codec and microphone sit on one I2S port, the output and +// the recorder are the two halves of one channel pair and share its clocks. +// Two things this output normally does to the channel would then change the +// recording under the recorder, so it stops doing them: +// +// It no longer opens a MONO channel for a mono sample. A mono slot mode would +// make the RX half mono too, so the channel stays stereo and a mono sample +// goes the converted path instead, which copies it to both slots on the +// pump's thread -- exactly what CircuitPython's output does with mono. +// +// And it does not retune the bus to a sample at another rate while the +// recorder holds the channel: that would move the microphone's rate, and the +// recording would carry on at the wrong speed with nothing to say so. It +// raises instead, naming both rates. With no recorder open it retunes as +// always, and the RX half follows. +static bool audiobusio_stereo_locked(audiobusio_i2sout_obj_t *self) { + return self->wire.din >= 0 || audiopump_i2s_input_held(); +} + // --- the feeder ----------------------------------------------------------- // Two reasons the pump cannot simply be pointed at the sample. @@ -449,7 +465,8 @@ static mp_obj_t audiobusio_i2sout_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *all_args) { enum { ARG_bit_clock, ARG_word_select, ARG_data, ARG_main_clock, ARG_left_justified, ARG_external_clock, - ARG_port, ARG_sample_rate, ARG_main_clock_fs, ARG_sink }; + ARG_port, ARG_sample_rate, ARG_main_clock_fs, ARG_sink, + ARG_data_in }; static const mp_arg_t allowed[] = { { MP_QSTR_bit_clock, MP_ARG_OBJ | MP_ARG_REQUIRED, { .u_obj = MP_OBJ_NULL } }, @@ -484,6 +501,12 @@ static mp_obj_t audiobusio_i2sout_make_new(const mp_obj_type_t *type, // Desktop only: the file the blocks are written to. { MP_QSTR_sink, MP_ARG_OBJ | MP_ARG_KW_ONLY, { .u_obj = mp_const_none } }, + // The microphone's data pin, on a board whose codec and microphone + // share this output's clocks. The channel is then opened as a TX/RX + // pair, so `_audioif.rx_open()` can record while this plays; see + // "sharing the wire with a microphone" below. + { MP_QSTR_data_in, MP_ARG_OBJ | MP_ARG_KW_ONLY, + { .u_obj = mp_const_none } }, }; mp_arg_val_t args[MP_ARRAY_SIZE(allowed)]; mp_arg_parse_all_kw_array(n_args, n_kw, all_args, MP_ARRAY_SIZE(allowed), @@ -521,7 +544,8 @@ static mp_obj_t audiobusio_i2sout_make_new(const mp_obj_type_t *type, self->wire.mclk_fs = args[ARG_main_clock_fs].u_int; self->wire.dma_desc = 6; self->wire.dma_frame = audiobusio_dma_frame(self->wire.rate); - self->wire.din = -1; + self->wire.din = audiobusio_pin_gpio(args[ARG_data_in].u_obj, + MP_QSTR_data_in); self->wire.in_port = -1; self->wire.in_bclk = -1; self->wire.in_ws = -1; @@ -598,7 +622,12 @@ static void audiobusio_start_converted(audiobusio_i2sout_obj_t *self, if (frames == 0) { mp_raise_ValueError(MP_ERROR_TEXT("sample has no frames")); } - const uint32_t need = frames * 4; // stereo signed 16-bit + // Stereo signed 16-bit, and twice what this sample needs: a `retarget()` + // keeps the conversion, and the tail it swaps in -- a root Mixer that + // audiodev builds when a second client arrives -- is sized at twice its + // first voice's block. The scratch cannot be resized under a running + // pump, so it is sized for that here rather than refused there. + const uint32_t need = frames * 4 * 2; if (self->conv_buf == MP_OBJ_NULL || self->conv_len < need) { self->conv_buf = mp_obj_new_bytearray_by_ref(need, m_new0(uint8_t, need)); @@ -673,6 +702,15 @@ static mp_obj_t audiobusio_i2sout_play(size_t n_args, const mp_obj_t *pos_args, mp_obj_t sample = args[ARG_sample].u_obj; audiosample_base_t *base = audiosample_check(sample); // raises TypeError + // Before anything stops: a refusal must leave what is playing playing. + if (audiopump_i2s_have() && audiopump_i2s_input_held() + && base->sample_rate != self->rate) { + mp_raise_msg_varg(&mp_type_ValueError, MP_ERROR_TEXT( + "a microphone is recording at %u Hz on this I2S channel; a " + "%u Hz sample would change its rate"), + (unsigned)self->rate, (unsigned)base->sample_rate); + } + // CircuitPython stops whatever was playing. So do we, and the join is // what makes the pump free for the new graph. audiobusio_halt(self); @@ -683,7 +721,8 @@ static mp_obj_t audiobusio_i2sout_play(size_t n_args, const mp_obj_t *pos_args, // moment -- the pump has been joined -- which is the only moment it is // safe to reopen. The slot count moves with it: a mono signed sample // opens a mono channel rather than being copied into a stereo one. - const bool direct = audiobusio_direct(sample, base); + const bool direct = audiobusio_direct(sample, base) + && !(audiobusio_stereo_locked(self) && base->channel_count != 2); const int want_channels = direct ? (int)base->channel_count : 2; if (audiopump_i2s_have() && (base->sample_rate != self->rate @@ -703,13 +742,17 @@ static mp_obj_t audiobusio_i2sout_play(size_t n_args, const mp_obj_t *pos_args, // is silence somebody asked for, and charging it here would make the // number grow with how long the board sat idle. audiopump_i2s_starved_reset(); + self->converting = false; + self->tail_channels = base->channel_count; if (direct) { self->sample = sample; audiobusio_start_direct(self, sample, self->loop); } else if (!audiobusio_file_backed(sample)) { audiobusio_start_converted(self, sample, self->loop, base); + self->converting = true; } else { audiobusio_start_fed(self, sample, self->loop, base); + self->tail_channels = 2; // the pump pulls the stereo ring } self->playing = true; #if MICROPY_ENABLE_SCHEDULER @@ -783,6 +826,20 @@ static mp_obj_t audiobusio_i2sout_retarget(size_t n_args, mp_raise_ValueError(MP_ERROR_TEXT( "retarget cannot retune the bus; call play() instead")); } + // The pump writes what it pulls (or what it converts it to) straight into + // a channel whose slot count was set for the old tail. A tail with another + // channel count would play at twice or half speed, so it is refused. + if (base->channel_count != self->tail_channels) { + mp_raise_ValueError(MP_ERROR_TEXT( + "retarget cannot change the channel count; call play() instead")); + } + if (self->converting && base->channel_count + && (base->max_buffer_length / (2u * base->channel_count)) * 4u + > self->conv_len) { + mp_raise_ValueError(MP_ERROR_TEXT( + "retarget: this sample's block is too big for the conversion " + "buffer; call play() instead")); + } audiopump_c_retarget(sample, args[ARG_loop].u_bool ? 1 : 0); self->sample = sample; self->loop = args[ARG_loop].u_bool; diff --git a/audiopump_i2s.h b/audiopump_i2s.h index de6fdc6..c6a7ad0 100644 --- a/audiopump_i2s.h +++ b/audiopump_i2s.h @@ -40,6 +40,37 @@ typedef struct { int in_mclk; } audiopump_i2s_cfg_t; +// Who is holding the open channel. The output (`audiobusio.I2SOut`, or +// `_audioif.i2s_start`) and the capture reader (`_audioif.rx_open`) each hold +// it, and it closes when the last one lets go. That is what lets a board whose +// speaker and microphone share one port record while it plays: both are the +// two halves of ONE channel pair, opened once, on one clock. +#define AUDIOPUMP_I2S_HOLD_OUT (1) +#define AUDIOPUMP_I2S_HOLD_IN (2) + +// One DMA descriptor, in frames, for a given rate: 5 ms, between 64 and 240. +// The output and the capture reader both open the channel, so they must size +// it the same way; the measurement above audiobusio_dma_frame in audiobusio.c +// says why it is sized in time. +static inline int audiopump_i2s_dma_frame(int rate) { + int frames = rate / 200; + if (frames < 64) { + frames = 64; + } + if (frames > 240) { + frames = 240; + } + return frames; +} + +// True while the capture reader holds the channel. The output must not retune +// it then: the microphone is clocked by the same BCLK, so a new rate would +// change the recording's rate under it. +bool audiopump_i2s_input_held(void); + +// The rate the open channel is clocked at, or 0 when nothing is open. +uint32_t audiopump_i2s_live_rate(void); + // True where this build has an I2S peripheral at all -- false on every // desktop port, which is how audiobusio decides what it is. bool audiopump_i2s_have(void); @@ -47,12 +78,19 @@ bool audiopump_i2s_have(void); // True while a channel is open. bool audiopump_i2s_is_open(void); -// Open TX (and RX when `din` >= 0) and enable the channel. Raises, so it -// needs an nlr handler above it. Returns how many bytes the DMA holds when -// full -- the block-to-wire latency floor. +// Open TX (and RX when `din` >= 0) and enable the channel, for the OUTPUT. +// Raises, so it needs an nlr handler above it. Returns how many bytes the DMA +// holds when full -- the block-to-wire latency floor. +// +// If the capture reader already holds an open channel, the output joins it +// instead, provided it is the same port, pins and rate; anything else raises, +// because the only way to give the output what it asked for would be to +// reopen the channel under a running recording. uint32_t audiopump_i2s_open(const audiopump_i2s_cfg_t *cfg); -// Close it. Safe to call when nothing is open. +// The output lets go. The channel closes unless the capture reader still +// holds it, in which case TX goes on clocking zeros and RX keeps recording. +// Safe to call when nothing is open. void audiopump_i2s_shutdown(void); // Silence on the wire since the count was armed: audio that should have