Skip to content

Commit 7dc7502

Browse files
mikeysklarclaude
andcommitted
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) <noreply@anthropic.com>
1 parent 509bb82 commit 7dc7502

6 files changed

Lines changed: 138 additions & 45 deletions

File tree

‎ports/raspberrypi/bindings/picodvi/AudioOut.c‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,9 @@ static void check_for_deinit(picodvi_audioout_obj_t *self) {
116116
//| Does not block. Use `playing` to block.
117117
//|
118118
//| Sample must be an `audiocore.WaveFile`, `audiocore.RawSample`, `audiomixer.Mixer`,
119-
//| `synthio.Synthesizer` or `audiomp3.MP3Decoder` with a sample rate of 48000 Hz.
119+
//| `synthio.Synthesizer` or `audiomp3.MP3Decoder` with a sample rate of 32000, 44100
120+
//| or 48000 Hz. Other rates raise `ValueError`; use `audiospeed.Resampler` to convert
121+
//| them. Changing rate between samples can make the display mute briefly.
120122
//|
121123
//| Mono samples will be converted to stereo by copying value to both the left channel and the right channel.
122124
//|

‎ports/raspberrypi/common-hal/picodvi/AudioOut.c‎

Lines changed: 84 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
//
55
// SPDX-License-Identifier: MIT
66

7-
// 48 kHz stereo DVI audio for the RP2350 picodvi Framebuffer.
7+
// 32, 44.1 or 48 kHz stereo DVI audio for the RP2350 picodvi Framebuffer.
88
// No interrupt-time allocation, sample conversion, packet encoding, or Python callbacks.
99

1010
#include <string.h>
@@ -24,14 +24,28 @@
2424
#include "supervisor/background_callback.h"
2525
#include "supervisor/port.h"
2626

27-
#define DVI_AUDIO_RATE 48000u
27+
typedef struct {
28+
uint32_t sample_rate;
29+
// The usual ACR N, for when no N gives an exact CTS.
30+
uint32_t usual_n;
31+
// IEC 60958 channel status byte 3.
32+
uint8_t channel_status;
33+
} dvi_audio_rate_t;
34+
35+
// The basic audio rates every DVI sink takes.
36+
static const dvi_audio_rate_t dvi_audio_rates[] = {
37+
{ 32000, 4096, 0x03 },
38+
{ 44100, 6272, 0x00 },
39+
{ 48000, 6144, 0x02 },
40+
};
41+
#define DVI_AUDIO_RATE_COUNT MP_ARRAY_SIZE(dvi_audio_rates)
2842
#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)
2943
// Lines per frame that carry a data island. At the usual 25 MHz pixel clock
30-
// a 625 line frame needs 240 sample packets, 20 ACR packets and 2
44+
// and 48 kHz a 625 line frame needs 240 sample packets, 20 ACR packets and 2
3145
// InfoFrames.
3246
#define DVI_AUDIO_SLOTS 280
3347
// Buffers for encoded sample packets. Other packets never change, so their
34-
// islands point at fixed copies. 160 hold about 13 ms of audio.
48+
// islands point at fixed copies. 160 hold about 13 ms of 48 kHz audio.
3549
#define DVI_AUDIO_POOL 160
3650
// Packet words of an island, without its two guard band words at each end.
3751
#define DVI_AUDIO_SLOT_WORDS (HSTX_DATA_ISLAND_WORDS - 4)
@@ -82,8 +96,12 @@ typedef struct dvi_audio_state {
8296
size_t pool_tail;
8397
size_t pool_count;
8498
uint32_t pixel_clock;
85-
// ACR packets per second, 128 * 48000 / N.
86-
uint32_t acr_rate;
99+
// The rate being sent, and the rate play() asked for, which takes over
100+
// from the next slot written. Indexes into dvi_audio_rates.
101+
size_t rate_index;
102+
size_t next_rate_index;
103+
// ACR packets per second at each rate, 128 * fs / N.
104+
uint32_t acr_rate[DVI_AUDIO_RATE_COUNT];
87105
// Sample and ACR timing, carried from slot to slot.
88106
uint64_t sample_phase;
89107
uint64_t acr_phase;
@@ -100,10 +118,10 @@ typedef struct dvi_audio_state {
100118
uint16_t slot_line[DVI_AUDIO_SLOTS];
101119
uint8_t slot_samples[DVI_AUDIO_SLOTS];
102120
// Packets that never change, for blanking [0] and vsync [1] lines.
103-
dvi_audio_island_t acr[2];
121+
dvi_audio_island_t acr[DVI_AUDIO_RATE_COUNT][2];
104122
dvi_audio_island_t null_packet[2];
105123
dvi_audio_island_t silence[2];
106-
dvi_audio_island_t audio_infoframe;
124+
dvi_audio_island_t audio_infoframe[DVI_AUDIO_RATE_COUNT];
107125
dvi_audio_island_t avi_infoframe;
108126
uint32_t prefix[2][DVI_AUDIO_PREFIX_WORDS];
109127
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) {
215233
return &audio->commands[(audio->slot_entry[slot] + 1) * audio->command_size - 1];
216234
}
217235

218-
static void dvi_audio_init_packets(dvi_audio_state_t *audio) {
219-
hstx_packet_t packet;
220-
// Pick N so that every frame has the same whole number of ACR packets,
221-
// and CTS is exact. Then a frame of silence repeats exactly and needs no
222-
// rewriting. N may be anything from 128 * fs / 1500 to 128 * fs / 300.
223-
// If no N fits, use the usual 6144 and round CTS.
236+
// Pick N so that every frame has the same whole number of ACR packets, and
237+
// CTS is exact. Then a frame of silence repeats exactly and needs no
238+
// rewriting. N may be anything from 128 * fs / 1500 to 128 * fs / 300.
239+
// If no N fits, use the usual N for the rate and round CTS.
240+
static uint32_t dvi_audio_find_n(uint32_t pixel_clock, const dvi_audio_rate_t *rate_info) {
224241
const uint64_t frame_pixels = (uint64_t)MODE_640_V_TOTAL_LINES * DVI_AUDIO_H_TOTAL;
225-
const uint32_t n_times_rate = 128 * DVI_AUDIO_RATE;
226-
uint32_t n = 6144;
227-
uint32_t per_frame = (1000 * frame_pixels + audio->pixel_clock / 2) / audio->pixel_clock;
242+
const uint32_t n_times_rate = 128 * rate_info->sample_rate;
243+
uint32_t per_frame = (1000 * frame_pixels + pixel_clock / 2) / pixel_clock;
228244
for (uint32_t k = per_frame - 1; k <= per_frame + 1; k++) {
229245
// ACR packets per second when each frame has k.
230-
uint64_t rate = (uint64_t)k * audio->pixel_clock;
246+
uint64_t rate = (uint64_t)k * pixel_clock;
231247
if (k == 0 || rate % frame_pixels != 0) {
232248
continue;
233249
}
@@ -237,30 +253,43 @@ static void dvi_audio_init_packets(dvi_audio_state_t *audio) {
237253
}
238254
uint32_t candidate = n_times_rate / rate;
239255
if (candidate >= n_times_rate / 1500 && candidate <= n_times_rate / 300 &&
240-
((uint64_t)audio->pixel_clock * candidate) % n_times_rate == 0) {
241-
n = candidate;
242-
break;
256+
((uint64_t)pixel_clock * candidate) % n_times_rate == 0) {
257+
return candidate;
243258
}
244259
}
245-
audio->acr_rate = n_times_rate / n;
246-
uint32_t cts = ((uint64_t)audio->pixel_clock * n + n_times_rate / 2) / n_times_rate;
247-
hstx_packet_set_acr(&packet, n, cts);
248-
for (int vsync = 0; vsync < 2; vsync++) {
249-
dvi_audio_encode(audio->acr[vsync], &packet, vsync);
260+
return rate_info->usual_n;
261+
}
262+
263+
static void dvi_audio_init_packets(dvi_audio_state_t *audio) {
264+
hstx_packet_t packet;
265+
for (size_t rate_index = 0; rate_index < DVI_AUDIO_RATE_COUNT; rate_index++) {
266+
const dvi_audio_rate_t *rate_info = &dvi_audio_rates[rate_index];
267+
const uint32_t n_times_rate = 128 * rate_info->sample_rate;
268+
uint32_t n = dvi_audio_find_n(audio->pixel_clock, rate_info);
269+
audio->acr_rate[rate_index] = n_times_rate / n;
270+
uint32_t cts = ((uint64_t)audio->pixel_clock * n + n_times_rate / 2) / n_times_rate;
271+
hstx_packet_set_acr(&packet, n, cts);
272+
for (int vsync = 0; vsync < 2; vsync++) {
273+
dvi_audio_encode(audio->acr[rate_index][vsync], &packet, vsync);
274+
}
275+
// The first two slots of every frame carry the InfoFrames.
276+
hstx_packet_set_audio_infoframe(&packet, rate_info->sample_rate, 2, 16);
277+
dvi_audio_encode(audio->audio_infoframe[rate_index], &packet, dvi_audio_slot_vsync(audio, 0));
250278
}
279+
// 48 kHz until play() asks for another rate.
280+
audio->rate_index = DVI_AUDIO_RATE_COUNT - 1;
281+
audio->next_rate_index = audio->rate_index;
251282
hstx_packet_init(&packet);
252283
for (int vsync = 0; vsync < 2; vsync++) {
253284
dvi_audio_encode(audio->null_packet[vsync], &packet, vsync);
254285
}
255-
// Frame position 4 never sets the block start flag.
286+
// Frame position 4 never sets the block start flag, and frames 4 to 7
287+
// carry no channel status bits, so silence is the same at every rate.
256288
audio_sample_t silence[4] = {{0}};
257-
hstx_packet_set_audio_samples(&packet, silence, 4, 4);
289+
hstx_packet_set_audio_samples(&packet, silence, 4, 4, dvi_audio_rates[DVI_AUDIO_RATE_COUNT - 1].channel_status);
258290
for (int vsync = 0; vsync < 2; vsync++) {
259291
dvi_audio_encode(audio->silence[vsync], &packet, vsync);
260292
}
261-
// The first two slots of every frame carry the InfoFrames.
262-
hstx_packet_set_audio_infoframe(&packet, DVI_AUDIO_RATE, 2, 16);
263-
dvi_audio_encode(audio->audio_infoframe, &packet, dvi_audio_slot_vsync(audio, 0));
264293
// Unspecified VIC: preserve CircuitPython's existing 625-line timing.
265294
hstx_packet_set_avi_infoframe(&packet, 0, 0);
266295
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
404433
size_t previous = slot == 0 ? DVI_AUDIO_SLOTS - 1 : slot - 1;
405434
size_t lines = (audio->slot_line[slot] + MODE_640_V_TOTAL_LINES - audio->slot_line[previous]) % MODE_640_V_TOTAL_LINES;
406435
const uint64_t pixel_clock = audio->pixel_clock;
407-
uint64_t sample_phase = audio->sample_phase + (uint64_t)lines * DVI_AUDIO_RATE * DVI_AUDIO_H_TOTAL;
408-
uint64_t acr_phase = audio->acr_phase + (uint64_t)lines * audio->acr_rate * DVI_AUDIO_H_TOTAL;
436+
uint64_t sample_phase = audio->sample_phase + (uint64_t)lines * dvi_audio_rates[audio->rate_index].sample_rate * DVI_AUDIO_H_TOTAL;
437+
uint64_t acr_phase = audio->acr_phase + (uint64_t)lines * audio->acr_rate[audio->rate_index] * DVI_AUDIO_H_TOTAL;
409438
// Never let sample timing build up a long burst.
410439
if (sample_phase > pixel_clock * 12) {
411440
sample_phase = pixel_clock * 12;
@@ -414,12 +443,12 @@ static const uint32_t *dvi_audio_next_packet(dvi_audio_state_t *audio, const int
414443
const uint32_t *island;
415444
*count = 0;
416445
if (slot == 0) {
417-
island = audio->audio_infoframe;
446+
island = audio->audio_infoframe[audio->rate_index];
418447
} else if (slot == 1) {
419448
island = audio->avi_infoframe;
420449
} else if (acr_phase >= pixel_clock) {
421450
acr_phase -= pixel_clock;
422-
island = audio->acr[vsync];
451+
island = audio->acr[audio->rate_index][vsync];
423452
} else {
424453
// Sample packets carry four frames, fewer only at the end of a
425454
// sample.
@@ -438,7 +467,8 @@ static const uint32_t *dvi_audio_next_packet(dvi_audio_state_t *audio, const int
438467
packet_samples[i].right = samples[2 * i + 1];
439468
}
440469
hstx_packet_t packet;
441-
audio->channel_frame = hstx_packet_set_audio_samples(&packet, packet_samples, n, audio->channel_frame);
470+
audio->channel_frame = hstx_packet_set_audio_samples(&packet, packet_samples, n, audio->channel_frame,
471+
dvi_audio_rates[audio->rate_index].channel_status);
442472
uint32_t *buffer = audio->pool[audio->pool_head];
443473
dvi_audio_encode(buffer, &packet, vsync);
444474
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
466496
//
467497
// With nothing to play, every frame comes out the same once the timing at
468498
// slot 0 repeats. Then the slots already hold the right packets, and this
469-
// stops writing until there is something to play.
499+
// stops writing until there is something to play. At 44.1 kHz a frame holds
500+
// 882 samples, not a whole number of packets, so frames never repeat and
501+
// silence keeps being written.
470502
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) {
471503
*used = 0;
472504
bool idle = available == 0 && !wait;
@@ -489,6 +521,7 @@ static bool dvi_audio_fill_slot(picodvi_framebuffer_obj_t *self, dvi_audio_state
489521
audio->idle_steady = false;
490522
audio->idle_run = 0;
491523
}
524+
audio->rate_index = audio->next_rate_index;
492525
// Free the pool buffers DMA has finished with.
493526
while (audio->pool_count > 0 && audio->pool_position[audio->pool_tail] < read) {
494527
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) {
692725
memset(audio->slot_samples, 0, sizeof(audio->slot_samples));
693726
}
694727

728+
// Sends the rate at rate_index in dvi_audio_rates from the next slot written.
729+
// The packets already written ahead of DMA keep the old rate.
730+
static void picodvi_framebuffer_audio_set_rate(picodvi_framebuffer_obj_t *self, size_t rate_index) {
731+
self->dvi_audio->next_rate_index = rate_index;
732+
}
733+
695734
// Convert up to space frames of the current sample buffer into the stage.
696735
static void audioout_convert(picodvi_audioout_obj_t *self, size_t space) {
697736
audiosample_base_t *base = audiosample_cast_obj(self->sample);
@@ -885,12 +924,19 @@ void common_hal_picodvi_audioout_deinit(picodvi_audioout_obj_t *self) {
885924

886925
void common_hal_picodvi_audioout_play(picodvi_audioout_obj_t *self, mp_obj_t sample, bool loop) {
887926
audiosample_base_t *base = audiosample_check(sample);
888-
mp_arg_validate_int(base->sample_rate, 48000, MP_QSTR_sample_rate);
927+
size_t rate_index = 0;
928+
while (rate_index < DVI_AUDIO_RATE_COUNT && dvi_audio_rates[rate_index].sample_rate != base->sample_rate) {
929+
rate_index++;
930+
}
931+
if (rate_index == DVI_AUDIO_RATE_COUNT) {
932+
mp_raise_ValueError_varg(MP_ERROR_TEXT("Invalid %q"), MP_QSTR_sample_rate);
933+
}
889934
mp_arg_validate_int_range(base->channel_count, 1, 2, MP_QSTR_channel_count);
890935
if (base->bits_per_sample != 8 && base->bits_per_sample != 16) {
891936
mp_raise_ValueError_varg(MP_ERROR_TEXT("Invalid %q"), MP_QSTR_bits_per_sample);
892937
}
893938
common_hal_picodvi_audioout_stop(self);
939+
picodvi_framebuffer_audio_set_rate(self->framebuffer, rate_index);
894940
audiosample_reset_buffer(sample, false, 0);
895941
self->bytes_per_frame = base->channel_count * base->bits_per_sample / 8;
896942
self->source_frames = 0;

‎ports/raspberrypi/common-hal/picodvi/dvi_audio_packet.c‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@ void hstx_packet_set_avi_infoframe(hstx_packet_t *packet, uint8_t vic, uint8_t p
184184
}
185185

186186
int hstx_packet_set_audio_samples(hstx_packet_t *packet, const audio_sample_t *samples, int num_samples,
187-
int frame_count) {
187+
int frame_count, uint8_t channel_status) {
188188
hstx_packet_init(packet);
189189
if (num_samples < 1) {
190190
num_samples = 1;
@@ -221,9 +221,10 @@ int hstx_packet_set_audio_samples(hstx_packet_t *packet, const audio_sample_t *s
221221
d[4] = right & 0xFF;
222222
d[5] = (right >> 8) & 0xFF;
223223

224-
// IEC 60958 byte 0 = 0x04 (copy permitted), byte 3 = 0x02 (48 kHz).
224+
// IEC 60958 byte 0 = 0x04 (copy permitted), byte 3 = channel_status
225+
// (the sample rate).
225226
int frame = (frame_count + i) % 192;
226-
uint8_t status = frame / 8 == 0 ? 0x04 : (frame / 8 == 3 ? 0x02 : 0);
227+
uint8_t status = frame / 8 == 0 ? 0x04 : (frame / 8 == 3 ? channel_status : 0);
227228
uint8_t c = (status >> (frame % 8)) & 1;
228229
bool p_left = compute_parity3(d[1], d[2], c);
229230
bool p_right = compute_parity3(d[4], d[5], c);

‎ports/raspberrypi/common-hal/picodvi/dvi_audio_packet.h‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,5 +34,6 @@ void hstx_packet_init(hstx_packet_t *packet);
3434
void hstx_packet_set_acr(hstx_packet_t *packet, uint32_t n, uint32_t cts);
3535
void hstx_packet_set_audio_infoframe(hstx_packet_t *packet, uint32_t sample_rate, uint8_t channels, uint8_t bits_per_sample);
3636
void hstx_packet_set_avi_infoframe(hstx_packet_t *packet, uint8_t vic, uint8_t pixel_repetition);
37-
int hstx_packet_set_audio_samples(hstx_packet_t *packet, const audio_sample_t *samples, int num_samples, int frame_count);
37+
int hstx_packet_set_audio_samples(hstx_packet_t *packet, const audio_sample_t *samples, int num_samples, int frame_count,
38+
uint8_t channel_status);
3839
void hstx_encode_data_island(hstx_data_island_t *out, const hstx_packet_t *packet, bool vsync_active, bool hsync_active);

‎tests/circuitpython-manual/picodvi/audioout_bad_values.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,8 @@ def check(name, fun):
3939
fb = picodvi.Framebuffer(320, 240, color_depth=8, **PINS)
4040
audio = picodvi.AudioOut(fb)
4141
silence = array.array("h", [0] * 48)
42-
check("44.1 kHz sample", lambda: audio.play(audiocore.RawSample(silence, sample_rate=44100)))
43-
check("32 kHz sample", lambda: audio.play(audiocore.RawSample(silence, sample_rate=32000)))
42+
check("22.05 kHz sample", lambda: audio.play(audiocore.RawSample(silence, sample_rate=22050)))
43+
check("96 kHz sample", lambda: audio.play(audiocore.RawSample(silence, sample_rate=96000)))
4444
check("second AudioOut", lambda: picodvi.AudioOut(fb))
4545
check("pause when stopped", audio.pause)
4646

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# SPDX-FileCopyrightText: 2026 Adafruit Industries
2+
#
3+
# SPDX-License-Identifier: MIT
4+
# Plays a 1 kHz tone over DVI at each supported sample rate, twice round.
5+
# Listen on a display with speakers: the pitch should not change. Pin names
6+
# are for the Fruit Jam, Metro RP2350 and Feather RP2350.
7+
import array
8+
import math
9+
import time
10+
11+
import audiocore
12+
import board
13+
import displayio
14+
import picodvi
15+
16+
PINS = dict(
17+
clk_dp=board.CKP,
18+
clk_dn=board.CKN,
19+
red_dp=board.D0P,
20+
red_dn=board.D0N,
21+
green_dp=board.D1P,
22+
green_dn=board.D1N,
23+
blue_dp=board.D2P,
24+
blue_dn=board.D2N,
25+
)
26+
27+
displayio.release_displays()
28+
fb = picodvi.Framebuffer(320, 240, color_depth=8, **PINS)
29+
audio = picodvi.AudioOut(fb)
30+
time.sleep(3) # Most displays take a few seconds to lock on to a new signal.
31+
32+
for rate in (32000, 44100, 48000) * 2:
33+
length = rate // 100 # Ten periods of 1 kHz.
34+
tone = audiocore.RawSample(
35+
array.array("h", [int(math.sin(math.pi * 20 * i / length) * 8000) for i in range(length)]),
36+
sample_rate=rate,
37+
)
38+
audio.play(tone, loop=True)
39+
time.sleep(2)
40+
print(rate, "playing", audio.playing)
41+
audio.stop()
42+
time.sleep(0.5)
43+
print("done")

0 commit comments

Comments
 (0)