From 7dc7502aedf8595aaac3f0eca5eb0e13cad38b76 Mon Sep 17 00:00:00 2001 From: Mikey Sklar Date: Wed, 14 Oct 2026 13:02:25 -0700 Subject: [PATCH] picodvi: AudioOut plays 32, 44.1 and 48 kHz play() takes the rate from the sample. The ACR and audio InfoFrame packets are built once per rate when audio starts, and the refill picks the set for the current rate, so DMA never reads a packet being rewritten. Channel status carries the matching IEC 60958 rate code. At 44.1 kHz the search for an exact CTS finds nothing near 20 ACR packets a frame, so it uses the usual N of 6272 and rounds CTS. Co-Authored-By: Claude Opus 5.5 (1M context) --- ports/raspberrypi/bindings/picodvi/AudioOut.c | 4 +- .../raspberrypi/common-hal/picodvi/AudioOut.c | 122 ++++++++++++------ .../common-hal/picodvi/dvi_audio_packet.c | 7 +- .../common-hal/picodvi/dvi_audio_packet.h | 3 +- .../picodvi/audioout_bad_values.py | 4 +- .../picodvi/audioout_rates.py | 43 ++++++ 6 files changed, 138 insertions(+), 45 deletions(-) create mode 100644 tests/circuitpython-manual/picodvi/audioout_rates.py diff --git a/ports/raspberrypi/bindings/picodvi/AudioOut.c b/ports/raspberrypi/bindings/picodvi/AudioOut.c index 70014ac9472..f5b61ace1b7 100644 --- a/ports/raspberrypi/bindings/picodvi/AudioOut.c +++ b/ports/raspberrypi/bindings/picodvi/AudioOut.c @@ -116,7 +116,9 @@ static void check_for_deinit(picodvi_audioout_obj_t *self) { //| Does not block. Use `playing` to block. //| //| Sample must be an `audiocore.WaveFile`, `audiocore.RawSample`, `audiomixer.Mixer`, -//| `synthio.Synthesizer` or `audiomp3.MP3Decoder` with a sample rate of 48000 Hz. +//| `synthio.Synthesizer` or `audiomp3.MP3Decoder` with a sample rate of 32000, 44100 +//| or 48000 Hz. Other rates raise `ValueError`; use `audiospeed.Resampler` to convert +//| them. Changing rate between samples can make the display mute briefly. //| //| Mono samples will be converted to stereo by copying value to both the left channel and the right channel. //| diff --git a/ports/raspberrypi/common-hal/picodvi/AudioOut.c b/ports/raspberrypi/common-hal/picodvi/AudioOut.c index ab5186ef4fa..340a22e045d 100644 --- a/ports/raspberrypi/common-hal/picodvi/AudioOut.c +++ b/ports/raspberrypi/common-hal/picodvi/AudioOut.c @@ -4,7 +4,7 @@ // // SPDX-License-Identifier: MIT -// 48 kHz stereo DVI audio for the RP2350 picodvi Framebuffer. +// 32, 44.1 or 48 kHz stereo DVI audio for the RP2350 picodvi Framebuffer. // No interrupt-time allocation, sample conversion, packet encoding, or Python callbacks. #include @@ -24,14 +24,28 @@ #include "supervisor/background_callback.h" #include "supervisor/port.h" -#define DVI_AUDIO_RATE 48000u +typedef struct { + uint32_t sample_rate; + // The usual ACR N, for when no N gives an exact CTS. + uint32_t usual_n; + // IEC 60958 channel status byte 3. + uint8_t channel_status; +} dvi_audio_rate_t; + +// The basic audio rates every DVI sink takes. +static const dvi_audio_rate_t dvi_audio_rates[] = { + { 32000, 4096, 0x03 }, + { 44100, 6272, 0x00 }, + { 48000, 6144, 0x02 }, +}; +#define DVI_AUDIO_RATE_COUNT MP_ARRAY_SIZE(dvi_audio_rates) #define DVI_AUDIO_H_TOTAL (MODE_640_H_FRONT_PORCH + MODE_640_H_SYNC_WIDTH + MODE_640_H_BACK_PORCH + MODE_640_H_ACTIVE_PIXELS) // Lines per frame that carry a data island. At the usual 25 MHz pixel clock -// a 625 line frame needs 240 sample packets, 20 ACR packets and 2 +// and 48 kHz a 625 line frame needs 240 sample packets, 20 ACR packets and 2 // InfoFrames. #define DVI_AUDIO_SLOTS 280 // Buffers for encoded sample packets. Other packets never change, so their -// islands point at fixed copies. 160 hold about 13 ms of audio. +// islands point at fixed copies. 160 hold about 13 ms of 48 kHz audio. #define DVI_AUDIO_POOL 160 // Packet words of an island, without its two guard band words at each end. #define DVI_AUDIO_SLOT_WORDS (HSTX_DATA_ISLAND_WORDS - 4) @@ -82,8 +96,12 @@ typedef struct dvi_audio_state { size_t pool_tail; size_t pool_count; uint32_t pixel_clock; - // ACR packets per second, 128 * 48000 / N. - uint32_t acr_rate; + // The rate being sent, and the rate play() asked for, which takes over + // from the next slot written. Indexes into dvi_audio_rates. + size_t rate_index; + size_t next_rate_index; + // ACR packets per second at each rate, 128 * fs / N. + uint32_t acr_rate[DVI_AUDIO_RATE_COUNT]; // Sample and ACR timing, carried from slot to slot. uint64_t sample_phase; uint64_t acr_phase; @@ -100,10 +118,10 @@ typedef struct dvi_audio_state { uint16_t slot_line[DVI_AUDIO_SLOTS]; uint8_t slot_samples[DVI_AUDIO_SLOTS]; // Packets that never change, for blanking [0] and vsync [1] lines. - dvi_audio_island_t acr[2]; + dvi_audio_island_t acr[DVI_AUDIO_RATE_COUNT][2]; dvi_audio_island_t null_packet[2]; dvi_audio_island_t silence[2]; - dvi_audio_island_t audio_infoframe; + dvi_audio_island_t audio_infoframe[DVI_AUDIO_RATE_COUNT]; dvi_audio_island_t avi_infoframe; uint32_t prefix[2][DVI_AUDIO_PREFIX_WORDS]; uint32_t suffix_blank[2][DVI_AUDIO_SUFFIX_BLANK_WORDS]; @@ -215,19 +233,17 @@ static uint32_t *dvi_audio_slot_island(dvi_audio_state_t *audio, size_t slot) { return &audio->commands[(audio->slot_entry[slot] + 1) * audio->command_size - 1]; } -static void dvi_audio_init_packets(dvi_audio_state_t *audio) { - hstx_packet_t packet; - // Pick N so that every frame has the same whole number of ACR packets, - // and CTS is exact. Then a frame of silence repeats exactly and needs no - // rewriting. N may be anything from 128 * fs / 1500 to 128 * fs / 300. - // If no N fits, use the usual 6144 and round CTS. +// Pick N so that every frame has the same whole number of ACR packets, and +// CTS is exact. Then a frame of silence repeats exactly and needs no +// rewriting. N may be anything from 128 * fs / 1500 to 128 * fs / 300. +// If no N fits, use the usual N for the rate and round CTS. +static uint32_t dvi_audio_find_n(uint32_t pixel_clock, const dvi_audio_rate_t *rate_info) { const uint64_t frame_pixels = (uint64_t)MODE_640_V_TOTAL_LINES * DVI_AUDIO_H_TOTAL; - const uint32_t n_times_rate = 128 * DVI_AUDIO_RATE; - uint32_t n = 6144; - uint32_t per_frame = (1000 * frame_pixels + audio->pixel_clock / 2) / audio->pixel_clock; + const uint32_t n_times_rate = 128 * rate_info->sample_rate; + uint32_t per_frame = (1000 * frame_pixels + pixel_clock / 2) / pixel_clock; for (uint32_t k = per_frame - 1; k <= per_frame + 1; k++) { // ACR packets per second when each frame has k. - uint64_t rate = (uint64_t)k * audio->pixel_clock; + uint64_t rate = (uint64_t)k * pixel_clock; if (k == 0 || rate % frame_pixels != 0) { continue; } @@ -237,30 +253,43 @@ static void dvi_audio_init_packets(dvi_audio_state_t *audio) { } uint32_t candidate = n_times_rate / rate; if (candidate >= n_times_rate / 1500 && candidate <= n_times_rate / 300 && - ((uint64_t)audio->pixel_clock * candidate) % n_times_rate == 0) { - n = candidate; - break; + ((uint64_t)pixel_clock * candidate) % n_times_rate == 0) { + return candidate; } } - audio->acr_rate = n_times_rate / n; - uint32_t cts = ((uint64_t)audio->pixel_clock * n + n_times_rate / 2) / n_times_rate; - hstx_packet_set_acr(&packet, n, cts); - for (int vsync = 0; vsync < 2; vsync++) { - dvi_audio_encode(audio->acr[vsync], &packet, vsync); + return rate_info->usual_n; +} + +static void dvi_audio_init_packets(dvi_audio_state_t *audio) { + hstx_packet_t packet; + for (size_t rate_index = 0; rate_index < DVI_AUDIO_RATE_COUNT; rate_index++) { + const dvi_audio_rate_t *rate_info = &dvi_audio_rates[rate_index]; + const uint32_t n_times_rate = 128 * rate_info->sample_rate; + uint32_t n = dvi_audio_find_n(audio->pixel_clock, rate_info); + audio->acr_rate[rate_index] = n_times_rate / n; + uint32_t cts = ((uint64_t)audio->pixel_clock * n + n_times_rate / 2) / n_times_rate; + hstx_packet_set_acr(&packet, n, cts); + for (int vsync = 0; vsync < 2; vsync++) { + dvi_audio_encode(audio->acr[rate_index][vsync], &packet, vsync); + } + // The first two slots of every frame carry the InfoFrames. + hstx_packet_set_audio_infoframe(&packet, rate_info->sample_rate, 2, 16); + dvi_audio_encode(audio->audio_infoframe[rate_index], &packet, dvi_audio_slot_vsync(audio, 0)); } + // 48 kHz until play() asks for another rate. + audio->rate_index = DVI_AUDIO_RATE_COUNT - 1; + audio->next_rate_index = audio->rate_index; hstx_packet_init(&packet); for (int vsync = 0; vsync < 2; vsync++) { dvi_audio_encode(audio->null_packet[vsync], &packet, vsync); } - // Frame position 4 never sets the block start flag. + // Frame position 4 never sets the block start flag, and frames 4 to 7 + // carry no channel status bits, so silence is the same at every rate. audio_sample_t silence[4] = {{0}}; - hstx_packet_set_audio_samples(&packet, silence, 4, 4); + hstx_packet_set_audio_samples(&packet, silence, 4, 4, dvi_audio_rates[DVI_AUDIO_RATE_COUNT - 1].channel_status); for (int vsync = 0; vsync < 2; vsync++) { dvi_audio_encode(audio->silence[vsync], &packet, vsync); } - // The first two slots of every frame carry the InfoFrames. - hstx_packet_set_audio_infoframe(&packet, DVI_AUDIO_RATE, 2, 16); - dvi_audio_encode(audio->audio_infoframe, &packet, dvi_audio_slot_vsync(audio, 0)); // Unspecified VIC: preserve CircuitPython's existing 625-line timing. hstx_packet_set_avi_infoframe(&packet, 0, 0); dvi_audio_encode(audio->avi_infoframe, &packet, dvi_audio_slot_vsync(audio, 1)); @@ -404,8 +433,8 @@ static const uint32_t *dvi_audio_next_packet(dvi_audio_state_t *audio, const int size_t previous = slot == 0 ? DVI_AUDIO_SLOTS - 1 : slot - 1; size_t lines = (audio->slot_line[slot] + MODE_640_V_TOTAL_LINES - audio->slot_line[previous]) % MODE_640_V_TOTAL_LINES; const uint64_t pixel_clock = audio->pixel_clock; - uint64_t sample_phase = audio->sample_phase + (uint64_t)lines * DVI_AUDIO_RATE * DVI_AUDIO_H_TOTAL; - uint64_t acr_phase = audio->acr_phase + (uint64_t)lines * audio->acr_rate * DVI_AUDIO_H_TOTAL; + uint64_t sample_phase = audio->sample_phase + (uint64_t)lines * dvi_audio_rates[audio->rate_index].sample_rate * DVI_AUDIO_H_TOTAL; + uint64_t acr_phase = audio->acr_phase + (uint64_t)lines * audio->acr_rate[audio->rate_index] * DVI_AUDIO_H_TOTAL; // Never let sample timing build up a long burst. if (sample_phase > pixel_clock * 12) { sample_phase = pixel_clock * 12; @@ -414,12 +443,12 @@ static const uint32_t *dvi_audio_next_packet(dvi_audio_state_t *audio, const int const uint32_t *island; *count = 0; if (slot == 0) { - island = audio->audio_infoframe; + island = audio->audio_infoframe[audio->rate_index]; } else if (slot == 1) { island = audio->avi_infoframe; } else if (acr_phase >= pixel_clock) { acr_phase -= pixel_clock; - island = audio->acr[vsync]; + island = audio->acr[audio->rate_index][vsync]; } else { // Sample packets carry four frames, fewer only at the end of a // sample. @@ -438,7 +467,8 @@ static const uint32_t *dvi_audio_next_packet(dvi_audio_state_t *audio, const int packet_samples[i].right = samples[2 * i + 1]; } hstx_packet_t packet; - audio->channel_frame = hstx_packet_set_audio_samples(&packet, packet_samples, n, audio->channel_frame); + audio->channel_frame = hstx_packet_set_audio_samples(&packet, packet_samples, n, audio->channel_frame, + dvi_audio_rates[audio->rate_index].channel_status); uint32_t *buffer = audio->pool[audio->pool_head]; dvi_audio_encode(buffer, &packet, vsync); audio->pool_position[audio->pool_head] = audio->write_position; @@ -466,7 +496,9 @@ static const uint32_t *dvi_audio_next_packet(dvi_audio_state_t *audio, const int // // With nothing to play, every frame comes out the same once the timing at // slot 0 repeats. Then the slots already hold the right packets, and this -// stops writing until there is something to play. +// stops writing until there is something to play. At 44.1 kHz a frame holds +// 882 samples, not a whole number of packets, so frames never repeat and +// silence keeps being written. static bool dvi_audio_fill_slot(picodvi_framebuffer_obj_t *self, dvi_audio_state_t *audio, const int16_t *samples, size_t available, bool wait, size_t *used) { *used = 0; bool idle = available == 0 && !wait; @@ -489,6 +521,7 @@ static bool dvi_audio_fill_slot(picodvi_framebuffer_obj_t *self, dvi_audio_state audio->idle_steady = false; audio->idle_run = 0; } + audio->rate_index = audio->next_rate_index; // Free the pool buffers DMA has finished with. while (audio->pool_count > 0 && audio->pool_position[audio->pool_tail] < read) { audio->pool_tail = (audio->pool_tail + 1) % DVI_AUDIO_POOL; @@ -692,6 +725,12 @@ static void picodvi_framebuffer_audio_stop(picodvi_framebuffer_obj_t *self) { memset(audio->slot_samples, 0, sizeof(audio->slot_samples)); } +// Sends the rate at rate_index in dvi_audio_rates from the next slot written. +// The packets already written ahead of DMA keep the old rate. +static void picodvi_framebuffer_audio_set_rate(picodvi_framebuffer_obj_t *self, size_t rate_index) { + self->dvi_audio->next_rate_index = rate_index; +} + // Convert up to space frames of the current sample buffer into the stage. static void audioout_convert(picodvi_audioout_obj_t *self, size_t space) { audiosample_base_t *base = audiosample_cast_obj(self->sample); @@ -885,12 +924,19 @@ void common_hal_picodvi_audioout_deinit(picodvi_audioout_obj_t *self) { void common_hal_picodvi_audioout_play(picodvi_audioout_obj_t *self, mp_obj_t sample, bool loop) { audiosample_base_t *base = audiosample_check(sample); - mp_arg_validate_int(base->sample_rate, 48000, MP_QSTR_sample_rate); + size_t rate_index = 0; + while (rate_index < DVI_AUDIO_RATE_COUNT && dvi_audio_rates[rate_index].sample_rate != base->sample_rate) { + rate_index++; + } + if (rate_index == DVI_AUDIO_RATE_COUNT) { + mp_raise_ValueError_varg(MP_ERROR_TEXT("Invalid %q"), MP_QSTR_sample_rate); + } mp_arg_validate_int_range(base->channel_count, 1, 2, MP_QSTR_channel_count); if (base->bits_per_sample != 8 && base->bits_per_sample != 16) { mp_raise_ValueError_varg(MP_ERROR_TEXT("Invalid %q"), MP_QSTR_bits_per_sample); } common_hal_picodvi_audioout_stop(self); + picodvi_framebuffer_audio_set_rate(self->framebuffer, rate_index); audiosample_reset_buffer(sample, false, 0); self->bytes_per_frame = base->channel_count * base->bits_per_sample / 8; self->source_frames = 0; diff --git a/ports/raspberrypi/common-hal/picodvi/dvi_audio_packet.c b/ports/raspberrypi/common-hal/picodvi/dvi_audio_packet.c index 69982c56b42..ff9f75669f5 100644 --- a/ports/raspberrypi/common-hal/picodvi/dvi_audio_packet.c +++ b/ports/raspberrypi/common-hal/picodvi/dvi_audio_packet.c @@ -184,7 +184,7 @@ void hstx_packet_set_avi_infoframe(hstx_packet_t *packet, uint8_t vic, uint8_t p } int hstx_packet_set_audio_samples(hstx_packet_t *packet, const audio_sample_t *samples, int num_samples, - int frame_count) { + int frame_count, uint8_t channel_status) { hstx_packet_init(packet); if (num_samples < 1) { num_samples = 1; @@ -221,9 +221,10 @@ int hstx_packet_set_audio_samples(hstx_packet_t *packet, const audio_sample_t *s d[4] = right & 0xFF; d[5] = (right >> 8) & 0xFF; - // IEC 60958 byte 0 = 0x04 (copy permitted), byte 3 = 0x02 (48 kHz). + // IEC 60958 byte 0 = 0x04 (copy permitted), byte 3 = channel_status + // (the sample rate). int frame = (frame_count + i) % 192; - uint8_t status = frame / 8 == 0 ? 0x04 : (frame / 8 == 3 ? 0x02 : 0); + uint8_t status = frame / 8 == 0 ? 0x04 : (frame / 8 == 3 ? channel_status : 0); uint8_t c = (status >> (frame % 8)) & 1; bool p_left = compute_parity3(d[1], d[2], c); bool p_right = compute_parity3(d[4], d[5], c); diff --git a/ports/raspberrypi/common-hal/picodvi/dvi_audio_packet.h b/ports/raspberrypi/common-hal/picodvi/dvi_audio_packet.h index 21427b1ff19..30828e49437 100644 --- a/ports/raspberrypi/common-hal/picodvi/dvi_audio_packet.h +++ b/ports/raspberrypi/common-hal/picodvi/dvi_audio_packet.h @@ -34,5 +34,6 @@ void hstx_packet_init(hstx_packet_t *packet); void hstx_packet_set_acr(hstx_packet_t *packet, uint32_t n, uint32_t cts); void hstx_packet_set_audio_infoframe(hstx_packet_t *packet, uint32_t sample_rate, uint8_t channels, uint8_t bits_per_sample); void hstx_packet_set_avi_infoframe(hstx_packet_t *packet, uint8_t vic, uint8_t pixel_repetition); -int hstx_packet_set_audio_samples(hstx_packet_t *packet, const audio_sample_t *samples, int num_samples, int frame_count); +int hstx_packet_set_audio_samples(hstx_packet_t *packet, const audio_sample_t *samples, int num_samples, int frame_count, + uint8_t channel_status); void hstx_encode_data_island(hstx_data_island_t *out, const hstx_packet_t *packet, bool vsync_active, bool hsync_active); diff --git a/tests/circuitpython-manual/picodvi/audioout_bad_values.py b/tests/circuitpython-manual/picodvi/audioout_bad_values.py index e6b946e7aaf..a4bad1d6468 100644 --- a/tests/circuitpython-manual/picodvi/audioout_bad_values.py +++ b/tests/circuitpython-manual/picodvi/audioout_bad_values.py @@ -39,8 +39,8 @@ def check(name, fun): fb = picodvi.Framebuffer(320, 240, color_depth=8, **PINS) audio = picodvi.AudioOut(fb) silence = array.array("h", [0] * 48) -check("44.1 kHz sample", lambda: audio.play(audiocore.RawSample(silence, sample_rate=44100))) -check("32 kHz sample", lambda: audio.play(audiocore.RawSample(silence, sample_rate=32000))) +check("22.05 kHz sample", lambda: audio.play(audiocore.RawSample(silence, sample_rate=22050))) +check("96 kHz sample", lambda: audio.play(audiocore.RawSample(silence, sample_rate=96000))) check("second AudioOut", lambda: picodvi.AudioOut(fb)) check("pause when stopped", audio.pause) diff --git a/tests/circuitpython-manual/picodvi/audioout_rates.py b/tests/circuitpython-manual/picodvi/audioout_rates.py new file mode 100644 index 00000000000..d2aa3025311 --- /dev/null +++ b/tests/circuitpython-manual/picodvi/audioout_rates.py @@ -0,0 +1,43 @@ +# SPDX-FileCopyrightText: 2026 Adafruit Industries +# +# SPDX-License-Identifier: MIT +# Plays a 1 kHz tone over DVI at each supported sample rate, twice round. +# Listen on a display with speakers: the pitch should not change. Pin names +# are for the Fruit Jam, Metro RP2350 and Feather RP2350. +import array +import math +import time + +import audiocore +import board +import displayio +import picodvi + +PINS = dict( + clk_dp=board.CKP, + clk_dn=board.CKN, + red_dp=board.D0P, + red_dn=board.D0N, + green_dp=board.D1P, + green_dn=board.D1N, + blue_dp=board.D2P, + blue_dn=board.D2N, +) + +displayio.release_displays() +fb = picodvi.Framebuffer(320, 240, color_depth=8, **PINS) +audio = picodvi.AudioOut(fb) +time.sleep(3) # Most displays take a few seconds to lock on to a new signal. + +for rate in (32000, 44100, 48000) * 2: + length = rate // 100 # Ten periods of 1 kHz. + tone = audiocore.RawSample( + array.array("h", [int(math.sin(math.pi * 20 * i / length) * 8000) for i in range(length)]), + sample_rate=rate, + ) + audio.play(tone, loop=True) + time.sleep(2) + print(rate, "playing", audio.playing) + audio.stop() + time.sleep(0.5) +print("done")