Skip to content

Commit ab3283e

Browse files
authored
Merge pull request #11607 from mikeysklar/dvi-audio-rates
DVI audio can play 44.1 kHz and 32 kHz sound too
2 parents f18e855 + 86633ef commit ab3283e

6 files changed

Lines changed: 150 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: 82 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,26 @@
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+
} dvi_audio_rate_t;
32+
33+
// The basic audio rates every DVI sink takes.
34+
static const dvi_audio_rate_t dvi_audio_rates[] = {
35+
{ 32000, 4096 },
36+
{ 44100, 6272 },
37+
{ 48000, 6144 },
38+
};
39+
#define DVI_AUDIO_RATE_COUNT MP_ARRAY_SIZE(dvi_audio_rates)
2840
#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)
2941
// 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
42+
// and 48 kHz a 625 line frame needs 240 sample packets, 20 ACR packets and 2
3143
// InfoFrames.
3244
#define DVI_AUDIO_SLOTS 280
3345
// Buffers for encoded sample packets. Other packets never change, so their
34-
// islands point at fixed copies. 160 hold about 13 ms of audio.
46+
// islands point at fixed copies. 160 hold about 13 ms of 48 kHz audio.
3547
#define DVI_AUDIO_POOL 160
3648
// Packet words of an island, without its two guard band words at each end.
3749
#define DVI_AUDIO_SLOT_WORDS (HSTX_DATA_ISLAND_WORDS - 4)
@@ -82,8 +94,12 @@ typedef struct dvi_audio_state {
8294
size_t pool_tail;
8395
size_t pool_count;
8496
uint32_t pixel_clock;
85-
// ACR packets per second, 128 * 48000 / N.
86-
uint32_t acr_rate;
97+
// The rate being sent, and the rate play() asked for, which takes over
98+
// from the next slot written. Indexes into dvi_audio_rates.
99+
size_t rate_index;
100+
size_t next_rate_index;
101+
// ACR packets per second at each rate, 128 * fs / N.
102+
uint32_t acr_rate[DVI_AUDIO_RATE_COUNT];
87103
// Sample and ACR timing, carried from slot to slot.
88104
uint64_t sample_phase;
89105
uint64_t acr_phase;
@@ -100,10 +116,10 @@ typedef struct dvi_audio_state {
100116
uint16_t slot_line[DVI_AUDIO_SLOTS];
101117
uint8_t slot_samples[DVI_AUDIO_SLOTS];
102118
// Packets that never change, for blanking [0] and vsync [1] lines.
103-
dvi_audio_island_t acr[2];
119+
dvi_audio_island_t acr[DVI_AUDIO_RATE_COUNT][2];
104120
dvi_audio_island_t null_packet[2];
105121
dvi_audio_island_t silence[2];
106-
dvi_audio_island_t audio_infoframe;
122+
dvi_audio_island_t audio_infoframe[DVI_AUDIO_RATE_COUNT];
107123
dvi_audio_island_t avi_infoframe;
108124
uint32_t prefix[2][DVI_AUDIO_PREFIX_WORDS];
109125
uint32_t suffix_blank[2][DVI_AUDIO_SUFFIX_BLANK_WORDS];
@@ -215,19 +231,17 @@ static uint32_t *dvi_audio_slot_island(dvi_audio_state_t *audio, size_t slot) {
215231
return &audio->commands[(audio->slot_entry[slot] + 1) * audio->command_size - 1];
216232
}
217233

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.
234+
// Pick N so that every frame has the same whole number of ACR packets, and
235+
// CTS is exact. Then a frame of silence repeats exactly and needs no
236+
// rewriting. N may be anything from 128 * fs / 1500 to 128 * fs / 300.
237+
// If no N fits, use the usual N for the rate and round CTS.
238+
static uint32_t dvi_audio_find_n(uint32_t pixel_clock, const dvi_audio_rate_t *rate_info) {
224239
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;
240+
const uint32_t n_times_rate = 128 * rate_info->sample_rate;
241+
uint32_t per_frame = (1000 * frame_pixels + pixel_clock / 2) / pixel_clock;
228242
for (uint32_t k = per_frame - 1; k <= per_frame + 1; k++) {
229243
// ACR packets per second when each frame has k.
230-
uint64_t rate = (uint64_t)k * audio->pixel_clock;
244+
uint64_t rate = (uint64_t)k * pixel_clock;
231245
if (k == 0 || rate % frame_pixels != 0) {
232246
continue;
233247
}
@@ -237,30 +251,43 @@ static void dvi_audio_init_packets(dvi_audio_state_t *audio) {
237251
}
238252
uint32_t candidate = n_times_rate / rate;
239253
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;
254+
((uint64_t)pixel_clock * candidate) % n_times_rate == 0) {
255+
return candidate;
243256
}
244257
}
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);
258+
return rate_info->usual_n;
259+
}
260+
261+
static void dvi_audio_init_packets(dvi_audio_state_t *audio) {
262+
hstx_packet_t packet;
263+
for (size_t rate_index = 0; rate_index < DVI_AUDIO_RATE_COUNT; rate_index++) {
264+
const dvi_audio_rate_t *rate_info = &dvi_audio_rates[rate_index];
265+
const uint32_t n_times_rate = 128 * rate_info->sample_rate;
266+
uint32_t n = dvi_audio_find_n(audio->pixel_clock, rate_info);
267+
audio->acr_rate[rate_index] = n_times_rate / n;
268+
uint32_t cts = ((uint64_t)audio->pixel_clock * n + n_times_rate / 2) / n_times_rate;
269+
hstx_packet_set_acr(&packet, n, cts);
270+
for (int vsync = 0; vsync < 2; vsync++) {
271+
dvi_audio_encode(audio->acr[rate_index][vsync], &packet, vsync);
272+
}
273+
// The first two slots of every frame carry the InfoFrames.
274+
hstx_packet_set_audio_infoframe(&packet, rate_info->sample_rate, 2, 16);
275+
dvi_audio_encode(audio->audio_infoframe[rate_index], &packet, dvi_audio_slot_vsync(audio, 0));
250276
}
277+
// 48 kHz until play() asks for another rate.
278+
audio->rate_index = DVI_AUDIO_RATE_COUNT - 1;
279+
audio->next_rate_index = audio->rate_index;
251280
hstx_packet_init(&packet);
252281
for (int vsync = 0; vsync < 2; vsync++) {
253282
dvi_audio_encode(audio->null_packet[vsync], &packet, vsync);
254283
}
255-
// Frame position 4 never sets the block start flag.
284+
// Frame position 4 never sets the block start flag, and frames 4 to 7
285+
// carry no channel status bits, so silence is the same at every rate.
256286
audio_sample_t silence[4] = {{0}};
257-
hstx_packet_set_audio_samples(&packet, silence, 4, 4);
287+
hstx_packet_set_audio_samples(&packet, silence, 4, 4, dvi_audio_rates[DVI_AUDIO_RATE_COUNT - 1].sample_rate);
258288
for (int vsync = 0; vsync < 2; vsync++) {
259289
dvi_audio_encode(audio->silence[vsync], &packet, vsync);
260290
}
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));
264291
// Unspecified VIC: preserve CircuitPython's existing 625-line timing.
265292
hstx_packet_set_avi_infoframe(&packet, 0, 0);
266293
dvi_audio_encode(audio->avi_infoframe, &packet, dvi_audio_slot_vsync(audio, 1));
@@ -404,8 +431,8 @@ static const uint32_t *dvi_audio_next_packet(dvi_audio_state_t *audio, const int
404431
size_t previous = slot == 0 ? DVI_AUDIO_SLOTS - 1 : slot - 1;
405432
size_t lines = (audio->slot_line[slot] + MODE_640_V_TOTAL_LINES - audio->slot_line[previous]) % MODE_640_V_TOTAL_LINES;
406433
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;
434+
uint64_t sample_phase = audio->sample_phase + (uint64_t)lines * dvi_audio_rates[audio->rate_index].sample_rate * DVI_AUDIO_H_TOTAL;
435+
uint64_t acr_phase = audio->acr_phase + (uint64_t)lines * audio->acr_rate[audio->rate_index] * DVI_AUDIO_H_TOTAL;
409436
// Never let sample timing build up a long burst.
410437
if (sample_phase > pixel_clock * 12) {
411438
sample_phase = pixel_clock * 12;
@@ -414,12 +441,12 @@ static const uint32_t *dvi_audio_next_packet(dvi_audio_state_t *audio, const int
414441
const uint32_t *island;
415442
*count = 0;
416443
if (slot == 0) {
417-
island = audio->audio_infoframe;
444+
island = audio->audio_infoframe[audio->rate_index];
418445
} else if (slot == 1) {
419446
island = audio->avi_infoframe;
420447
} else if (acr_phase >= pixel_clock) {
421448
acr_phase -= pixel_clock;
422-
island = audio->acr[vsync];
449+
island = audio->acr[audio->rate_index][vsync];
423450
} else {
424451
// Sample packets carry four frames, fewer only at the end of a
425452
// sample.
@@ -438,7 +465,8 @@ static const uint32_t *dvi_audio_next_packet(dvi_audio_state_t *audio, const int
438465
packet_samples[i].right = samples[2 * i + 1];
439466
}
440467
hstx_packet_t packet;
441-
audio->channel_frame = hstx_packet_set_audio_samples(&packet, packet_samples, n, audio->channel_frame);
468+
audio->channel_frame = hstx_packet_set_audio_samples(&packet, packet_samples, n, audio->channel_frame,
469+
dvi_audio_rates[audio->rate_index].sample_rate);
442470
uint32_t *buffer = audio->pool[audio->pool_head];
443471
dvi_audio_encode(buffer, &packet, vsync);
444472
audio->pool_position[audio->pool_head] = audio->write_position;
@@ -466,7 +494,9 @@ static const uint32_t *dvi_audio_next_packet(dvi_audio_state_t *audio, const int
466494
//
467495
// With nothing to play, every frame comes out the same once the timing at
468496
// slot 0 repeats. Then the slots already hold the right packets, and this
469-
// stops writing until there is something to play.
497+
// stops writing until there is something to play. At 44.1 kHz a frame holds
498+
// 882 samples, not a whole number of packets, so frames never repeat and
499+
// silence keeps being written.
470500
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) {
471501
*used = 0;
472502
bool idle = available == 0 && !wait;
@@ -489,6 +519,7 @@ static bool dvi_audio_fill_slot(picodvi_framebuffer_obj_t *self, dvi_audio_state
489519
audio->idle_steady = false;
490520
audio->idle_run = 0;
491521
}
522+
audio->rate_index = audio->next_rate_index;
492523
// Free the pool buffers DMA has finished with.
493524
while (audio->pool_count > 0 && audio->pool_position[audio->pool_tail] < read) {
494525
audio->pool_tail = (audio->pool_tail + 1) % DVI_AUDIO_POOL;
@@ -692,6 +723,12 @@ static void picodvi_framebuffer_audio_stop(picodvi_framebuffer_obj_t *self) {
692723
memset(audio->slot_samples, 0, sizeof(audio->slot_samples));
693724
}
694725

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

886923
void common_hal_picodvi_audioout_play(picodvi_audioout_obj_t *self, mp_obj_t sample, bool loop) {
887924
audiosample_base_t *base = audiosample_check(sample);
888-
mp_arg_validate_int(base->sample_rate, 48000, MP_QSTR_sample_rate);
925+
size_t rate_index = 0;
926+
while (rate_index < DVI_AUDIO_RATE_COUNT && dvi_audio_rates[rate_index].sample_rate != base->sample_rate) {
927+
rate_index++;
928+
}
929+
if (rate_index == DVI_AUDIO_RATE_COUNT) {
930+
mp_raise_ValueError_varg(MP_ERROR_TEXT("Invalid %q"), MP_QSTR_sample_rate);
931+
}
889932
mp_arg_validate_int_range(base->channel_count, 1, 2, MP_QSTR_channel_count);
890933
if (base->bits_per_sample != 8 && base->bits_per_sample != 16) {
891934
mp_raise_ValueError_varg(MP_ERROR_TEXT("Invalid %q"), MP_QSTR_bits_per_sample);
892935
}
893936
common_hal_picodvi_audioout_stop(self);
937+
picodvi_framebuffer_audio_set_rate(self->framebuffer, rate_index);
894938
audiosample_reset_buffer(sample, false, 0);
895939
self->bytes_per_frame = base->channel_count * base->bits_per_sample / 8;
896940
self->source_frames = 0;

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

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -183,8 +183,22 @@ void hstx_packet_set_avi_infoframe(hstx_packet_t *packet, uint8_t vic, uint8_t p
183183
compute_all_parity(packet);
184184
}
185185

186+
// IEC 60958-3 channel status byte 3 sample frequency code.
187+
static uint8_t channel_status_sample_frequency(uint32_t sample_rate) {
188+
switch (sample_rate) {
189+
case 32000:
190+
return 0x03;
191+
case 44100:
192+
return 0x00;
193+
case 48000:
194+
return 0x02;
195+
default:
196+
return 0x01; // Sample rate not indicated.
197+
}
198+
}
199+
186200
int hstx_packet_set_audio_samples(hstx_packet_t *packet, const audio_sample_t *samples, int num_samples,
187-
int frame_count) {
201+
int frame_count, uint32_t sample_rate) {
188202
hstx_packet_init(packet);
189203
if (num_samples < 1) {
190204
num_samples = 1;
@@ -209,6 +223,7 @@ int hstx_packet_set_audio_samples(hstx_packet_t *packet, const audio_sample_t *s
209223
packet->header[2] = b_flags << 4;
210224
compute_header_parity(packet);
211225

226+
uint8_t sample_frequency = channel_status_sample_frequency(sample_rate);
212227
for (int i = 0; i < num_samples; i++) {
213228
uint8_t *d = packet->subpacket[i];
214229
int16_t left = samples[i].left;
@@ -221,9 +236,9 @@ int hstx_packet_set_audio_samples(hstx_packet_t *packet, const audio_sample_t *s
221236
d[4] = right & 0xFF;
222237
d[5] = (right >> 8) & 0xFF;
223238

224-
// IEC 60958 byte 0 = 0x04 (copy permitted), byte 3 = 0x02 (48 kHz).
239+
// IEC 60958 byte 0 = 0x04 (copy permitted), byte 3 = sample frequency.
225240
int frame = (frame_count + i) % 192;
226-
uint8_t status = frame / 8 == 0 ? 0x04 : (frame / 8 == 3 ? 0x02 : 0);
241+
uint8_t status = frame / 8 == 0 ? 0x04 : (frame / 8 == 3 ? sample_frequency : 0);
227242
uint8_t c = (status >> (frame % 8)) & 1;
228243
bool p_left = compute_parity3(d[1], d[2], c);
229244
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+
uint32_t sample_rate);
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)