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")