diff --git a/docs/environment.rst b/docs/environment.rst
index 26823650491..2bbaa95f1ef 100644
--- a/docs/environment.rst
+++ b/docs/environment.rst
@@ -241,6 +241,22 @@ Example: Configure the display to 640x480 black and white (1 bit per pixel):
`Adafruit Feather RP2350 `_
`Adafruit Metro RP2350 `_
+CIRCUITPY_PICODVI_AUDIO (boolean)
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+Whether the display configured at board initialization also sends sound through
+the DVI cable, as ``supervisor.runtime.audio``. Defaults to true. It needs a 640-wide
+output: 640x480, 320x240 or 160x120. Audio uses about 37 KB of internal RAM
+next to a 640x480 display. Set this to false to keep that RAM free, for
+example on a monitor without speakers.
+
+``supervisor.runtime.audio`` is a :py:class:`picodvi.AudioOut`. It becomes ``None`` when
+the display is released by ``displayio.release_displays()`` or when it is
+deinitialized, and stays ``None`` until the next reset.
+
+`Adafruit Feather RP2350 `_
+`Adafruit Fruit Jam `_
+`Adafruit Metro RP2350 `_
+
CIRCUITPY_SAFEMODE_DELAY (float)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Wait for the specified amount of time, in seconds, for the user to press the reset button
diff --git a/ports/raspberrypi/bindings/picodvi/AudioOut.c b/ports/raspberrypi/bindings/picodvi/AudioOut.c
index 6ad98bf5046..70014ac9472 100644
--- a/ports/raspberrypi/bindings/picodvi/AudioOut.c
+++ b/ports/raspberrypi/bindings/picodvi/AudioOut.c
@@ -16,17 +16,24 @@
//| class AudioOut:
//| """Plays audio through the DVI cable of a `Framebuffer`, on displays
//| that have speakers or an audio output. RP2350 only: elsewhere the
-//| constructor raises `NotImplementedError`."""
+//| constructor raises `NotImplementedError`.
+//|
+//| On boards with a default DVI display, ``supervisor.runtime.audio`` is an AudioOut
+//| for the display configured at startup. It is ``None`` if that display
+//| was not configured, its mode has no room for audio, or it was released.
+//| Set ``CIRCUITPY_PICODVI_AUDIO = false`` in ``settings.toml`` to turn it
+//| off."""
//|
//| def __init__(self, framebuffer: Framebuffer) -> None:
//| """Create an AudioOut that sends audio with ``framebuffer``'s video.
//|
//| The framebuffer must use a 640-pixel-wide output mode, such as
-//| 640x480 or 320x240. This reserves about 120 KB (640 wide) or
-//| 150 KB (320 or 160 wide) of internal RAM until `deinit`, so create
-//| the AudioOut soon after the Framebuffer. A 640x480 framebuffer with
-//| 8-bit color leaves too little RAM. The display receives silence
-//| whenever nothing is playing.
+//| 640x480 or 320x240. This reserves about 37 KB (640 wide) or
+//| 50 KB (320 or 160 wide) of internal RAM until `deinit`, so create
+//| the AudioOut soon after the Framebuffer. With 640x480 and 8-bit
+//| color, usually only the display configured at startup has room
+//| for audio. The display receives silence whenever nothing is
+//| playing.
//|
//| :param Framebuffer framebuffer: The display output to add audio to
//|
diff --git a/ports/raspberrypi/common-hal/picodvi/AudioOut.c b/ports/raspberrypi/common-hal/picodvi/AudioOut.c
index 1473ea8440d..ab5186ef4fa 100644
--- a/ports/raspberrypi/common-hal/picodvi/AudioOut.c
+++ b/ports/raspberrypi/common-hal/picodvi/AudioOut.c
@@ -9,67 +9,126 @@
#include
+#include "py/gc.h"
#include "py/runtime.h"
#include "bindings/picodvi/AudioOut.h"
#include "common-hal/picodvi/dvi_audio_packet.h"
#include "hardware/clocks.h"
+#include "hardware/dma.h"
#include "hardware/sync.h"
#include "pico/stdlib.h"
+#include "pico/time.h"
#include "shared-module/audiocore/__init__.h"
+#include "shared-bindings/supervisor/Runtime.h"
#include "supervisor/background_callback.h"
#include "supervisor/port.h"
#define DVI_AUDIO_RATE 48000u
-#define DVI_AUDIO_BANKS 2
#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)
-// The longest line dvi_audio_build_line() writes: an active line.
-#define DVI_AUDIO_LINE_WORDS (7 + HSTX_DATA_ISLAND_WORDS + 4 + 9)
-
-typedef uint32_t dvi_audio_line_t[DVI_AUDIO_LINE_WORDS];
-
-typedef struct {
- uint32_t *commands;
- dvi_audio_line_t *lines;
- volatile uint32_t samples;
- volatile uint32_t order;
- volatile bool ready;
-} dvi_audio_bank_t;
+// 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
+// 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.
+#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)
+// The refill never writes a slot this close ahead of the one DMA reads.
+#define DVI_AUDIO_MARGIN 4
+// Nor more than this far ahead, so play() starts within about 15 ms.
+#define DVI_AUDIO_AHEAD (DVI_AUDIO_SLOTS * 3 / 4)
+// While a sample is waiting for data, silence is only written this close
+// ahead of DMA.
+#define DVI_AUDIO_LOW_WATER (DVI_AUDIO_SLOTS / 4)
+// The sample pool holds about 13 ms, less than one 20 ms frame, so a timer
+// queues the refill this often as well as once per frame.
+#define DVI_AUDIO_REFILL_US 2500
+#define DVI_AUDIO_PREFIX_WORDS 9
+#define DVI_AUDIO_SUFFIX_BLANK_WORDS 9
+#define DVI_AUDIO_SUFFIX_ACTIVE_WORDS 15
+
+typedef uint32_t dvi_audio_island_t[DVI_AUDIO_SLOT_WORDS];
typedef struct dvi_audio_state {
- // Plain video command list plus audio headers, silence and active-line
- // preambles. Banks start as copies of it, and it is shown when no bank is
- // ready.
- uint32_t *base_commands;
- dvi_audio_bank_t bank[DVI_AUDIO_BANKS];
- volatile int active;
+ // One fixed command list. Each island line is three transfers: a prefix
+ // up to and including the leading guard band, the island, and a suffix
+ // from the trailing guard band on. Once DMA has read a slot, the refill
+ // points its island at the packet for the next frame.
+ uint32_t *commands;
+ dvi_audio_island_t *pool;
+ size_t command_size;
+ repeating_timer_t timer;
+ bool timer_running;
+ // Frames started on the audio command list, counted by the frame
+ // interrupt.
+ volatile uint32_t frames;
// Set to go back to video only at the next frame, then acknowledged by
// the frame interrupt once nothing reads this state any more.
volatile bool detach;
volatile bool detached;
- uint32_t next_order;
+ // The next slot to write, counted from the first audio frame, and its
+ // index in the frame.
+ uint64_t write_position;
+ size_t write_slot;
+ // The last read position found, to search on from.
+ uint32_t read_frames;
+ size_t read_slot;
+ // Pool buffers are used in order. Each remembers the slot it was
+ // written for, and is free again once DMA has read that slot.
+ uint64_t pool_position[DVI_AUDIO_POOL];
+ size_t pool_head;
+ size_t pool_tail;
+ size_t pool_count;
uint32_t pixel_clock;
- uint32_t max_packets;
- uint64_t phase;
- // ACR timing, carried from frame to frame so the average is exact.
+ // ACR packets per second, 128 * 48000 / N.
+ uint32_t acr_rate;
+ // Sample and ACR timing, carried from slot to slot.
+ uint64_t sample_phase;
uint64_t acr_phase;
int channel_frame;
- // Fixed lines the command lists point at: ACR, audio InfoFrame and AVI
- // InfoFrame on lines 0-2, then ACR and silence on a blanking line [0] and
- // an active line [1].
- dvi_audio_line_t header_line[3];
- uint32_t header_length[3];
- dvi_audio_line_t acr_line[2];
- uint32_t acr_length[2];
- // One bit per line that carries an ACR packet in the base list.
- uint32_t acr_lines[(MODE_640_V_TOTAL_LINES + 31) / 32];
- dvi_audio_line_t silence_line[2];
- uint32_t silence_length[2];
+ // Idle slots written in a row, the timing at the last idle slot 0, and
+ // whether the slots hold a repeating idle frame that needs no writes.
+ size_t idle_run;
+ uint64_t idle_sample_phase;
+ uint64_t idle_acr_phase;
+ bool idle_steady;
+ // Command list entry of each slot's island, its line, and the sample
+ // frames of the playing sample it holds.
+ uint16_t slot_entry[DVI_AUDIO_SLOTS];
+ 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 null_packet[2];
+ dvi_audio_island_t silence[2];
+ dvi_audio_island_t audio_infoframe;
+ dvi_audio_island_t avi_infoframe;
+ uint32_t prefix[2][DVI_AUDIO_PREFIX_WORDS];
+ uint32_t suffix_blank[2][DVI_AUDIO_SUFFIX_BLANK_WORDS];
+ uint32_t suffix_active[DVI_AUDIO_SUFFIX_ACTIVE_WORDS];
} dvi_audio_state_t;
// Static so it can never be freed while it is queued.
static background_callback_t dvi_audio_refill_callback;
+#if defined(DEFAULT_DVI_BUS_CLK_DP)
+// supervisor.runtime.audio. Static so it outlives the VM, like the boot display.
+picodvi_audioout_obj_t picodvi_board_audioout = {
+ .base = { .type = &mp_type_NoneType },
+};
+
+static bool is_board_audioout(const picodvi_audioout_obj_t *self) {
+ return self == &picodvi_board_audioout;
+}
+#else
+static bool is_board_audioout(const picodvi_audioout_obj_t *self) {
+ (void)self;
+ return false;
+}
+#endif
+
// Active line with the video preamble and guard band that audio mode needs.
static uint32_t dvi_audio_vactive_line640[] = {
HSTX_CMD_RAW_REPEAT | MODE_640_H_FRONT_PORCH,
@@ -89,115 +148,196 @@ static uint32_t dvi_audio_vactive_line640[] = {
HSTX_CMD_TMDS | MODE_640_H_ACTIVE_PIXELS
};
-static uint32_t dvi_audio_build_line(uint32_t *p, const hstx_data_island_t *island, bool vsync, bool active) {
- uint32_t *start = p;
- uint32_t h0 = vsync ? SYNC_V0_H0 : SYNC_V1_H0;
- uint32_t h1 = vsync ? SYNC_V0_H1 : SYNC_V1_H1;
- uint32_t preamble = (h0 & 0x3ff) | (TMDS_CTRL_01 << 10) | (TMDS_CTRL_01 << 20);
- *p++ = HSTX_CMD_RAW_REPEAT | MODE_640_H_FRONT_PORCH;
- *p++ = h1;
- *p++ = HSTX_CMD_NOP;
- *p++ = HSTX_CMD_RAW_REPEAT | 8;
- *p++ = preamble;
- *p++ = HSTX_CMD_NOP;
- *p++ = HSTX_CMD_RAW | HSTX_DATA_ISLAND_WORDS;
- memcpy(p, island->words, sizeof(island->words));
- p += HSTX_DATA_ISLAND_WORDS;
- *p++ = HSTX_CMD_NOP;
- *p++ = HSTX_CMD_RAW_REPEAT | (MODE_640_H_SYNC_WIDTH - 8 - HSTX_DATA_ISLAND_WORDS);
- *p++ = h0;
- *p++ = HSTX_CMD_NOP;
- if (active) {
- *p++ = HSTX_CMD_RAW_REPEAT | (MODE_640_H_BACK_PORCH - 10);
+// Fills the line pieces around a slot. The island sits in the horizontal
+// sync pulse, after an 8 character data island preamble.
+static void dvi_audio_init_line_pieces(dvi_audio_state_t *audio) {
+ for (int vsync = 0; vsync < 2; vsync++) {
+ uint32_t h0 = vsync ? SYNC_V0_H0 : SYNC_V1_H0;
+ uint32_t h1 = vsync ? SYNC_V0_H1 : SYNC_V1_H1;
+ // The guard band only depends on the sync levels.
+ hstx_packet_t packet;
+ hstx_data_island_t island;
+ hstx_packet_init(&packet);
+ hstx_encode_data_island(&island, &packet, vsync, true);
+ uint32_t guard = island.words[0];
+
+ uint32_t *p = audio->prefix[vsync];
+ *p++ = HSTX_CMD_RAW_REPEAT | MODE_640_H_FRONT_PORCH;
*p++ = h1;
*p++ = HSTX_CMD_NOP;
*p++ = HSTX_CMD_RAW_REPEAT | 8;
- *p++ = (h1 & 0x3ff) | (TMDS_CTRL_01 << 10) | (TMDS_CTRL_00 << 20);
- *p++ = HSTX_CMD_NOP;
- *p++ = HSTX_CMD_RAW_REPEAT | 2;
- *p++ = 0x2ccu | (0x133u << 10) | (0x2ccu << 20);
- *p++ = HSTX_CMD_TMDS | MODE_640_H_ACTIVE_PIXELS;
- } else {
- *p++ = HSTX_CMD_RAW_REPEAT | (MODE_640_H_BACK_PORCH + MODE_640_H_ACTIVE_PIXELS);
- *p++ = h1;
+ *p++ = (h0 & 0x3ff) | (TMDS_CTRL_01 << 10) | (TMDS_CTRL_01 << 20);
*p++ = HSTX_CMD_NOP;
+ *p++ = HSTX_CMD_RAW | HSTX_DATA_ISLAND_WORDS;
+ *p++ = guard;
+ *p++ = guard;
+
+ uint32_t *s = audio->suffix_blank[vsync];
+ *s++ = guard;
+ *s++ = guard;
+ *s++ = HSTX_CMD_NOP;
+ *s++ = HSTX_CMD_RAW_REPEAT | (MODE_640_H_SYNC_WIDTH - 8 - HSTX_DATA_ISLAND_WORDS);
+ *s++ = h0;
+ *s++ = HSTX_CMD_NOP;
+ *s++ = HSTX_CMD_RAW_REPEAT | (MODE_640_H_BACK_PORCH + MODE_640_H_ACTIVE_PIXELS);
+ *s++ = h1;
+ *s++ = HSTX_CMD_NOP;
+
+ if (!vsync) {
+ // Active lines are never in vsync.
+ memcpy(audio->suffix_active, audio->suffix_blank[0], 6 * sizeof(uint32_t));
+ s = &audio->suffix_active[6];
+ *s++ = HSTX_CMD_RAW_REPEAT | (MODE_640_H_BACK_PORCH - 10);
+ *s++ = h1;
+ *s++ = HSTX_CMD_NOP;
+ *s++ = HSTX_CMD_RAW_REPEAT | 8;
+ *s++ = (h1 & 0x3ff) | (TMDS_CTRL_01 << 10) | (TMDS_CTRL_00 << 20);
+ *s++ = HSTX_CMD_NOP;
+ *s++ = HSTX_CMD_RAW_REPEAT | 2;
+ *s++ = 0x2ccu | (0x133u << 10) | (0x2ccu << 20);
+ *s++ = HSTX_CMD_TMDS | MODE_640_H_ACTIVE_PIXELS;
+ }
}
- return p - start;
}
-// Returns true when an ACR packet is due on this line. ACR packets go out at
-// 128 * 48000 / 6144 = 1000 per second, spread over every line.
-static bool dvi_audio_acr_due(uint64_t *phase, uint32_t pixel_clock) {
- bool due = *phase >= pixel_clock;
- if (due) {
- *phase -= pixel_clock;
- }
- *phase += (uint64_t)(128 * DVI_AUDIO_RATE / 6144) * DVI_AUDIO_H_TOTAL;
- return due;
+static void dvi_audio_encode(uint32_t *out, const hstx_packet_t *packet, bool vsync) {
+ hstx_data_island_t island;
+ hstx_encode_data_island(&island, packet, vsync, true);
+ memcpy(out, &island.words[2], sizeof(dvi_audio_island_t));
}
-static void dvi_audio_init_lines(dvi_audio_state_t *audio) {
+static bool dvi_audio_slot_vsync(const dvi_audio_state_t *audio, size_t slot) {
+ return audio->slot_line[slot] < MODE_640_V_SYNC_WIDTH;
+}
+
+// The command list word holding the address of a slot's island.
+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;
- hstx_data_island_t island;
- uint32_t cts = ((uint64_t)audio->pixel_clock * 6144 + (128 * DVI_AUDIO_RATE / 2)) / (128 * DVI_AUDIO_RATE);
- hstx_packet_set_acr(&packet, 6144, cts);
- hstx_encode_data_island(&island, &packet, true, true);
- audio->header_length[0] = dvi_audio_build_line(audio->header_line[0], &island, true, false);
- hstx_encode_data_island(&island, &packet, false, true);
- for (int active = 0; active < 2; active++) {
- audio->acr_length[active] = dvi_audio_build_line(audio->acr_line[active], &island, false, active);
- }
- // The base list, shown only when no bank is ready, has a fixed ACR
- // pattern that starts on line 0.
- uint64_t acr_phase = audio->pixel_clock;
- for (size_t line = 0; line < MODE_640_V_TOTAL_LINES; line++) {
- if (dvi_audio_acr_due(&acr_phase, audio->pixel_clock)) {
- audio->acr_lines[line / 32] |= 1u << (line % 32);
+ // 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.
+ 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;
+ 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;
+ if (k == 0 || rate % frame_pixels != 0) {
+ continue;
+ }
+ rate /= frame_pixels;
+ if (n_times_rate % rate != 0) {
+ continue;
+ }
+ 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;
}
}
- audio->acr_phase = audio->pixel_clock;
- hstx_packet_set_audio_infoframe(&packet, DVI_AUDIO_RATE, 2, 16);
- hstx_encode_data_island(&island, &packet, true, true);
- audio->header_length[1] = dvi_audio_build_line(audio->header_line[1], &island, true, false);
- // Unspecified VIC: preserve CircuitPython's existing 625-line timing.
- hstx_packet_set_avi_infoframe(&packet, 0, 0);
- hstx_encode_data_island(&island, &packet, false, true);
- audio->header_length[2] = dvi_audio_build_line(audio->header_line[2], &island, false, false);
- // Frame position 4 never sets the block start flag, as in pico_hdmi.
+ 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);
+ }
+ 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.
audio_sample_t silence[4] = {{0}};
hstx_packet_set_audio_samples(&packet, silence, 4, 4);
- hstx_encode_data_island(&island, &packet, false, true);
- for (int active = 0; active < 2; active++) {
- audio->silence_length[active] = dvi_audio_build_line(audio->silence_line[active], &island, false, active);
+ 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));
}
-static bool dvi_audio_acr_line(const dvi_audio_state_t *audio, size_t line) {
- return (audio->acr_lines[line / 32] >> (line % 32)) & 1;
+// Builds the audio command list from the video one. Lines without a slot
+// keep their video transfers, except that active lines get the video
+// preamble and guard band that audio mode needs. Every island starts as a
+// null packet.
+static void dvi_audio_build_commands(dvi_audio_state_t *audio, const picodvi_framebuffer_obj_t *self) {
+ const size_t size = audio->command_size;
+ // Word offset of the transfer count and read address within an entry.
+ const size_t data = size - 2;
+ const size_t active_start = MODE_640_V_SYNC_WIDTH + MODE_640_V_BACK_PORCH;
+ const size_t active_end = active_start + MODE_640_V_ACTIVE_LINES;
+ const uint32_t *video = self->dma_commands;
+ uint32_t *out = audio->commands;
+ size_t slot = 0;
+ for (size_t line = 0; line < MODE_640_V_TOTAL_LINES; line++) {
+ bool active = line >= active_start && line < active_end;
+ bool vsync = line < MODE_640_V_SYNC_WIDTH;
+ // video points at this line's first entry, the whole line when
+ // blanking or the part before the pixels when active.
+ if (slot < DVI_AUDIO_SLOTS && audio->slot_line[slot] == line) {
+ const uint32_t *pieces[3] = {
+ audio->prefix[vsync], audio->null_packet[vsync],
+ active ? audio->suffix_active : audio->suffix_blank[vsync],
+ };
+ const uint32_t lengths[3] = {
+ DVI_AUDIO_PREFIX_WORDS, DVI_AUDIO_SLOT_WORDS,
+ active ? DVI_AUDIO_SUFFIX_ACTIVE_WORDS : DVI_AUDIO_SUFFIX_BLANK_WORDS,
+ };
+ for (int i = 0; i < 3; i++) {
+ if (i == 1) {
+ audio->slot_entry[slot] = (out - audio->commands) / size;
+ }
+ memcpy(out, video, data * sizeof(uint32_t));
+ out[data] = lengths[i];
+ out[data + 1] = (uintptr_t)pieces[i];
+ out += size;
+ }
+ slot++;
+ } else {
+ memcpy(out, video, size * sizeof(uint32_t));
+ if (active) {
+ out[data] = MP_ARRAY_SIZE(dvi_audio_vactive_line640);
+ out[data + 1] = (uintptr_t)dvi_audio_vactive_line640;
+ }
+ out += size;
+ }
+ video += size;
+ if (active) {
+ // The pixel transfer.
+ memcpy(out, video, size * sizeof(uint32_t));
+ out += size;
+ video += size;
+ }
+ }
+ // The empty last entry that raises the frame interrupt.
+ memcpy(out, video, size * sizeof(uint32_t));
}
-// Returns true when a sample packet is due on this line. Reserved lines,
-// headers and ACR, never carry sample packets.
-static bool dvi_audio_packet_due(uint64_t *phase, uint32_t pixel_clock, bool reserved) {
- *phase += (uint64_t)DVI_AUDIO_RATE * DVI_AUDIO_H_TOTAL;
- if (reserved || *phase < (uint64_t)pixel_clock * 4) {
- return false;
- }
- *phase -= (uint64_t)pixel_clock * 4;
- return true;
+// Command list words for the framebuffer's video list plus two more
+// transfers on each slot line.
+static size_t dvi_audio_commands_len(const picodvi_framebuffer_obj_t *self, size_t command_size) {
+ return self->dma_commands_len + DVI_AUDIO_SLOTS * 2 * command_size;
}
static void dvi_audio_free_state(dvi_audio_state_t *audio) {
- if (audio->base_commands) {
- port_free(audio->base_commands);
+ if (audio->timer_running) {
+ cancel_repeating_timer(&audio->timer);
}
- for (int i = 0; i < DVI_AUDIO_BANKS; i++) {
- if (audio->bank[i].commands) {
- port_free(audio->bank[i].commands);
- }
- if (audio->bank[i].lines) {
- port_free(audio->bank[i].lines);
- }
+ if (audio->commands) {
+ port_free(audio->commands);
+ }
+ if (audio->pool) {
+ port_free(audio->pool);
}
port_free(audio);
}
@@ -210,6 +350,10 @@ void picodvi_audioout_framebuffer_deinit(picodvi_framebuffer_obj_t *self) {
audioout->sample = MP_OBJ_NULL;
audioout->framebuffer = NULL;
self->audioout = MP_OBJ_NULL;
+ if (is_board_audioout(audioout)) {
+ // supervisor.runtime.audio goes back to None, as the display does.
+ audioout->base.type = &mp_type_NoneType;
+ }
}
dvi_audio_state_t *audio = self->dvi_audio;
if (!audio) {
@@ -219,64 +363,234 @@ void picodvi_audioout_framebuffer_deinit(picodvi_framebuffer_obj_t *self) {
dvi_audio_free_state(audio);
}
-static void dvi_audio_allocate(picodvi_framebuffer_obj_t *self) {
+// The first slot DMA has not finished reading, counted from the first audio
+// frame. Before the first audio frame starts, nothing has been read.
+static uint64_t dvi_audio_read_position(dvi_audio_state_t *audio, const picodvi_framebuffer_obj_t *self) {
+ uint32_t frames;
+ uintptr_t read_addr;
+ do {
+ frames = audio->frames;
+ read_addr = dma_hw->ch[self->dma_command_channel].read_addr;
+ } while (frames != audio->frames);
+ if (frames == 0) {
+ return 0;
+ }
+ // Entries the command channel has loaded. The last one may still be
+ // sending.
+ size_t loaded = (read_addr - (uintptr_t)audio->commands) / (audio->command_size * sizeof(uint32_t));
+ // Count the slots whose transfer is done, slot_entry[i] + 1 < loaded,
+ // going on from the last answer in the same frame.
+ size_t done = frames == audio->read_frames ? audio->read_slot : 0;
+ while (done < DVI_AUDIO_SLOTS && audio->slot_entry[done] + 1u < loaded) {
+ done++;
+ }
+ audio->read_frames = frames;
+ audio->read_slot = done;
+ return (uint64_t)(frames - 1) * DVI_AUDIO_SLOTS + done;
+}
+
+static void dvi_audio_advance(dvi_audio_state_t *audio) {
+ audio->write_position++;
+ audio->write_slot = audio->write_slot + 1 == DVI_AUDIO_SLOTS ? 0 : audio->write_slot + 1;
+}
+
+// Works out the packet for the next slot and advances the timing past it:
+// an InfoFrame, an ACR packet, up to four sample frames from samples into a
+// pool buffer, silence or a null packet. Takes real samples only while
+// available is not zero, and sets *count to how many it took. Returns NULL,
+// changing nothing, if the samples need a pool buffer and none is free.
+static const uint32_t *dvi_audio_next_packet(dvi_audio_state_t *audio, const int16_t *samples, size_t available, size_t *count) {
+ size_t slot = audio->write_slot;
+ 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;
+ // Never let sample timing build up a long burst.
+ if (sample_phase > pixel_clock * 12) {
+ sample_phase = pixel_clock * 12;
+ }
+ bool vsync = dvi_audio_slot_vsync(audio, slot);
+ const uint32_t *island;
+ *count = 0;
+ if (slot == 0) {
+ island = audio->audio_infoframe;
+ } else if (slot == 1) {
+ island = audio->avi_infoframe;
+ } else if (acr_phase >= pixel_clock) {
+ acr_phase -= pixel_clock;
+ island = audio->acr[vsync];
+ } else {
+ // Sample packets carry four frames, fewer only at the end of a
+ // sample.
+ size_t due = 0;
+ while (due < 4 && sample_phase >= (due + 1) * pixel_clock) {
+ due++;
+ }
+ if (available > 0 && (due == 4 || (due > 0 && due >= available))) {
+ if (audio->pool_count == DVI_AUDIO_POOL) {
+ return NULL;
+ }
+ size_t n = MIN(due, available);
+ audio_sample_t packet_samples[4];
+ for (size_t i = 0; i < n; i++) {
+ packet_samples[i].left = samples[2 * i];
+ 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);
+ uint32_t *buffer = audio->pool[audio->pool_head];
+ dvi_audio_encode(buffer, &packet, vsync);
+ audio->pool_position[audio->pool_head] = audio->write_position;
+ audio->pool_head = (audio->pool_head + 1) % DVI_AUDIO_POOL;
+ audio->pool_count++;
+ sample_phase -= n * pixel_clock;
+ island = buffer;
+ *count = n;
+ } else if (available == 0 && due == 4) {
+ sample_phase -= 4 * pixel_clock;
+ island = audio->silence[vsync];
+ } else {
+ island = audio->null_packet[vsync];
+ }
+ }
+ audio->sample_phase = sample_phase;
+ audio->acr_phase = acr_phase;
+ dvi_audio_advance(audio);
+ return island;
+}
+
+// Points the next slot at its packet. Sets *used to the real sample frames
+// it took. With wait set, writes nothing unless DMA is close behind. Returns
+// false when the slot cannot be written yet. Never allocates or raises.
+//
+// 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.
+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;
+ if (audio->detach || (audio->idle_steady && idle)) {
+ return false;
+ }
+ uint64_t read = dvi_audio_read_position(audio, self);
+ if (audio->idle_steady) {
+ // Catch the timing up to just ahead of DMA. The slots passed over
+ // already hold the packets it works out. Whole frames repeat, so
+ // skip them, then work through the rest of one.
+ uint64_t target = read + DVI_AUDIO_MARGIN;
+ if (audio->write_position < target) {
+ audio->write_position += (target - audio->write_position) / DVI_AUDIO_SLOTS * DVI_AUDIO_SLOTS;
+ }
+ while (audio->write_position < target) {
+ size_t count;
+ dvi_audio_next_packet(audio, NULL, 0, &count);
+ }
+ audio->idle_steady = false;
+ audio->idle_run = 0;
+ }
+ // 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;
+ audio->pool_count--;
+ }
+ if (audio->write_position >= read + DVI_AUDIO_AHEAD) {
+ return false;
+ }
+ if (audio->frames > 0 && audio->write_position < read + DVI_AUDIO_MARGIN) {
+ // Fell behind. The slots skipped send null packets rather than
+ // whatever they last pointed at. A frame or more behind, null every
+ // slot once and skip the whole frames.
+ uint64_t target = read + DVI_AUDIO_MARGIN;
+ if (target - audio->write_position >= DVI_AUDIO_SLOTS) {
+ for (size_t slot = 0; slot < DVI_AUDIO_SLOTS; slot++) {
+ *dvi_audio_slot_island(audio, slot) = (uintptr_t)audio->null_packet[dvi_audio_slot_vsync(audio, slot)];
+ audio->slot_samples[slot] = 0;
+ }
+ audio->write_position += (target - audio->write_position) / DVI_AUDIO_SLOTS * DVI_AUDIO_SLOTS;
+ }
+ while (audio->write_position < target) {
+ size_t slot = audio->write_slot;
+ *dvi_audio_slot_island(audio, slot) = (uintptr_t)audio->null_packet[dvi_audio_slot_vsync(audio, slot)];
+ audio->slot_samples[slot] = 0;
+ dvi_audio_advance(audio);
+ }
+ audio->idle_run = 0;
+ }
+ if (wait && audio->write_position >= read + DVI_AUDIO_LOW_WATER) {
+ return false;
+ }
+ size_t slot = audio->write_slot;
+ if (slot == 0 && idle) {
+ if (audio->idle_run >= DVI_AUDIO_SLOTS &&
+ audio->sample_phase == audio->idle_sample_phase &&
+ audio->acr_phase == audio->idle_acr_phase) {
+ audio->idle_steady = true;
+ return false;
+ }
+ audio->idle_sample_phase = audio->sample_phase;
+ audio->idle_acr_phase = audio->acr_phase;
+ }
+ size_t count;
+ const uint32_t *island = dvi_audio_next_packet(audio, samples, available, &count);
+ if (island == NULL) {
+ // Wait for DMA to free a pool buffer.
+ return false;
+ }
+ *dvi_audio_slot_island(audio, slot) = (uintptr_t)island;
+ audio->slot_samples[slot] = count;
+ audio->idle_run = idle ? audio->idle_run + 1 : 0;
+ *used = count;
+ return true;
+}
+
+static void dvi_audio_queue_refill(void);
+
+static bool __not_in_flash_func(dvi_audio_timer_callback)(repeating_timer_t * timer) {
+ (void)timer;
+ dvi_audio_queue_refill();
+ return true;
+}
+
+// Allocates the audio state and starts sending audio signaling and silence
+// from the next frame. Returns false, leaving nothing allocated, if internal
+// RAM is short. Never raises, so it can run before the VM starts.
+static bool dvi_audio_try_allocate(picodvi_framebuffer_obj_t *self) {
if (self->dvi_audio) {
- return;
+ return true;
}
dvi_audio_state_t *audio = port_malloc(sizeof(*audio), true);
if (!audio) {
- m_malloc_fail(sizeof(*audio));
+ return false;
}
memset(audio, 0, sizeof(*audio));
- audio->active = -1;
- audio->pixel_clock = clock_get_hz(clk_hstx) / 5;
- // Room for the most packets one frame can need at this pixel clock.
- audio->max_packets = (uint64_t)MODE_640_V_TOTAL_LINES * DVI_AUDIO_RATE * DVI_AUDIO_H_TOTAL /
- ((uint64_t)audio->pixel_clock * 4) + 2;
- size_t commands_size = self->dma_commands_len * sizeof(uint32_t);
- size_t lines_size = audio->max_packets * sizeof(dvi_audio_line_t);
+ audio->command_size = self->output_width != self->width ? 4 : 2;
// DMA reads all of these, so they must be in internal RAM.
- audio->base_commands = port_malloc(commands_size, true);
- bool ok = audio->base_commands != NULL;
- for (int i = 0; i < DVI_AUDIO_BANKS && ok; i++) {
- audio->bank[i].commands = port_malloc(commands_size, true);
- audio->bank[i].lines = port_malloc(lines_size, true);
- ok = audio->bank[i].commands != NULL && audio->bank[i].lines != NULL;
- }
- if (!ok) {
+ audio->commands = port_malloc(dvi_audio_commands_len(self, audio->command_size) * sizeof(uint32_t), true);
+ audio->pool = port_malloc(DVI_AUDIO_POOL * sizeof(dvi_audio_island_t), true);
+ if (!audio->commands || !audio->pool) {
dvi_audio_free_state(audio);
- m_malloc_fail(commands_size + lines_size);
+ return false;
}
- dvi_audio_init_lines(audio);
- memcpy(audio->base_commands, self->dma_commands, commands_size);
- bool doubled = self->output_width != self->width;
- size_t command_size = doubled ? 4 : 2;
- size_t entry_offset = doubled ? 2 : 0;
- const size_t active_start = MODE_640_V_SYNC_WIDTH + MODE_640_V_BACK_PORCH;
- const size_t active_end = active_start + MODE_640_V_ACTIVE_LINES;
- size_t command_word = 0;
- uint64_t phase = 0;
- for (size_t line = 0; line < MODE_640_V_TOTAL_LINES; line++) {
- bool active = line >= active_start && line < active_end;
- uint32_t *entry = &audio->base_commands[command_word + entry_offset];
- if (dvi_audio_packet_due(&phase, audio->pixel_clock, line < 3 || dvi_audio_acr_line(audio, line))) {
- // Keep the audio link running with silence when no bank is ready.
- entry[0] = audio->silence_length[active];
- entry[1] = (uintptr_t)audio->silence_line[active];
- } else if (line < 3) {
- entry[0] = audio->header_length[line];
- entry[1] = (uintptr_t)audio->header_line[line];
- } else if (dvi_audio_acr_line(audio, line)) {
- entry[0] = audio->acr_length[active];
- entry[1] = (uintptr_t)audio->acr_line[active];
- } else if (active) {
- entry[0] = MP_ARRAY_SIZE(dvi_audio_vactive_line640);
- entry[1] = (uintptr_t)dvi_audio_vactive_line640;
- }
- command_word += command_size * (active ? 2 : 1);
+ audio->pixel_clock = clock_get_hz(clk_hstx) / 5;
+ for (size_t i = 0; i < DVI_AUDIO_SLOTS; i++) {
+ audio->slot_line[i] = i * MODE_640_V_TOTAL_LINES / DVI_AUDIO_SLOTS;
+ }
+ dvi_audio_init_line_pieces(audio);
+ dvi_audio_init_packets(audio);
+ dvi_audio_build_commands(audio, self);
+ // Start with silence, before the frame interrupt can switch to the list.
+ size_t used;
+ while (dvi_audio_fill_slot(self, audio, NULL, 0, false, &used)) {
+ }
+ audio->timer_running = add_repeating_timer_us(-DVI_AUDIO_REFILL_US, dvi_audio_timer_callback, NULL, &audio->timer);
+ if (!audio->timer_running) {
+ // Refilling once a frame can't keep ahead of DMA.
+ dvi_audio_free_state(audio);
+ return false;
}
- __dmb();
self->dvi_audio = audio;
+ return true;
}
static void audioout_refill(picodvi_audioout_obj_t *self);
@@ -292,13 +606,18 @@ static void dvi_audio_run_refill(void *data) {
audioout_refill(MP_OBJ_TO_PTR(self->audioout));
}
-// Called from the frame interrupt after the next frame has started.
-void __not_in_flash_func(picodvi_audioout_frame_done)(void) {
- if (active_picodvi->dvi_audio && active_picodvi->audioout != MP_OBJ_NULL) {
+// Called from interrupts: the frame interrupt and the refill timer.
+static void __not_in_flash_func(dvi_audio_queue_refill)(void) {
+ if (active_picodvi != NULL && active_picodvi->dvi_audio && active_picodvi->audioout != MP_OBJ_NULL) {
background_callback_add(&dvi_audio_refill_callback, dvi_audio_run_refill, active_picodvi);
}
}
+// Called from the frame interrupt after the next frame has started.
+void __not_in_flash_func(picodvi_audioout_frame_done)(void) {
+ dvi_audio_queue_refill();
+}
+
// Called at frame boundary only; old DMA frame has completed.
uint32_t *__not_in_flash_func(picodvi_audioout_next_frame)(void) {
dvi_audio_state_t *audio = active_picodvi->dvi_audio;
@@ -307,116 +626,11 @@ uint32_t *__not_in_flash_func(picodvi_audioout_next_frame)(void) {
}
if (audio->detach) {
// From this frame on, DMA reads only the plain video commands.
- audio->active = -1;
audio->detached = true;
return active_picodvi->dma_commands;
}
- if (audio->active >= 0) {
- audio->bank[audio->active].samples = 0;
- audio->active = -1;
- }
- int next = -1;
- for (int i = 0; i < DVI_AUDIO_BANKS; i++) {
- if (!audio->bank[i].ready) {
- continue;
- }
- if (next < 0 || (int32_t)(audio->bank[i].order - audio->bank[next].order) < 0) {
- next = i;
- }
- }
- if (next >= 0) {
- audio->active = next;
- audio->bank[next].ready = false;
- return audio->bank[next].commands;
- }
- // No bank ready: send a frame of silence rather than repeat the last one.
- return audio->base_commands;
-}
-
-// Encodes up to one frame of 16-bit stereo samples, interleaved left, right,
-// into a free bank, with silence after them. Sets *used to the sample frames
-// taken. Returns false, changing nothing, if audio is not reserved or no bank
-// is free. Never allocates or raises.
-static bool picodvi_framebuffer_audio_fill(picodvi_framebuffer_obj_t *self, const int16_t *samples, size_t available, size_t *used) {
- dvi_audio_state_t *audio = self->dvi_audio;
- if (!audio) {
- return false;
- }
- int bank_index = -1;
- // Read active + ready atomically: an IRQ can promote a ready bank to active
- // between these two observations. Once selected, an unready bank cannot be
- // taken by the ISR until this writer publishes it below.
- uint32_t irq_state = save_and_disable_interrupts();
- for (int i = 0; i < DVI_AUDIO_BANKS; i++) {
- if (audio->active != i && !audio->bank[i].ready) {
- bank_index = i;
- break;
- }
- }
- restore_interrupts(irq_state);
- if (bank_index < 0) {
- return false;
- }
- dvi_audio_bank_t *bank = &audio->bank[bank_index];
- memcpy(bank->commands, audio->base_commands, self->dma_commands_len * sizeof(uint32_t));
- size_t command_word = 0;
- size_t samples_read = 0;
- unsigned packets = 0;
- bool doubled = self->output_width != self->width;
- size_t command_size = doubled ? 4 : 2;
- size_t entry_offset = doubled ? 2 : 0;
- const size_t active_start = MODE_640_V_SYNC_WIDTH + MODE_640_V_BACK_PORCH;
- const size_t active_end = active_start + MODE_640_V_ACTIVE_LINES;
- for (size_t line = 0; line < MODE_640_V_TOTAL_LINES; line++) {
- bool active = line >= active_start && line < active_end;
- uint32_t *entry = &bank->commands[command_word + entry_offset];
- bool acr = dvi_audio_acr_due(&audio->acr_phase, audio->pixel_clock);
- bool due = dvi_audio_packet_due(&audio->phase, audio->pixel_clock, line < 3 || acr);
- if (line == 0 && acr) {
- entry[0] = audio->header_length[0];
- entry[1] = (uintptr_t)audio->header_line[0];
- } else if (line == 1 || line == 2) {
- // InfoFrames, as in the base list.
- } else if (acr) {
- entry[0] = audio->acr_length[active];
- entry[1] = (uintptr_t)audio->acr_line[active];
- } else if (!due) {
- if (active) {
- entry[0] = MP_ARRAY_SIZE(dvi_audio_vactive_line640);
- entry[1] = (uintptr_t)dvi_audio_vactive_line640;
- } else {
- const uint32_t *plain = &self->dma_commands[command_word + entry_offset];
- entry[0] = plain[0];
- entry[1] = plain[1];
- }
- } else if (samples_read >= available || packets >= audio->max_packets) {
- entry[0] = audio->silence_length[active];
- entry[1] = (uintptr_t)audio->silence_line[active];
- } else {
- audio_sample_t frames[4] = {{0}};
- size_t count = MIN(available - samples_read, 4);
- for (size_t j = 0; j < count; j++) {
- size_t k = samples_read + j;
- frames[j].left = samples[2 * k];
- frames[j].right = samples[2 * k + 1];
- }
- hstx_packet_t packet;
- hstx_data_island_t island;
- audio->channel_frame = hstx_packet_set_audio_samples(&packet, frames, count, audio->channel_frame);
- hstx_encode_data_island(&island, &packet, false, true);
- entry[0] = dvi_audio_build_line(bank->lines[packets], &island, false, active);
- entry[1] = (uintptr_t)bank->lines[packets];
- packets++;
- samples_read += count;
- }
- command_word += command_size * (active ? 2 : 1);
- }
- bank->samples = samples_read;
- bank->order = audio->next_order++;
- __dmb();
- bank->ready = true;
- *used = samples_read;
- return true;
+ audio->frames++;
+ return audio->commands;
}
// Allocates the audio state and starts sending audio signaling and silence.
@@ -426,7 +640,9 @@ static void picodvi_framebuffer_audio_reserve(picodvi_framebuffer_obj_t *self) {
if (self->output_width != 640) {
mp_raise_ValueError_varg(MP_ERROR_TEXT("Invalid %q"), MP_QSTR_framebuffer);
}
- dvi_audio_allocate(self);
+ if (!dvi_audio_try_allocate(self)) {
+ m_malloc_fail(sizeof(dvi_audio_state_t));
+ }
}
// Stops audio and frees the audio state, waiting up to two frames for DMA to
@@ -448,40 +664,34 @@ static void picodvi_framebuffer_audio_release(picodvi_framebuffer_obj_t *self) {
dvi_audio_free_state(audio);
}
-// Returns the number of sample frames queued or playing, 0 if not reserved.
+// Returns the sample frames written ahead of DMA, 0 if not reserved.
static size_t picodvi_framebuffer_audio_pending(picodvi_framebuffer_obj_t *self) {
dvi_audio_state_t *audio = self->dvi_audio;
if (!audio) {
return 0;
}
- uint32_t irq_state = save_and_disable_interrupts();
+ uint64_t read = dvi_audio_read_position(audio, self);
size_t count = 0;
- for (int i = 0; i < DVI_AUDIO_BANKS; i++) {
- if (audio->bank[i].ready || audio->active == i) {
- count += audio->bank[i].samples;
+ if (audio->write_position > read) {
+ size_t slot = read % DVI_AUDIO_SLOTS;
+ for (uint64_t ahead = audio->write_position - read; ahead > 0; ahead--) {
+ count += audio->slot_samples[slot];
+ slot = slot + 1 == DVI_AUDIO_SLOTS ? 0 : slot + 1;
}
}
- restore_interrupts(irq_state);
return count;
}
-// Drops queued banks, so silence follows the frame now playing.
+// Forgets the sample frames written ahead of DMA. They still play, for at
+// most about 15 ms.
static void picodvi_framebuffer_audio_stop(picodvi_framebuffer_obj_t *self) {
dvi_audio_state_t *audio = self->dvi_audio;
if (!audio) {
return;
}
- uint32_t irq_state = save_and_disable_interrupts();
- for (int i = 0; i < DVI_AUDIO_BANKS; i++) {
- audio->bank[i].ready = false;
- if (audio->active != i) {
- audio->bank[i].samples = 0;
- }
- }
- restore_interrupts(irq_state);
+ memset(audio->slot_samples, 0, sizeof(audio->slot_samples));
}
-
// 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);
@@ -555,31 +765,41 @@ static void audioout_stage(picodvi_audioout_obj_t *self) {
}
}
-// Runs in the background after every frame, and once from play() and
-// resume(). Fills every free bank from the stage, or with silence when
-// nothing is playing, so the ACR timing carries on between frames.
+// Runs in the background after every frame and every few milliseconds, and
+// once from play() and resume(). Points every slot DMA has finished with at
+// the next packet: from the stage, or silence when nothing is playing, so
+// the timing carries on.
static void audioout_refill(picodvi_audioout_obj_t *self) {
- if (self->refilling) {
+ dvi_audio_state_t *audio = self->framebuffer->dvi_audio;
+ if (self->refilling || !audio) {
return;
}
self->refilling = true;
+ // Stage frames already sent to slots.
+ size_t offset = 0;
while (true) {
- size_t available = 0;
- if (self->sample != MP_OBJ_NULL && !self->paused) {
+ // stop() may empty the stage while a sample buffer is fetched.
+ offset = MIN(offset, self->stage_frames);
+ bool playing = self->sample != MP_OBJ_NULL && !self->paused;
+ if (playing && self->stage_frames - offset < 4) {
+ // Move what is left to the front, then top the stage up.
+ self->stage_frames -= offset;
+ memmove(self->stage, &self->stage[offset * 2], self->stage_frames * 4);
+ offset = 0;
audioout_stage(self);
- available = self->stage_frames;
- // Wait for more of a sample that is still playing.
- if (available == 0 && !self->source_done) {
- break;
- }
}
+ size_t available = playing ? self->stage_frames - offset : 0;
+ // A sample still playing may have more frames later.
+ bool wait = playing && available == 0 && !self->source_done;
size_t used;
- if (!picodvi_framebuffer_audio_fill(self->framebuffer, self->stage, available, &used)) {
+ if (!dvi_audio_fill_slot(self->framebuffer, audio, &self->stage[offset * 2], available, wait, &used)) {
break;
}
- self->stage_frames -= used;
- memmove(self->stage, &self->stage[used * 2], self->stage_frames * 4);
+ offset += used;
}
+ offset = MIN(offset, self->stage_frames);
+ self->stage_frames -= offset;
+ memmove(self->stage, &self->stage[offset * 2], self->stage_frames * 4);
self->refilling = false;
}
@@ -595,11 +815,56 @@ void common_hal_picodvi_audioout_construct(picodvi_audioout_obj_t *self, picodvi
}
void picodvi_audioout_reset(void) {
- if (active_picodvi != NULL && active_picodvi->audioout != MP_OBJ_NULL) {
- common_hal_picodvi_audioout_deinit(MP_OBJ_TO_PTR(active_picodvi->audioout));
+ if (active_picodvi == NULL || active_picodvi->audioout == MP_OBJ_NULL) {
+ return;
+ }
+ picodvi_audioout_obj_t *self = MP_OBJ_TO_PTR(active_picodvi->audioout);
+ if (is_board_audioout(self)) {
+ // Keep supervisor.runtime.audio for the next VM. Drop the sample, which is on
+ // the VM heap.
+ common_hal_picodvi_audioout_stop(self);
+ return;
+ }
+ common_hal_picodvi_audioout_deinit(self);
+}
+
+void picodvi_audioout_collect_ptrs(picodvi_framebuffer_obj_t *framebuffer) {
+ if (framebuffer->audioout == MP_OBJ_NULL) {
+ return;
+ }
+ gc_collect_ptr(framebuffer->audioout);
+ // supervisor.runtime.audio is not on the heap, so the GC does not trace into it.
+ picodvi_audioout_obj_t *self = MP_OBJ_TO_PTR(framebuffer->audioout);
+ gc_collect_ptr(self->sample);
+}
+
+#if defined(DEFAULT_DVI_BUS_CLK_DP)
+bool picodvi_audioout_construct_board(picodvi_framebuffer_obj_t *framebuffer) {
+ picodvi_audioout_obj_t *self = &picodvi_board_audioout;
+ if (framebuffer->output_width != 640 || framebuffer->audioout != MP_OBJ_NULL) {
+ return false;
}
+ if (!dvi_audio_try_allocate(framebuffer)) {
+ return false;
+ }
+ memset(self, 0, sizeof(*self));
+ self->base.type = &picodvi_audioout_type;
+ self->framebuffer = framebuffer;
+ self->sample = MP_OBJ_NULL;
+ // The framebuffer schedules its refills, which send silence until a
+ // sample plays.
+ framebuffer->audioout = MP_OBJ_FROM_PTR(self);
+ return true;
}
+mp_obj_t common_hal_supervisor_runtime_get_audio(void) {
+ if (picodvi_board_audioout.base.type != &picodvi_audioout_type) {
+ return mp_const_none;
+ }
+ return MP_OBJ_FROM_PTR(&picodvi_board_audioout);
+}
+#endif
+
bool common_hal_picodvi_audioout_deinited(picodvi_audioout_obj_t *self) {
return self->framebuffer == NULL;
}
@@ -612,6 +877,10 @@ void common_hal_picodvi_audioout_deinit(picodvi_audioout_obj_t *self) {
self->framebuffer->audioout = MP_OBJ_NULL;
picodvi_framebuffer_audio_release(self->framebuffer);
self->framebuffer = NULL;
+ if (is_board_audioout(self)) {
+ // Nothing can construct it again until the next reset.
+ self->base.type = &mp_type_NoneType;
+ }
}
void common_hal_picodvi_audioout_play(picodvi_audioout_obj_t *self, mp_obj_t sample, bool loop) {
@@ -639,6 +908,8 @@ void common_hal_picodvi_audioout_play(picodvi_audioout_obj_t *self, mp_obj_t sam
void common_hal_picodvi_audioout_stop(picodvi_audioout_obj_t *self) {
self->sample = MP_OBJ_NULL;
self->paused = false;
+ // Drops what was staged.
+ self->stage_frames = 0;
picodvi_framebuffer_audio_stop(self->framebuffer);
}
diff --git a/ports/raspberrypi/common-hal/picodvi/AudioOut.h b/ports/raspberrypi/common-hal/picodvi/AudioOut.h
index 9875f15342f..d271bc5fc67 100644
--- a/ports/raspberrypi/common-hal/picodvi/AudioOut.h
+++ b/ports/raspberrypi/common-hal/picodvi/AudioOut.h
@@ -43,5 +43,23 @@ void picodvi_audioout_frame_done(void);
// and deinit its AudioOut.
void picodvi_audioout_framebuffer_deinit(picodvi_framebuffer_obj_t *framebuffer);
-// Deinit any AudioOut before the VM heap goes away.
+// Deinit any AudioOut before the VM heap goes away. The board AudioOut only
+// stops, since it lives as long as the boot framebuffer.
void picodvi_audioout_reset(void);
+
+// Marks the AudioOut of each framebuffer, and its sample, as in use for the GC.
+void picodvi_audioout_collect_ptrs(picodvi_framebuffer_obj_t *framebuffer);
+
+#if defined(DEFAULT_DVI_BUS_CLK_DP)
+// supervisor.runtime.audio. Its type is NoneType until
+// picodvi_audioout_construct_board succeeds, and again after it or its
+// framebuffer is deinitialized. supervisor.runtime.audio returns None then.
+extern picodvi_audioout_obj_t picodvi_board_audioout;
+
+// Makes picodvi_board_audioout send audio with the boot framebuffer. Runs
+// before the VM starts, so it never allocates on the VM heap and never raises.
+// Returns true on success. Returns false, leaving the board AudioOut as None
+// and nothing allocated, if the output is not 640 pixels wide or internal RAM
+// is short.
+bool picodvi_audioout_construct_board(picodvi_framebuffer_obj_t *framebuffer);
+#endif
diff --git a/ports/raspberrypi/common-hal/picodvi/__init__.c b/ports/raspberrypi/common-hal/picodvi/__init__.c
index c2d4b297b39..a9276096529 100644
--- a/ports/raspberrypi/common-hal/picodvi/__init__.c
+++ b/ports/raspberrypi/common-hal/picodvi/__init__.c
@@ -16,6 +16,10 @@
#include "py/runtime.h"
#include "supervisor/port_heap.h"
+#if CIRCUITPY_PICODVI_AUDIOOUT && defined(DEFAULT_DVI_BUS_CLK_DP)
+#include "common-hal/picodvi/AudioOut.h"
+#endif
+
#if defined(DEFAULT_DVI_BUS_CLK_DP)
static bool picodvi_autoconstruct_enabled(mp_int_t *default_width, mp_int_t *default_height) {
char buf[sizeof("detect")];
@@ -101,6 +105,11 @@ void picodvi_autoconstruct(void) {
return;
}
+ #if CIRCUITPY_PICODVI_AUDIOOUT
+ bool audio = true;
+ (void)settings_get_bool("CIRCUITPY_PICODVI_AUDIO", &audio);
+ #endif
+
mp_int_t width = default_width;
mp_int_t height = 0;
mp_int_t color_depth = 8;
@@ -162,6 +171,14 @@ void picodvi_autoconstruct(void) {
MP_OBJ_FROM_PTR(fb),
rotation,
true);
+
+ #if CIRCUITPY_PICODVI_AUDIOOUT
+ // Before the VM heap exists, so the audio buffers get internal RAM. On
+ // failure supervisor.runtime.audio is None, and the display still works.
+ if (audio) {
+ (void)picodvi_audioout_construct_board(fb);
+ }
+ #endif
}
#else
void picodvi_autoconstruct(void) {
diff --git a/shared-bindings/supervisor/Runtime.c b/shared-bindings/supervisor/Runtime.c
index 5b2885f26c0..aa99026980a 100644
--- a/shared-bindings/supervisor/Runtime.c
+++ b/shared-bindings/supervisor/Runtime.c
@@ -243,6 +243,29 @@ MP_PROPERTY_GETSET(supervisor_runtime_display_obj,
(mp_obj_t)&supervisor_runtime_set_display_obj);
#endif
+MP_WEAK mp_obj_t common_hal_supervisor_runtime_get_audio(void) {
+ return mp_const_none;
+}
+
+//| audio: picodvi.AudioOut | None
+//| """The default audio output for the supervisor and user code.
+//|
+//| On boards with a default DVI display this is a `picodvi.AudioOut` that
+//| sends sound over the DVI cable along with the picture. It is `None` when
+//| the display did not start, when audio is turned off with
+//| ``CIRCUITPY_PICODVI_AUDIO = false`` in ``settings.toml``, or after the
+//| display is released with ``displayio.release_displays()``.
+//|
+//| On boards without such an output, this property is always `None`."""
+//|
+static mp_obj_t supervisor_runtime_get_audio(mp_obj_t self) {
+ return common_hal_supervisor_runtime_get_audio();
+}
+MP_DEFINE_CONST_FUN_OBJ_1(supervisor_runtime_get_audio_obj, supervisor_runtime_get_audio);
+
+MP_PROPERTY_GETTER(supervisor_runtime_audio_obj,
+ (mp_obj_t)&supervisor_runtime_get_audio_obj);
+
static const mp_rom_map_elem_t supervisor_runtime_locals_dict_table[] = {
{ MP_ROM_QSTR(MP_QSTR_usb_connected), MP_ROM_PTR(&supervisor_runtime_usb_connected_obj) },
{ MP_ROM_QSTR(MP_QSTR_serial_connected), MP_ROM_PTR(&supervisor_runtime_serial_connected_obj) },
@@ -257,6 +280,7 @@ static const mp_rom_map_elem_t supervisor_runtime_locals_dict_table[] = {
#else
{ MP_ROM_QSTR(MP_QSTR_display), MP_ROM_NONE },
#endif
+ { MP_ROM_QSTR(MP_QSTR_audio), MP_ROM_PTR(&supervisor_runtime_audio_obj) },
};
static MP_DEFINE_CONST_DICT(supervisor_runtime_locals_dict, supervisor_runtime_locals_dict_table);
diff --git a/shared-bindings/supervisor/Runtime.h b/shared-bindings/supervisor/Runtime.h
index 054281e5490..42cc6f3a979 100644
--- a/shared-bindings/supervisor/Runtime.h
+++ b/shared-bindings/supervisor/Runtime.h
@@ -23,6 +23,11 @@ bool common_hal_supervisor_runtime_get_serial_connected(void);
uint32_t common_hal_supervisor_runtime_get_serial_bytes_available(void);
+// Returns the audio output that the supervisor set up along with the boot
+// display, or mp_const_none if there is none. The default returns
+// mp_const_none; a port overrides it when it has one. Never raises.
+mp_obj_t common_hal_supervisor_runtime_get_audio(void);
+
// TODO: placeholders for future functions
// bool common_hal_get_supervisor_runtime_repl_active(void);
// bool common_hal_get_supervisor_runtime_usb_enumerated(void);
diff --git a/shared-module/displayio/__init__.c b/shared-module/displayio/__init__.c
index fd1dff5b7a2..be1bd972964 100644
--- a/shared-module/displayio/__init__.c
+++ b/shared-module/displayio/__init__.c
@@ -42,6 +42,10 @@
#include "shared-module/aurora_epaper/aurora_framebuffer.h"
#endif
+#if CIRCUITPY_PICODVI_AUDIOOUT
+#include "common-hal/picodvi/AudioOut.h"
+#endif
+
#if CIRCUITPY_MIPIDSI
#include "shared-bindings/mipidsi/Display.h"
#endif
@@ -396,7 +400,7 @@ void displayio_gc_collect(void) {
#endif
#if CIRCUITPY_PICODVI_AUDIOOUT
if (display_bus_type == &picodvi_framebuffer_type) {
- gc_collect_ptr(display_buses[i].picodvi.audioout);
+ picodvi_audioout_collect_ptrs(&display_buses[i].picodvi);
}
#endif
}
diff --git a/tests/circuitpython-manual/picodvi/board_audio.py b/tests/circuitpython-manual/picodvi/board_audio.py
new file mode 100644
index 00000000000..dba3ba03f6d
--- /dev/null
+++ b/tests/circuitpython-manual/picodvi/board_audio.py
@@ -0,0 +1,45 @@
+# SPDX-FileCopyrightText: 2026 Adafruit Industries
+#
+# SPDX-License-Identifier: MIT
+# A board with a default DVI display, a display attached and no display
+# settings in settings.toml.
+# Plays a tone on supervisor.runtime.audio, then checks it becomes
+# None when the display is released.
+#
+# By hand: CIRCUITPY_PICODVI_AUDIO = false in settings.toml, then reset:
+# supervisor.runtime.audio is None and the display is 8-bit.
+import array
+import math
+import time
+
+import audiocore
+import displayio
+import picodvi
+import supervisor
+
+audio = supervisor.runtime.audio
+print("supervisor.runtime.audio", audio)
+assert isinstance(audio, picodvi.AudioOut)
+fb = supervisor.runtime.display.framebuffer
+print("boot display", fb.width, fb.height, fb.color_depth)
+
+tone = audiocore.RawSample(
+ array.array("h", [int(math.sin(math.pi * 2 * i / 48) * 8000) for i in range(48)]),
+ sample_rate=48000,
+)
+audio.play(tone, loop=True)
+time.sleep(1)
+print("playing", audio.playing)
+audio.stop()
+print("stopped", not audio.playing)
+
+try:
+ picodvi.AudioOut(fb)
+ print("FAIL: second AudioOut on the boot display")
+except RuntimeError as e:
+ print("second AudioOut refused:", e)
+
+displayio.release_displays()
+print("after release", supervisor.runtime.audio)
+assert supervisor.runtime.audio is None
+print("done")