From d27bc9fed2042f24303970d4d7b480d58dac9703 Mon Sep 17 00:00:00 2001 From: Diego Parrilla Date: Wed, 30 Sep 2026 13:03:26 +0200 Subject: [PATCH 1/4] STE DMA sound: the chip plays, the RP writes ahead On an STE or a Mega STE the sound goes through the DMA sound chip: 8-bit samples at 12,517 Hz, mono, no interrupt per sample. The chip reads ST RAM only, so userfw loops it over a 2 KB ring just below screen page A ($6F800) and fills the ring every VBL, right after the wake: it reads where the chip plays (the frame counter's middle and low bytes, the middle one read again as it moves), reports it by 8 bytes at $FB8Dxx, and copies the cart buffer, the ring's mirror, from where its last copy ended up to 768 bytes (3 VBLs) ahead of the chip, so a missed VBL still finds samples. About 0.35 ms at 8 MHz; Timer-B stays off on this path and its 1.6 ms per VBL are free. At boot userfw sets the Microwire as TOS does (0 dB, tone flat, the YM mixed in); it stops the chip on the way out to GEM and before a cold reset. The ST takes the DMA path when the hello's machine family is the STE's and the RP has not asked for the YM (a word in slot 6 of the shared variables); audio.c applies the same rule at each hello. The audio buffer grows to 2 KB, the mirror; on the YM path its first 1 KB holds the slices as before. APP_FREE moves to $FA4980. audio.c's writer keeps the mirror written 928 bytes ahead of where the chip plays now (its last report plus the samples played since: a missed VBL sends no report), never where the ST copies; late writes and underruns are counted, and an empty FIFO holds the last sample. The FIFO holds a VBL more than AUDIO_FIFO_SLICES so the main loop may still go 80 ms between top-ups. Apps give YM pairs as before or signed 8-bit PCM at any rate (audio_play_pcm_loop, audio_set_pcm_callback); either plays on both outputs, converted at runtime through the 64-entry table both ways (a pair outside it by the YM's volume curve) with linear resampling. A YM source on the YM path goes through byte for byte as before. audio_prefer_ym() keeps an STE on the YM from its next boot; audio_uses_dma() and the audioOutput counter say which output plays. tests/host/test_audio_dma.c runs the output against a simulated STE: steady play, main-loop stalls within and beyond the tolerance, a missed VBL, an ST reset, YM pairs on the DMA chip, PCM on the YM, the output switch. Every sample played is the source in order, the held last sample, or silence before the start. Cartridge code: 8,928 bytes. --- rp/src/audio.c | 506 ++++++++++++++++++++--- rp/src/include/audio.h | 84 ++-- rp/src/include/cart_shared.h | 49 ++- rp/src/include/st_session.h | 4 + rp/src/include/target_firmware.h | 410 +++++++++--------- rp/src/st_session.c | 2 + target/atarist/src/inc/sidecart_layout.s | 42 +- target/atarist/src/userfw.s | 166 +++++++- tests/host/test_audio.c | 4 + tests/host/test_audio_dma.c | 426 +++++++++++++++++++ tests/host/test_layout.py | 29 +- 11 files changed, 1396 insertions(+), 326 deletions(-) create mode 100644 tests/host/test_audio_dma.c diff --git a/rp/src/audio.c b/rp/src/audio.c index e386e5d..ca4fbdc 100644 --- a/rp/src/audio.c +++ b/rp/src/audio.c @@ -1,29 +1,36 @@ /** * File: audio.c - * Description: Cart-shared audio buffer producer. + * Description: The app's sound, to the YM or to the DMA sound chip. * - * The m68k Timer-B IRQ plays (vA, vB) pairs from the cart buffer at - * CART_AUDIO_BUFFER_OFFSET, one slice of CART_AUDIO_SLICES per VBL: the - * ST's VBL handler moves Timer-B to the next slice and reports which one - * through the ROM3 window CART_ROM3_AUDIO_SLICE_WINDOW. The RP writes only - * the slices ahead of the one playing, so the ST never plays a slice while - * it is being written. + * Two outputs, one per machine (the ST's hello says which): + * - the YM: the m68k Timer-B IRQ plays (vA, vB) pairs from the cart buffer + * at CART_AUDIO_BUFFER_OFFSET, one slice of CART_AUDIO_SLICES per VBL: + * the ST's VBL handler moves Timer-B to the next slice and reports which + * one through CART_ROM3_AUDIO_SLICE_WINDOW. The RP writes only the slices + * ahead of the one playing, so the ST never plays a slice while it is + * being written; + * - the DMA sound chip of an STE or a Mega STE: it plays 8-bit samples at + * 12,517 Hz from a ring in ST RAM, which the ST fills every VBL from the + * cart buffer, its mirror, up to CART_AUDIO_DMA_LEAD bytes ahead of where + * it reports the chip plays (CART_ROM3_DMA_POS_WINDOW). The RP writes the + * mirror further ahead still, never where the ST copies. + * The ST picks the output at boot by the same rule the RP applies at the + * hello (select_output): the DMA chip when the machine has one, unless the + * app asked for the YM (audio_prefer_ym; CART_AUDIO_OUT_OFFSET). * * Two stages, so that the sound does not depend on the app's frame loop: - * - audio_render_frame(), from the main loop, tops up a small RAM FIFO - * with the app's fill callback (a loop in flash, a .YMS file on the SD - * card, the app's own generator); - * - a repeating timer interrupt (audio_writer) follows the ST's reports - * and moves one VBL of samples from the FIFO into each slice ahead. - * The main loop may go AUDIO_FIFO_SLICES VBLs between two top-ups without - * the sound noticing (80 ms by default): each VBL takes one slice from the - * FIFO, the slices written ahead included. A sample reaches the ST at most - * AUDIO_FIFO_SLICES + AUDIO_SLICES_AHEAD VBLs after the callback produced - * it. When the FIFO is empty the slice holds the last sample (an underrun, - * counted), never stale data. + * - audio_render_frame(), from the main loop, tops up a small RAM FIFO in + * the output's format with the app's source: YM pairs (a loop in flash, a + * .YMS file on the SD card, the app's callback) or 8-bit PCM (a loop, a + * callback), converted when the output wants the other (the 64-entry + * two-channel table both ways, linear resampling between the rates); + * - a repeating timer interrupt (audio_writer) follows the ST's reports and + * moves samples from the FIFO into the cart buffer ahead of the ST. + * The main loop may go about AUDIO_FIFO_SLICES VBLs between two top-ups + * without the sound noticing (80 ms by default). When the FIFO is empty the + * output holds the last sample (an underrun, counted), never stale data. * - * See audio.h for the public API and `audio_play_loop` / - * `audio_set_fill_callback` semantics. + * See audio.h for the public API. */ #include "audio.h" @@ -40,6 +47,7 @@ #include "hardware/sync.h" #include "pico/stdlib.h" #include "pico/time.h" +#include "st_session.h" /* A PAL VBL, nominally. The slices follow the ST's own VBLs; this is the * period the sample arithmetic below is for. */ @@ -51,47 +59,100 @@ * = 223.43 B/VBL @ 2 B/sample (dual-channel mode). * Rounded up to 224, of a slice's CART_AUDIO_SLICE_BYTES. */ #define AUDIO_FILL_BYTES_PER_VBL 224u +#define AUDIO_FILL_SAMPLES_PER_VBL (AUDIO_FILL_BYTES_PER_VBL / 2u) /* Slices written ahead of the one the ST plays. */ #define AUDIO_SLICES_AHEAD 2u -/* The FIFO between the app's fill callback and the slice writer, in VBLs of - * samples: the longest the main loop may go between two top-ups. More - * delays the sound as much: up to AUDIO_FIFO_SLICES + AUDIO_SLICES_AHEAD - * VBLs from the callback to the ST. */ +/* The FIFO between the app's source and the writer, in VBLs of samples: + * the longest the main loop may go between two top-ups. More delays the + * sound as much: on the YM up to AUDIO_FIFO_SLICES + AUDIO_SLICES_AHEAD + * VBLs from the source to the ST. */ #ifndef AUDIO_FIFO_SLICES #define AUDIO_FIFO_SLICES 4u #endif -/* The slice writer runs this often; the ST's report is seen at most this - * long after its VBL, a slice ahead of when it is needed. */ +/* The writer runs this often; the ST's report is seen at most this long + * after its VBL, well before the samples are needed. */ #define AUDIO_WRITER_PERIOD_US 4000 /* How far back in the ROM3 ring the writer looks for the latest report: a - * frame adds a few samples (the report, the blit acknowledgement, keys). */ + * frame adds a few samples (the reports, the blit acknowledgement, keys). */ #define AUDIO_REPORT_LOOKBACK 64u /* No new report for this long: the ST left userfw (GEM, a reset). The next * report starts the count again. */ #define AUDIO_REPORT_TIMEOUT_US 200000u +/* Must match TIMERB_COUNT in target/atarist/src/userfw.s: + * 2.4576 MHz / 4 / 110 = 5,585.45 Hz */ +#define AUDIO_NATIVE_RATE_HZ 5585u + +/* The DMA output: its FIFO holds a VBL more than AUDIO_FIFO_SLICES VBLs + * of samples at 12,517 Hz (250.3 a VBL): it is topped up by the chunk, and + * the main loop may still go AUDIO_FIFO_SLICES VBLs between two top-ups. + * The writer keeps the mirror written this far + * ahead of where the chip plays now: its last report, plus the samples + * played since (a missed VBL sends no report, and the chip goes on). At its + * next VBL the ST copies up to CART_AUDIO_DMA_LEAD ahead; the rest covers + * the writer's period and how late it sees a report (4 ms, 50 samples, + * each). */ +#define AUDIO_DMA_FIFO_BYTES ((AUDIO_FIFO_SLICES + 1u) * 256u) +#define AUDIO_DMA_AHEAD (CART_AUDIO_DMA_LEAD + 160u) +/* The estimate goes at most this far past the last report. */ +#define AUDIO_DMA_EST_MAX_US (3u * AUDIO_FRAME_PERIOD_US) +#define AUDIO_DMA_RING_MASK (CART_AUDIO_DMA_RING_BYTES - 1u) + +_Static_assert(AUDIO_DMA_RATE_HZ == CART_AUDIO_DMA_RATE_HZ, + "audio.h and cart_shared.h disagree on the DMA rate"); + +/* A PCM callback is asked for this many samples at a time. */ +#define AUDIO_PCM_CHUNK 64u + +typedef enum { + AUDIO_OUT_NONE = 0, /* between two outputs: the writer waits */ + AUDIO_OUT_YM, + AUDIO_OUT_DMA, +} audio_out_t; + static uint8_t *s_audio_buf; +static volatile audio_out_t s_out; +static bool s_prefer_ym; +static uint32_t s_hellos_seen; + +/* The app's source: YM pairs at AUDIO_NATIVE_RATE_HZ, or PCM at a rate. */ static audio_fill_cb_t s_fill_cb; +static audio_pcm_cb_t s_pcm_cb; +static uint32_t s_pcm_rate; +/* The YM output's FIFO: whole VBLs of pairs. */ static uint8_t s_fifo[AUDIO_FIFO_SLICES][AUDIO_FILL_BYTES_PER_VBL]; static volatile uint32_t s_fifo_head; /* slices produced (main loop) */ static volatile uint32_t s_fifo_tail; /* slices consumed (writer) */ +/* The DMA output's FIFO: samples. */ +static int8_t s_dma_fifo[AUDIO_DMA_FIFO_BYTES]; +static volatile uint32_t s_dma_head; /* samples produced (main loop) */ +static volatile uint32_t s_dma_tail; /* samples consumed (writer) */ + +/* The YM writer's view of the ST. */ static bool s_st_known; /* the ST's slice is known */ static uint32_t s_st_vbl; /* VBLs counted from the reports; its low bits are the slice */ static uint32_t s_next_vbl; /* the next VBL whose slice is not written yet */ static uint16_t s_last_report; static uint32_t s_last_report_us; static uint8_t s_hold[2]; /* the last sample written: what an underrun holds */ + +/* The DMA writer's view of the ST. */ +static bool s_dma_known; +static uint32_t s_dma_front; /* the next offset of the ring the RP writes */ +static int8_t s_dma_hold; + static repeating_timer_t s_writer_timer; /* Readable over SWD by their symbols (tools/dev/swd.py counters). */ -uint32_t audioSlicesWritten; -uint32_t audioUnderruns; /* slices written as a held sample: the FIFO was empty */ -uint32_t audioLateSlices; /* slices the ST started before the RP had written them */ +uint32_t audioSlicesWritten; /* VBLs of samples the writer followed */ +uint32_t audioUnderruns; /* writes of a held sample: the FIFO was empty */ +uint32_t audioLateSlices; /* the ST played or copied what the RP had not written */ +uint32_t audioOutput; /* 1: the YM, 2: the DMA sound chip */ static bool audio_writer(repeating_timer_t *timer); @@ -101,15 +162,17 @@ static const uint8_t *s_loop_data; static uint32_t s_loop_bytes; static uint32_t s_loop_pos; +/* audio_play_pcm_loop()'s span. */ +static const int8_t *s_pcm_loop_data; +static uint32_t s_pcm_loop_samples; +static uint32_t s_pcm_loop_pos; + /* .YMS-file streaming state. audio_play_yms_file() validates the * 16-byte header, stores the data offset, and installs audio_yms_cb * as the per-VBL fill callback. The file is kept open for the * lifetime of playback. */ #define AUDIO_YMS_HEADER_SIZE 16u #define AUDIO_YMS_MODE_DUAL_GHOST 1u -/* Must match TIMERB_COUNT in target/atarist/src/userfw.s: - * 2.4576 MHz / 4 / 110 = 5,585.45 Hz */ -#define AUDIO_NATIVE_RATE_HZ 5585u static FIL s_yms_file; static bool s_yms_open; @@ -117,29 +180,228 @@ static bool s_yms_open; static bool s_yms_failing; static FSIZE_t s_yms_data_offset; +/* --- PCM <-> YM pairs ---------------------------------------------------- */ + +/* The (vA, vB) pairs of the 1988 Ghostbusters demo's SAMPLE1 table, indexed + * by the upper 6 bits of an unsigned 8-bit sample: tools/wav_to_ym4.py's + * GHOSTBUSTERS_LUT (tests/host/test_audio_convert.c compares the two). */ +static const uint8_t k_ghost_pairs[64][2] = { + {0, 0}, {0, 2}, {1, 2}, {2, 2}, {2, 3}, {1, 4}, {2, 4}, {2, 5}, + {0, 6}, {2, 6}, {3, 6}, {4, 6}, {2, 7}, {4, 7}, {5, 7}, {2, 8}, + {3, 8}, {4, 8}, {5, 8}, {2, 9}, {3, 9}, {4, 9}, {5, 9}, {6, 9}, + {7, 9}, {3, 10}, {4, 10}, {5, 10}, {6, 10}, {7, 10}, {0, 11}, {1, 11}, + {2, 11}, {4, 11}, {5, 11}, {6, 11}, {7, 11}, {8, 11}, {8, 11}, {9, 11}, + {9, 11}, {0, 12}, {1, 12}, {2, 12}, {3, 12}, {4, 12}, {5, 12}, {6, 12}, + {8, 12}, {8, 12}, {9, 12}, {9, 12}, {9, 12}, {10, 12}, {0, 13}, {2, 13}, + {3, 13}, {4, 13}, {5, 13}, {6, 13}, {7, 13}, {8, 13}, {8, 13}, {9, 13}, +}; + +/* A YM channel's amplitude at each volume, x 10,000: 2^((v - 15) / 2), the + * curve tools/wav_to_ym4.py fits pairs to. */ +static const uint16_t k_ym_amp[16] = {0, 78, 110, 156, 221, 313, + 442, 625, 884, 1250, 1768, 2500, + 3536, 5000, 7071, 10000}; + +/* A pair (vA << 4 | vB) as a signed 8-bit sample: the middle of the table + * entries it is, else the entry nearest in summed amplitude. */ +static int8_t s_pair_pcm[256]; + +static int8_t pcm_of_index(uint32_t index) { + return (int8_t)((int32_t)(index * 4u + 2u) - 128); +} + +static void build_pair_pcm(void) { + for (uint32_t pair = 0; pair < 256u; pair++) { + uint32_t a = pair >> 4, b = pair & 15u; + uint32_t sum = 0, count = 0; + for (uint32_t i = 0; i < 64u; i++) { + if (k_ghost_pairs[i][0] == a && k_ghost_pairs[i][1] == b) { + sum += i; + count++; + } + } + if (count != 0u) { + s_pair_pcm[pair] = pcm_of_index((sum + count / 2u) / count); + continue; + } + uint32_t amp = k_ym_amp[a] + k_ym_amp[b]; + uint32_t best = 0, best_diff = UINT32_MAX; + for (uint32_t i = 0; i < 64u; i++) { + uint32_t e = k_ym_amp[k_ghost_pairs[i][0]] + k_ym_amp[k_ghost_pairs[i][1]]; + uint32_t diff = e > amp ? e - amp : amp - e; + if (diff < best_diff) { + best_diff = diff; + best = i; + } + } + s_pair_pcm[pair] = pcm_of_index(best); + } +} + +/* --- The source, as a stream of samples ---------------------------------- */ + +static uint8_t s_ym_stage[AUDIO_FILL_BYTES_PER_VBL]; +static uint32_t s_ym_stage_pos = AUDIO_FILL_BYTES_PER_VBL; +static int8_t s_pcm_stage[AUDIO_PCM_CHUNK]; +static uint32_t s_pcm_stage_pos = AUDIO_PCM_CHUNK; + +/* Linear resampling from the source's rate: s0 and s1 are the source + * samples around the output sample, pos the fraction between them (16.16). */ +static bool s_rs_primed; +static int32_t s_rs_s0; +static int32_t s_rs_s1; +static uint32_t s_rs_pos; + +static void source_restart(void) { + s_ym_stage_pos = AUDIO_FILL_BYTES_PER_VBL; + s_pcm_stage_pos = AUDIO_PCM_CHUNK; + s_rs_primed = false; +} + +static uint32_t source_rate(void) { + return s_pcm_cb != NULL ? s_pcm_rate : AUDIO_NATIVE_RATE_HZ; +} + +/* The next source sample, as signed 8-bit PCM. */ +static int32_t source_sample(void) { + if (s_fill_cb != NULL) { + if (s_ym_stage_pos >= AUDIO_FILL_BYTES_PER_VBL) { + s_fill_cb(s_ym_stage, AUDIO_FILL_BYTES_PER_VBL); + s_ym_stage_pos = 0; + } + uint32_t pair = ((s_ym_stage[s_ym_stage_pos] & 15u) << 4) | + (s_ym_stage[s_ym_stage_pos + 1u] & 15u); + s_ym_stage_pos += 2u; + return s_pair_pcm[pair]; + } + if (s_pcm_cb != NULL) { + if (s_pcm_stage_pos >= AUDIO_PCM_CHUNK) { + s_pcm_cb(s_pcm_stage, AUDIO_PCM_CHUNK); + s_pcm_stage_pos = 0; + } + return s_pcm_stage[s_pcm_stage_pos++]; + } + return 0; +} + +/* The next output sample at `step` source samples per output sample + * (16.16): 1.0 passes the source through unchanged. */ +static int32_t resample(uint32_t step) { + if (!s_rs_primed) { + s_rs_s0 = source_sample(); + s_rs_s1 = source_sample(); + s_rs_pos = 0; + s_rs_primed = true; + } + int32_t v = s_rs_s0 + (((s_rs_s1 - s_rs_s0) * (int32_t)(s_rs_pos >> 4)) >> 12); + s_rs_pos += step; + while (s_rs_pos >= 0x10000u) { + s_rs_pos -= 0x10000u; + s_rs_s0 = s_rs_s1; + s_rs_s1 = source_sample(); + } + return v; +} + +static uint32_t rate_step(uint32_t out_rate) { + return (uint32_t)(((uint64_t)source_rate() << 16) / out_rate); +} + +/* One VBL of YM pairs. A YM source goes through as it is. */ +static void produce_ym(uint8_t *dst) { + if (s_fill_cb != NULL) { + s_fill_cb(dst, AUDIO_FILL_BYTES_PER_VBL); + return; + } + uint32_t step = rate_step(AUDIO_NATIVE_RATE_HZ); + for (uint32_t i = 0; i < AUDIO_FILL_SAMPLES_PER_VBL; i++) { + uint32_t index = (uint32_t)(resample(step) + 128) >> 2; + dst[2u * i] = k_ghost_pairs[index][0]; + dst[2u * i + 1u] = k_ghost_pairs[index][1]; + } +} + +/* `n` samples for the DMA chip. */ +static void produce_pcm(int8_t *dst, uint32_t n) { + uint32_t step = rate_step(CART_AUDIO_DMA_RATE_HZ); + for (uint32_t i = 0; i < n; i++) dst[i] = (int8_t)resample(step); +} + +/* --- The outputs ----------------------------------------------------------- */ + +static void write_audio_out(void) { + *((volatile uint16_t *)((uintptr_t)&__rom_in_ram_start__ + + CART_AUDIO_OUT_OFFSET)) = + (uint16_t)(s_prefer_ym ? CART_AUDIO_OUT_YM : CART_AUDIO_OUT_AUTO); +} + +/* The output for the ST that just said hello: the ST applies the same rule + * (userfw.s, .sound_ym). The writer waits while the state starts over. */ +static void select_output(uint8_t machine) { + audio_out_t out = (!s_prefer_ym && (machine >> 4) == 1u) ? AUDIO_OUT_DMA + : AUDIO_OUT_YM; + s_out = AUDIO_OUT_NONE; + __dmb(); + s_fifo_head = s_fifo_tail = 0; + s_dma_head = s_dma_tail = 0; + s_st_known = false; + s_dma_known = false; + s_hold[0] = s_hold[1] = 0; + s_dma_hold = 0; + source_restart(); + /* The ST copies the mirror before the RP has seen its first report: let + * that be silence, not the last session's sound. */ + memset(s_audio_buf, 0, CART_AUDIO_BUFFER_SIZE); + __dmb(); + s_out = out; + audioOutput = out == AUDIO_OUT_DMA ? 2u : 1u; + DPRINTF("audio: machine $%02X, %s\n", (unsigned)machine, + out == AUDIO_OUT_DMA ? "DMA sound, 12,517 Hz" : "YM, 5,585 Hz"); +} + void audio_init(void) { uint8_t *base = (uint8_t *)&__rom_in_ram_start__; s_audio_buf = base + CART_AUDIO_BUFFER_OFFSET; s_fill_cb = NULL; + s_pcm_cb = NULL; s_yms_open = false; - - /* ERASE_FIRMWARE_IN_RAM at emul_start already zeroed the cart - * buffer (= silence on YM). With no callback installed, the - * buffer stays zero until an app calls audio_play_loop() or - * audio_set_fill_callback(). */ + build_pair_pcm(); + write_audio_out(); + s_hellos_seen = st_session_hellos(); + select_output(st_session_machine()); add_repeating_timer_us(-AUDIO_WRITER_PERIOD_US, audio_writer, NULL, &s_writer_timer); - DPRINTF("audio_init: %u slices of %u B at offset $%04X, %u B/VBL, " + DPRINTF("audio_init: %u B at offset $%04X; YM %u slices of %u B, " "FIFO %u VBLs\n", + (unsigned)CART_AUDIO_BUFFER_SIZE, (unsigned)CART_AUDIO_BUFFER_OFFSET, (unsigned)CART_AUDIO_SLICES, (unsigned)CART_AUDIO_SLICE_BYTES, - (unsigned)CART_AUDIO_BUFFER_OFFSET, - (unsigned)AUDIO_FILL_BYTES_PER_VBL, (unsigned)AUDIO_FIFO_SLICES); + (unsigned)AUDIO_FIFO_SLICES); } +void audio_prefer_ym(bool prefer) { + s_prefer_ym = prefer; + write_audio_out(); + DPRINTF("audio: %s from the next ST boot\n", + prefer ? "the YM" : "the DMA chip where there is one"); +} + +bool audio_uses_dma(void) { return s_out == AUDIO_OUT_DMA; } + +/* --- Sources ----------------------------------------------------------------- */ + void audio_set_fill_callback(audio_fill_cb_t cb) { + s_pcm_cb = NULL; s_fill_cb = cb; + source_restart(); +} + +void audio_set_pcm_callback(audio_pcm_cb_t cb, uint32_t rate_hz) { + s_fill_cb = NULL; + s_pcm_rate = rate_hz != 0u ? rate_hz : CART_AUDIO_DMA_RATE_HZ; + s_pcm_cb = cb; + source_restart(); } static void audio_loop_cb(uint8_t *buf, uint32_t bytes) { @@ -160,7 +422,25 @@ void audio_play_loop(const uint8_t *data, uint32_t bytes) { s_loop_data = data; s_loop_bytes = bytes; s_loop_pos = 0; - s_fill_cb = audio_loop_cb; + audio_set_fill_callback(audio_loop_cb); +} + +static void audio_pcm_loop_cb(int8_t *buf, uint32_t samples) { + for (uint32_t i = 0; i < samples; i++) { + buf[i] = s_pcm_loop_data[s_pcm_loop_pos]; + if (++s_pcm_loop_pos >= s_pcm_loop_samples) s_pcm_loop_pos = 0; + } +} + +void audio_play_pcm_loop(const int8_t *pcm, uint32_t samples, uint32_t rate_hz) { + if (pcm == NULL || samples == 0u) { + audio_set_fill_callback(NULL); + return; + } + s_pcm_loop_data = pcm; + s_pcm_loop_samples = samples; + s_pcm_loop_pos = 0; + audio_set_pcm_callback(audio_pcm_loop_cb, rate_hz); } static void audio_yms_cb(uint8_t *buf, uint32_t bytes) { @@ -243,13 +523,17 @@ int audio_play_yms_file(const char *path) { s_yms_open = true; s_yms_data_offset = AUDIO_YMS_HEADER_SIZE; - s_fill_cb = audio_yms_cb; + audio_set_fill_callback(audio_yms_cb); DPRINTF("audio_play_yms_file: '%s' streaming (rate %lu Hz, mode %u)\n", path, (unsigned long)rate, (unsigned)hdr[12]); return 0; } +/* --- The writers (timer interrupt) -------------------------------------------- */ + +static bool source_set(void) { return s_fill_cb != NULL || s_pcm_cb != NULL; } + /* Write the slice for the ST's VBL number `vbl`: one VBL of samples from * the FIFO, then the last sample held to the end of the slice (Timer-B may * read a sample past a VBL's worth). An empty FIFO holds the last sample @@ -266,7 +550,7 @@ static void __not_in_flash_func(audio_write_slice)(uint32_t vbl) { s_hold[1] = src[AUDIO_FILL_BYTES_PER_VBL - 1u]; s_fifo_tail = tail + 1u; filled = AUDIO_FILL_BYTES_PER_VBL; - } else if (s_fill_cb != NULL) { + } else if (source_set()) { audioUnderruns++; } for (uint32_t i = filled; i < CART_AUDIO_SLICE_BYTES; i += 2u) { @@ -276,25 +560,42 @@ static void __not_in_flash_func(audio_write_slice)(uint32_t vbl) { audioSlicesWritten++; } -/* The slice writer, every AUDIO_WRITER_PERIOD_US in a timer interrupt: find - * the slice the ST plays from its latest report, and write the - * AUDIO_SLICES_AHEAD slices after it. Everything it touches is in RAM. */ -static bool __not_in_flash_func(audio_writer)(repeating_timer_t *timer) { - (void)timer; - uint16_t report; - if (!commemul_latest(CART_ROM3_WINDOW_MASK, CART_ROM3_AUDIO_SLICE_WINDOW, - AUDIO_REPORT_LOOKBACK, &report)) { - return true; /* the ST is not playing: nothing to follow */ +/* The latest report in `window`, and whether it is new since the last + * call (`fresh`). */ +typedef enum { + REPORT_NONE, /* none in the ring: the ST is not playing */ + REPORT_STALE, /* none new for AUDIO_REPORT_TIMEOUT_US: the ST left userfw */ + REPORT_OK, +} report_t; + +static report_t __not_in_flash_func(latest_report)(uint16_t window, + uint16_t *report, + bool *fresh) { + if (!commemul_latest(CART_ROM3_WINDOW_MASK, window, AUDIO_REPORT_LOOKBACK, + report)) { + return REPORT_NONE; } uint32_t now_us = time_us_32(); - if (report != s_last_report) { - s_last_report = report; + *fresh = *report != s_last_report; + if (*fresh) { + s_last_report = *report; s_last_report_us = now_us; } else if (now_us - s_last_report_us > AUDIO_REPORT_TIMEOUT_US) { - s_st_known = false; /* stale: the ST stopped playing */ - return true; + return REPORT_STALE; } + return REPORT_OK; +} +/* The YM: find the slice the ST plays from its latest report, and write the + * AUDIO_SLICES_AHEAD slices after it. */ +static void __not_in_flash_func(ym_writer)(void) { + uint16_t report; + bool fresh; + report_t r = latest_report(CART_ROM3_AUDIO_SLICE_WINDOW, &report, &fresh); + if (r != REPORT_OK) { + if (r == REPORT_STALE) s_st_known = false; /* the ST stopped playing */ + return; + } uint32_t slice = report & (CART_AUDIO_SLICES - 1u); if (!s_st_known) { s_st_known = true; @@ -315,17 +616,102 @@ static bool __not_in_flash_func(audio_writer)(repeating_timer_t *timer) { audio_write_slice(s_next_vbl); s_next_vbl++; } +} + +/* The DMA chip: at each report the ST has just copied the mirror up to + * CART_AUDIO_DMA_LEAD ahead of where the chip plays; keep writing it + * AUDIO_DMA_AHEAD ahead of where it plays now, from the FIFO or, when that + * is empty, the last sample held. Byte i of the ring is byte i ^ 1 of the + * buffer. */ +static void __not_in_flash_func(dma_writer)(void) { + uint16_t report; + bool fresh; + report_t r = latest_report(CART_ROM3_DMA_POS_WINDOW, &report, &fresh); + if (r != REPORT_OK) { + if (r == REPORT_STALE) s_dma_known = false; + return; + } + uint32_t play = (report & 0xFFu) * 8u; + if (!s_dma_known) { + s_dma_known = true; + s_dma_front = (play + CART_AUDIO_DMA_LEAD) & AUDIO_DMA_RING_MASK; + } else if (fresh) { + audioSlicesWritten++; + uint32_t ahead = (s_dma_front - play) & AUDIO_DMA_RING_MASK; + if (ahead < CART_AUDIO_DMA_LEAD || ahead > CART_AUDIO_DMA_RING_BYTES / 2u) { + /* The ST copied what the RP had not written yet. */ + audioLateSlices++; + s_dma_front = (play + CART_AUDIO_DMA_LEAD) & AUDIO_DMA_RING_MASK; + } + } + uint32_t since_us = time_us_32() - s_last_report_us; + if (since_us > AUDIO_DMA_EST_MAX_US) since_us = AUDIO_DMA_EST_MAX_US; + uint32_t now_at = play + since_us * CART_AUDIO_DMA_RATE_HZ / 1000000u; + uint32_t target = (now_at + AUDIO_DMA_AHEAD) & AUDIO_DMA_RING_MASK; + uint32_t n = (target - s_dma_front) & AUDIO_DMA_RING_MASK; + if (n > CART_AUDIO_DMA_RING_BYTES / 2u) return; /* already that far */ + bool held = false; + uint32_t front = s_dma_front; + uint32_t tail = s_dma_tail; + uint32_t head = s_dma_head; + for (uint32_t i = 0; i < n; i++) { + if (tail != head) { + s_dma_hold = s_dma_fifo[tail % AUDIO_DMA_FIFO_BYTES]; + tail++; + } else { + held = true; + } + s_audio_buf[front ^ 1u] = (uint8_t)s_dma_hold; + front = (front + 1u) & AUDIO_DMA_RING_MASK; + } + s_dma_tail = tail; + s_dma_front = front; + if (held && source_set()) audioUnderruns++; +} + +/* Every AUDIO_WRITER_PERIOD_US in a timer interrupt. Everything it touches + * is in RAM. */ +static bool __not_in_flash_func(audio_writer)(repeating_timer_t *timer) { + (void)timer; + audio_out_t out = s_out; + if (out == AUDIO_OUT_YM) { + ym_writer(); + } else if (out == AUDIO_OUT_DMA) { + dma_writer(); + } return true; } +/* --- The main loop's part ------------------------------------------------------ */ + void audio_render_frame(void) { - if (s_fill_cb == NULL) { + /* A new ST session: the output it plays through. */ + uint32_t hellos = st_session_hellos(); + if (hellos != s_hellos_seen) { + s_hellos_seen = hellos; + select_output(st_session_machine()); + } + if (!source_set()) { + return; + } + if (s_out == AUDIO_OUT_DMA) { + /* Top up the FIFO a chunk at a time: the writer reads only the samples + * below s_dma_head. */ + while (AUDIO_DMA_FIFO_BYTES - (s_dma_head - s_dma_tail) >= AUDIO_PCM_CHUNK) { + int8_t chunk[AUDIO_PCM_CHUNK]; + produce_pcm(chunk, AUDIO_PCM_CHUNK); + uint32_t head = s_dma_head; + for (uint32_t i = 0; i < AUDIO_PCM_CHUNK; i++) { + s_dma_fifo[(head + i) % AUDIO_DMA_FIFO_BYTES] = chunk[i]; + } + __dmb(); + s_dma_head = head + AUDIO_PCM_CHUNK; + } return; } /* Top up the FIFO: the writer reads only the slices below s_fifo_head. */ while (s_fifo_head - s_fifo_tail < AUDIO_FIFO_SLICES) { - s_fill_cb(s_fifo[s_fifo_head % AUDIO_FIFO_SLICES], - AUDIO_FILL_BYTES_PER_VBL); + produce_ym(s_fifo[s_fifo_head % AUDIO_FIFO_SLICES]); __dmb(); s_fifo_head = s_fifo_head + 1u; } diff --git a/rp/src/include/audio.h b/rp/src/include/audio.h index 288cc0c..8dd3616 100644 --- a/rp/src/include/audio.h +++ b/rp/src/include/audio.h @@ -1,49 +1,50 @@ /** * File: audio.h - * Description: Cart-shared audio buffer producer + app-facing API. + * Description: The app's sound, through the YM or the DMA sound chip. * - * The m68k Timer-B IRQ (target/atarist/src/userfw.s) reads sample - * bytes from the cart buffer at CART_AUDIO_BUFFER_OFFSET, one slice per - * VBL, and writes them to YM2149 volume registers. The RP writes the - * slices ahead of the one playing from a timer interrupt, so the sound - * never tears and never repeats, however long the app takes to draw a - * frame: audio_render_frame() only keeps a small FIFO topped up, from the - * main loop, with the app's audio (audio.c). + * Two outputs, picked at every ST boot (audio.c): + * - on an STE or a Mega STE, the DMA sound chip: 8-bit samples at + * 12,517 Hz, no interrupt per sample, from a ring in ST RAM that the ST + * fills every VBL from the cart buffer; + * - elsewhere (or when the app prefers it: audio_prefer_ym()), the YM: the + * m68k Timer-B IRQ (target/atarist/src/userfw.s) plays (vA, vB) volume + * pairs from the cart buffer at 5,585 Hz, one slice per VBL. + * Either way the RP writes ahead of what the ST plays from a timer + * interrupt, so the sound never tears and never repeats, however long the + * app takes to draw a frame: audio_render_frame() only keeps a small FIFO + * topped up, from the main loop, with the app's audio. * * Latency and stalls: the main loop may go AUDIO_FIFO_SLICES VBLs (4: * 80 ms by default) between two calls to audio_render_frame() without the - * sound noticing, and a sample plays at most AUDIO_FIFO_SLICES + 2 VBLs - * (120 ms) after the callback produced it. An app that wants less latency - * defines a smaller AUDIO_FIFO_SLICES (at least 1) when it builds audio.c, - * and tolerates shorter stalls. When the FIFO runs dry the ST holds the - * last sample, and audioUnderruns counts it (readable over SWD). + * sound noticing. A sample plays at most AUDIO_FIFO_SLICES + 2 VBLs + * (120 ms) after the source produced it on the YM, and about 150 ms on + * the DMA chip, whose ring stays a few VBLs ahead. An app that wants less + * latency defines a smaller AUDIO_FIFO_SLICES (at least 1) when it builds + * audio.c, and tolerates shorter stalls. When the FIFO runs dry the output + * holds the last sample, and audioUnderruns counts it (readable over SWD). * - * Apps install audio content one of two ways: + * The app's audio is YM pairs or 8-bit PCM; either plays on both outputs, + * converted by the RP (the two-channel table both ways, linear resampling): * - * 1. audio_play_loop(data, bytes) -- convenience wrapper. The - * library installs a built-in callback that loops the given - * static buffer indefinitely. Typical use case for a baked-in - * jingle or sound effect. + * 1. audio_play_loop(data, bytes) / audio_play_yms_file(path) / + * audio_set_fill_callback(cb): YM pairs, 2 bytes per sample (vA, vB) + * at 5,585 Hz, as tools/wav_to_ym4.py --mode dual-ghost writes them. + * The YM plays them as they are; the DMA chip plays the 6-bit samples + * they stand for. + * 2. audio_play_pcm_loop(pcm, samples, rate) / + * audio_set_pcm_callback(cb, rate): signed 8-bit PCM at any rate. The + * DMA chip plays it at 12,517 Hz (unchanged at that rate); the YM plays + * its upper 6 bits through the same table. * - * 2. audio_set_fill_callback(cb) -- low-level. The library - * invokes `cb(buf, bytes)` once per VBL of samples it needs, - * with `bytes` set to the m68k's per-VBL consumption; the - * callback writes exactly that many bytes into `buf`. Use for - * streaming sources (e.g. SD-backed PCM). - * - * Sample format is whatever the m68k Timer-B handler expects -- - * the default handler in userfw.s reads 2 bytes per sample - * (vA, vB) in dual-channel Ghostbusters-LUT mode. The library is - * format-agnostic; it just copies bytes into the cart buffer. - * - * If no callback is installed, audio_render_frame() is a no-op and - * the slices hold the last sample played (zero after boot = silence). + * With no source installed, audio_render_frame() only follows the ST, and + * the output holds the last sample played (zero after boot = silence). * Calling audio_set_fill_callback(NULL) re-enters this silent state. */ #ifndef AUDIO_H_INCLUDED #define AUDIO_H_INCLUDED +#include #include #ifdef __cplusplus @@ -64,6 +65,27 @@ typedef void (*audio_fill_cb_t)(uint8_t *buf, uint32_t bytes); * installed callback. Call once during boot. */ void audio_init(void); +/* The DMA chip's rate, and the signed 8-bit sample callback: write exactly + * `samples` samples into `buf` (the library asks for a few dozen at a + * time). Called from audio_render_frame(), as for audio_fill_cb_t. */ +#define AUDIO_DMA_RATE_HZ 12517u +typedef void (*audio_pcm_cb_t)(int8_t *buf, uint32_t samples); + +/* Install a PCM source at `rate_hz` (AUDIO_DMA_RATE_HZ plays unchanged on + * the DMA chip); replaces any other source. */ +void audio_set_pcm_callback(audio_pcm_cb_t cb, uint32_t rate_hz); + +/* Loop `samples` signed 8-bit samples at `rate_hz`; `pcm` must stay live + * while it plays. Replaces any other source. */ +void audio_play_pcm_loop(const int8_t *pcm, uint32_t samples, uint32_t rate_hz); + +/* Keep to the YM even on a machine with the DMA chip, from the next ST + * boot (the ST picks its output when it boots). Default: false. */ +void audio_prefer_ym(bool prefer); + +/* True when the ST plays through the DMA chip (as of its last boot). */ +bool audio_uses_dma(void); + /* Top up the FIFO through the fill callback (if one is installed). Call * once per main-loop iteration; more often does no harm. The slices * themselves are written from a timer interrupt, on the ST's VBLs. */ diff --git a/rp/src/include/cart_shared.h b/rp/src/include/cart_shared.h index e04d803..b8271d0 100644 --- a/rp/src/include/cart_shared.h +++ b/rp/src/include/cart_shared.h @@ -44,12 +44,13 @@ * previous iteration) * $FA4010 SHARED_VARIABLES 240 B (60 indexed 4-byte slots, * app-free, but for slots 2..5: - * commands for the IKBD, and - * slots 12..19: the palette). - * $FA4100 AUDIO_BUFFER 1024 B (YM volume pairs) - * $FA4500 BOOT_STATUS 2 B (read once by pre_auto: 0 = start) - * $FA4502 BOOT_MESSAGE 126 B (why the RP refused to start) - * $FA4580 APP_FREE ~15.4 KB free arena, ends at FRAMEBUFFER + * commands for the IKBD, slot 6: + * the audio output, and slots + * 12..19: the palette). + * $FA4100 AUDIO_BUFFER 2048 B (YM volume pairs, or DMA samples) + * $FA4900 BOOT_STATUS 2 B (read once by pre_auto: 0 = start) + * $FA4902 BOOT_MESSAGE 126 B (why the RP refused to start) + * $FA4980 APP_FREE ~14.4 KB free arena, ends at FRAMEBUFFER * $FA8300 FRAMEBUFFER 32 KB (320x200 4 bpp low-res) * $FAFFFF end of region */ @@ -75,6 +76,15 @@ #define CART_IKBD_OUT_MAX (CART_IKBD_OUT_SIZE - 4) #define CART_IKBD_OUT_BUSY 0x8000u +/* Which way the ST plays the sound, as the RP asks: a word in slot 6 of + * SHARED_VARIABLES, read once per ST boot. CART_AUDIO_OUT_AUTO (0, what + * the window's erase leaves) takes the DMA sound chip on an STE or a Mega + * STE and the YM elsewhere; CART_AUDIO_OUT_YM keeps to the YM (audio.c). */ +#define CART_AUDIO_OUT_OFFSET \ + (CART_SHARED_VARIABLES_OFFSET + (6 * 4)) /* $4028 */ +#define CART_AUDIO_OUT_AUTO 0u +#define CART_AUDIO_OUT_YM 1u + /* 16-entry ST palette published by the RP, applied by the m68k VBL * handler to $FFFF8240..$FFFF825E each frame. Format: 16 contiguous * 16-bit words. Each word is the standard ST 9-bit palette format @@ -92,14 +102,25 @@ #define CART_PALETTE_ENTRIES 16 #define CART_PALETTE_SIZE (CART_PALETTE_ENTRIES * 2) /* 32 B */ -/* Audio sample buffer: (vA, vB) YM2149 volume pairs, two bytes per - * sample for channels A and B. The m68k Timer-B IRQ handler fires at - * ~5,585 Hz and reads one pair per fire; its VBL handler points the read - * cursor at the start of the next slice every VBL (below), so a frame reads - * the first ~224 bytes of one slice. */ +/* Audio sample buffer, used one of two ways (audio.c): + * - the YM: (vA, vB) YM2149 volume pairs, two bytes per sample for + * channels A and B. The m68k Timer-B IRQ handler fires at ~5,585 Hz and + * reads one pair per fire; its VBL handler points the read cursor at the + * start of the next slice every VBL (below), so a frame reads the first + * ~224 bytes of one slice; + * - the DMA sound chip of an STE or a Mega STE: the whole buffer mirrors + * the ring of 8-bit signed samples the chip plays from ST RAM at + * 12,517 Hz. Every VBL the ST reports where the chip plays + * (CART_ROM3_DMA_POS_WINDOW) and copies the mirror up to + * CART_AUDIO_DMA_LEAD bytes ahead of it; the RP writes further ahead. + * Byte i of the ring is byte i ^ 1 of the RP's buffer (the cart bus + * swaps the bytes of each word). */ #define CART_AUDIO_BUFFER_OFFSET \ (CART_SHARED_VARIABLES_OFFSET + (CART_SHARED_VARIABLES_SLOTS * 4)) -#define CART_AUDIO_BUFFER_SIZE 1024 +#define CART_AUDIO_BUFFER_SIZE 2048 +#define CART_AUDIO_DMA_RING_BYTES CART_AUDIO_BUFFER_SIZE +#define CART_AUDIO_DMA_LEAD 768 +#define CART_AUDIO_DMA_RATE_HZ 12517u /* The buffer is CART_AUDIO_SLICES slices of one VBL each. The ST's VBL * handler moves Timer-B to the next slice and reports which one through * CART_ROM3_AUDIO_SLICE_WINDOW; the RP writes only the slices after it @@ -221,7 +242,8 @@ * $FB8Axx TOS version, low byte; sent just before the hello * $FB8Bxx when the blit ended, in Timer-B counts after the VBL, high byte * ($FFFF: after the next VBL); userfw.s FB_SLACK_REPORT (fb.c) - * $FB8Cxx the same, low byte; sent just after the high byte */ + * $FB8Cxx the same, low byte; sent just after the high byte + * $FB8Dxx VBL: the DMA sound chip plays byte xx * 8 of its ring (audio.c) */ #define CART_ROM3_WINDOW_MASK 0xFF00u #define CART_ROM3_IKBD_COUNT_WINDOW 0x8300u #define CART_ROM3_BLIT_DONE_WINDOW 0x8400u @@ -233,6 +255,7 @@ #define CART_ROM3_TOS_LO_WINDOW 0x8A00u #define CART_ROM3_FB_SLACK_HI_WINDOW 0x8B00u #define CART_ROM3_FB_SLACK_LO_WINDOW 0x8C00u +#define CART_ROM3_DMA_POS_WINDOW 0x8D00u /* The cart bus byte-swaps WITHIN each 16-bit word: RP stores LE, * m68k reads BE, and the swap makes that transparent for uint16_t. diff --git a/rp/src/include/st_session.h b/rp/src/include/st_session.h index f851e12..77aeeb7 100644 --- a/rp/src/include/st_session.h +++ b/rp/src/include/st_session.h @@ -42,6 +42,10 @@ bool st_session_consume_boot(void); uint8_t st_session_machine(void); uint16_t st_session_tos_version(void); +/* The hellos seen since the RP started: a module that must start over with + * each ST boot compares it with the count it last saw (audio.c). */ +uint32_t st_session_hellos(void); + /* Refuses to start the app on the ST: pre_auto prints `reason` and returns to * GEM, as it does in high resolution. At most CART_BOOT_MESSAGE_SIZE - 1 * characters; "\r\n" breaks a line, and the ST adds one at the end. The ST diff --git a/rp/src/include/target_firmware.h b/rp/src/include/target_firmware.h index fbb4475..1f16e26 100644 --- a/rp/src/include/target_firmware.h +++ b/rp/src/include/target_firmware.h @@ -1,8 +1,8 @@ const uint16_t target_firmware[] __attribute__((aligned(4))) = { - 0xABCD, 0xEF42, 0x0000, 0x0000, 0x08FA, 0x001E, 0x0000, 0x0000, 0x4EC1, 0x5D3E, 0x0000, 0x03BC, 0x5445, 0x524D, 0x0000, 0x3F3C, + 0xABCD, 0xEF42, 0x0000, 0x0000, 0x08FA, 0x001E, 0x0000, 0x0000, 0x55C7, 0x5D3E, 0x0000, 0x03BC, 0x5445, 0x524D, 0x0000, 0x3F3C, 0x0002, 0x4E4E, 0x548F, 0x2440, 0x45EA, 0xF000, 0x264A, 0x2C3C, 0x0000, 0x0394, 0x43F9, 0x00FA, 0x0046, 0xE44E, 0x5346, 0x24D9, 0x51CE, 0xFFFC, 0x4ED3, 0x2C40, 0x0038, 0x0008, 0x0484, 0x3F3C, 0x0004, 0x4E4E, 0x548F, 0xB07C, 0x0002, 0x6700, 0x0034, 0x4A79, - 0x00FA, 0x4500, 0x6606, 0x4EF9, 0x00FA, 0x0800, 0x4879, 0x00FA, 0x4502, 0x3F3C, 0x0009, 0x4E41, 0x5C8F, 0x4879, 0x00FA, 0x008C, + 0x00FA, 0x4900, 0x6606, 0x4EF9, 0x00FA, 0x0800, 0x4879, 0x00FA, 0x4902, 0x3F3C, 0x0009, 0x4E41, 0x5C8F, 0x4879, 0x00FA, 0x008C, 0x3F3C, 0x0009, 0x4E41, 0x5C8F, 0x6000, 0x008A, 0x0D0A, 0x0000, 0x4879, 0x00FA, 0x00A2, 0x3F3C, 0x0009, 0x4E41, 0x5C8F, 0x6000, 0x0074, 0x4869, 0x6768, 0x2072, 0x6573, 0x6F6C, 0x7574, 0x696F, 0x6E20, 0x2836, 0x3430, 0x7834, 0x3030, 0x2920, 0x6E6F, 0x7420, 0x7375, 0x7070, 0x6F72, 0x7465, 0x642E, 0x0D0A, 0x5377, 0x6974, 0x6368, 0x2074, 0x6F20, 0x6C6F, 0x7720, 0x6F72, 0x206D, 0x6564, @@ -66,208 +66,218 @@ const uint16_t target_firmware[] __attribute__((aligned(4))) = { 0x41F9, 0x00FC, 0x0002, 0x0C78, 0x00FC, 0x0004, 0x6706, 0x41F9, 0x00E0, 0x0002, 0x3210, 0x7000, 0x1001, 0xE049, 0x41F9, 0x00FB, 0x8900, 0x4A30, 0x1000, 0x41F9, 0x00FB, 0x8A00, 0x4A30, 0x0000, 0x7000, 0x2238, 0x05A0, 0x6726, 0x2041, 0x2218, 0x6720, 0x0C81, 0x5F4D, 0x4348, 0x6704, 0x5848, 0x60F0, 0x2210, 0x2001, 0x4840, 0xE948, 0xE849, 0x0241, 0x000F, 0x8041, 0x0240, 0x00FF, 0x41F9, - 0x00FB, 0x8800, 0x4A30, 0x0000, 0x3F3C, 0x0002, 0x4E4E, 0x548F, 0x23C0, 0x0007, 0x7FE8, 0x23F8, 0x0070, 0x0007, 0x7FE0, 0x41FA, - 0x1846, 0x21C8, 0x0070, 0x2A79, 0x0007, 0x7FE8, 0x4BED, 0xFFE0, 0x40E7, 0x007C, 0x0700, 0x2B78, 0x0068, 0x0000, 0x2B78, 0x0110, - 0x0004, 0x2B78, 0x0114, 0x0008, 0x2B78, 0x0118, 0x000C, 0x2B78, 0x0120, 0x0010, 0x2B78, 0x0134, 0x0014, 0x1B78, 0xFA07, 0x0018, - 0x1B78, 0xFA09, 0x0019, 0x1B78, 0xFA13, 0x001A, 0x1B78, 0xFA15, 0x001B, 0x1B78, 0xFA17, 0x001C, 0x41FA, 0x1880, 0x21C8, 0x0068, - 0x21C8, 0x0110, 0x21C8, 0x0114, 0x21C8, 0x0118, 0x21C8, 0x0120, 0x21C8, 0x0134, 0x4238, 0xFA1B, 0x4238, 0xFA19, 0x4238, 0xFA07, - 0x4238, 0xFA09, 0x4238, 0xFA13, 0x4238, 0xFA15, 0x0838, 0x0001, 0xFC00, 0x67F8, 0x11FC, 0x0080, 0xFC02, 0x0838, 0x0001, 0xFC00, - 0x67F8, 0x11FC, 0x0001, 0xFC02, 0x203C, 0x0006, 0x1A80, 0x0838, 0x0000, 0xFC00, 0x670E, 0x1238, 0xFC02, 0x0201, 0x00FE, 0x0C01, - 0x00F0, 0x6704, 0x5380, 0x66E6, 0x203C, 0x0000, 0x8CA0, 0x5380, 0x66FC, 0x33FC, 0xFFFF, 0x0007, 0x7FF6, 0x33FC, 0xFFFF, 0x0007, - 0x7FF8, 0x4279, 0x0007, 0x7FFA, 0x43FA, 0x17BA, 0x21C9, 0x0118, 0x11FC, 0x0096, 0xFC00, 0x11FC, 0x0015, 0xFC04, 0x08F8, 0x0006, - 0xFA09, 0x08F8, 0x0006, 0xFA15, 0x11FC, 0x0007, 0x8800, 0x11FC, 0x00FC, 0x8802, 0x11FC, 0x0000, 0x8800, 0x11FC, 0x0000, 0x8802, - 0x11FC, 0x0001, 0x8800, 0x11FC, 0x0000, 0x8802, 0x11FC, 0x0002, 0x8800, 0x11FC, 0x0000, 0x8802, 0x11FC, 0x0003, 0x8800, 0x11FC, - 0x0000, 0x8802, 0x11FC, 0x0009, 0x8800, 0x11FC, 0x0000, 0x8802, 0x11FC, 0x0008, 0x8800, 0x11FC, 0x0000, 0x8802, 0x41FA, 0x1730, - 0x21C8, 0x0120, 0x102D, 0x001C, 0x0200, 0x00F7, 0x11C0, 0xFA17, 0x11FC, 0x006E, 0xFA21, 0x11FC, 0x0001, 0xFA1B, 0x4279, 0x0007, - 0x7FF4, 0x207C, 0x00FA, 0x4100, 0x08F8, 0x0000, 0xFA07, 0x08F8, 0x0000, 0xFA13, 0x46DF, 0x23FC, 0x0007, 0x0000, 0x0007, 0x7FEC, - 0x33F9, 0x00FA, 0x400C, 0x0007, 0x7FF0, 0x11FC, 0x0007, 0x8201, 0x46FC, 0x2300, 0x33FC, 0xFFFF, 0x0007, 0x7FF2, 0x4E72, 0x2300, - 0x4A79, 0x0007, 0x7FF2, 0x66F4, 0x4BF9, 0x00FA, 0x4040, 0x4CD5, 0x00FF, 0x48F8, 0x00FF, 0x8240, 0x3039, 0x00FA, 0x400C, 0xB079, - 0x0007, 0x7FF0, 0x6700, 0x150E, 0x33C0, 0x0007, 0x7FF0, 0x2A79, 0x0007, 0x7FEC, 0x4BED, 0x7CE0, 0x2C7C, 0x00FA, 0x8300, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, - 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x00FF, 0x48ED, 0x00FF, 0x7CE0, 0x11F9, 0x0007, 0x7FEE, 0x8203, - 0x200D, 0x0A80, 0x0000, 0x8000, 0x23C0, 0x0007, 0x7FEC, 0x4A39, 0x00FB, 0x8400, 0x3039, 0x00FA, 0x4018, 0x0800, 0x000F, 0x666E, - 0xB079, 0x0007, 0x7FF6, 0x670C, 0x33C0, 0x0007, 0x7FF6, 0x4279, 0x0007, 0x7FF8, 0x3239, 0x0007, 0x7FF8, 0x6B52, 0x3439, 0x00FA, - 0x401A, 0xB47C, 0x000C, 0x6302, 0x740C, 0xB242, 0x642A, 0x0838, 0x0001, 0xFC00, 0x6738, 0x43F9, 0x00FA, 0x401C, 0x1431, 0x1000, - 0xB079, 0x00FA, 0x4018, 0x6626, 0x4A02, 0x6704, 0x11C2, 0xFC02, 0x5279, 0x0007, 0x7FF8, 0x6016, 0x33FC, 0xFFFF, 0x0007, 0x7FF8, - 0xC07C, 0x00FF, 0x43F9, 0x00FB, 0x8700, 0x4A31, 0x0000, 0x2039, 0x00FA, 0x4000, 0xB0BC, 0x0000, 0x0001, 0x6700, 0x00AE, 0xB0BC, - 0x0000, 0x0002, 0x6600, 0xEA2E, 0x007C, 0x0700, 0x2A79, 0x0007, 0x7FE8, 0x4BED, 0xFFE0, 0x4238, 0xFA1B, 0x4238, 0xFA19, 0x11ED, + 0x00FB, 0x8800, 0x4A30, 0x0000, 0x33C0, 0x0007, 0x7F80, 0x3F3C, 0x0002, 0x4E4E, 0x548F, 0x23C0, 0x0007, 0x7FE8, 0x23F8, 0x0070, + 0x0007, 0x7FE0, 0x41FA, 0x198C, 0x21C8, 0x0070, 0x2A79, 0x0007, 0x7FE8, 0x4BED, 0xFFE0, 0x40E7, 0x007C, 0x0700, 0x2B78, 0x0068, + 0x0000, 0x2B78, 0x0110, 0x0004, 0x2B78, 0x0114, 0x0008, 0x2B78, 0x0118, 0x000C, 0x2B78, 0x0120, 0x0010, 0x2B78, 0x0134, 0x0014, + 0x1B78, 0xFA07, 0x0018, 0x1B78, 0xFA09, 0x0019, 0x1B78, 0xFA13, 0x001A, 0x1B78, 0xFA15, 0x001B, 0x1B78, 0xFA17, 0x001C, 0x41FA, + 0x19C6, 0x21C8, 0x0068, 0x21C8, 0x0110, 0x21C8, 0x0114, 0x21C8, 0x0118, 0x21C8, 0x0120, 0x21C8, 0x0134, 0x4238, 0xFA1B, 0x4238, + 0xFA19, 0x4238, 0xFA07, 0x4238, 0xFA09, 0x4238, 0xFA13, 0x4238, 0xFA15, 0x0838, 0x0001, 0xFC00, 0x67F8, 0x11FC, 0x0080, 0xFC02, + 0x0838, 0x0001, 0xFC00, 0x67F8, 0x11FC, 0x0001, 0xFC02, 0x203C, 0x0006, 0x1A80, 0x0838, 0x0000, 0xFC00, 0x670E, 0x1238, 0xFC02, + 0x0201, 0x00FE, 0x0C01, 0x00F0, 0x6704, 0x5380, 0x66E6, 0x203C, 0x0000, 0x8CA0, 0x5380, 0x66FC, 0x33FC, 0xFFFF, 0x0007, 0x7FF6, + 0x33FC, 0xFFFF, 0x0007, 0x7FF8, 0x4279, 0x0007, 0x7FFA, 0x43FA, 0x1900, 0x21C9, 0x0118, 0x11FC, 0x0096, 0xFC00, 0x11FC, 0x0015, + 0xFC04, 0x08F8, 0x0006, 0xFA09, 0x08F8, 0x0006, 0xFA15, 0x11FC, 0x0007, 0x8800, 0x11FC, 0x00FC, 0x8802, 0x11FC, 0x0000, 0x8800, + 0x11FC, 0x0000, 0x8802, 0x11FC, 0x0001, 0x8800, 0x11FC, 0x0000, 0x8802, 0x11FC, 0x0002, 0x8800, 0x11FC, 0x0000, 0x8802, 0x11FC, + 0x0003, 0x8800, 0x11FC, 0x0000, 0x8802, 0x11FC, 0x0009, 0x8800, 0x11FC, 0x0000, 0x8802, 0x11FC, 0x0008, 0x8800, 0x11FC, 0x0000, + 0x8802, 0x41FA, 0x1876, 0x21C8, 0x0120, 0x102D, 0x001C, 0x0200, 0x00F7, 0x11C0, 0xFA17, 0x4279, 0x0007, 0x7FF4, 0x207C, 0x00FA, + 0x4100, 0x4279, 0x0007, 0x7F82, 0x0C79, 0x0001, 0x00FA, 0x4028, 0x6700, 0x0092, 0x3039, 0x0007, 0x7F80, 0xE848, 0x0C40, 0x0001, + 0x6600, 0x0082, 0x4238, 0x8901, 0x43F9, 0x0006, 0xF800, 0x303C, 0x01FF, 0x4299, 0x51C8, 0xFFFC, 0x203C, 0x0006, 0xF800, 0x11C0, + 0x8907, 0xE088, 0x11C0, 0x8905, 0xE088, 0x11C0, 0x8903, 0x203C, 0x0007, 0x0000, 0x11C0, 0x8913, 0xE088, 0x11C0, 0x8911, 0xE088, + 0x11C0, 0x890F, 0x11FC, 0x0081, 0x8921, 0x43FA, 0x1858, 0x7205, 0x343C, 0x07D0, 0x0C78, 0x07FF, 0x8924, 0x6704, 0x51CA, 0xFFF6, + 0x31FC, 0x07FF, 0x8924, 0x31D9, 0x8922, 0x51C9, 0xFFE4, 0x4279, 0x0007, 0x7F84, 0x11FC, 0x0003, 0x8901, 0x33FC, 0xFFFF, 0x0007, + 0x7F82, 0x6018, 0x11FC, 0x006E, 0xFA21, 0x11FC, 0x0001, 0xFA1B, 0x08F8, 0x0000, 0xFA07, 0x08F8, 0x0000, 0xFA13, 0x46DF, 0x23FC, + 0x0007, 0x0000, 0x0007, 0x7FEC, 0x33F9, 0x00FA, 0x400C, 0x0007, 0x7FF0, 0x11FC, 0x0007, 0x8201, 0x46FC, 0x2300, 0x33FC, 0xFFFF, + 0x0007, 0x7FF2, 0x4E72, 0x2300, 0x4A79, 0x0007, 0x7FF2, 0x66F4, 0x4A79, 0x0007, 0x7F82, 0x6700, 0x0084, 0x7000, 0x1038, 0x890B, + 0x1238, 0x890D, 0xB038, 0x890B, 0x66F0, 0xE148, 0x1001, 0x907C, 0xF800, 0xC07C, 0x07F8, 0x3200, 0xE649, 0x43F9, 0x00FB, 0x8D00, + 0x4A31, 0x1000, 0xD07C, 0x0300, 0xC07C, 0x07FF, 0x3239, 0x0007, 0x7F84, 0x33C0, 0x0007, 0x7F84, 0x3400, 0x9441, 0xC47C, 0x07FF, + 0xB47C, 0x0400, 0x630E, 0x3200, 0x927C, 0x0300, 0xC27C, 0x07FF, 0x343C, 0x0300, 0xE64A, 0x5342, 0x6B22, 0x45F9, 0x00FA, 0x4100, + 0x47F9, 0x0006, 0xF800, 0x27B2, 0x1000, 0x1000, 0x27B2, 0x1004, 0x1004, 0x5041, 0xC27C, 0x07FF, 0x51CA, 0xFFEC, 0x4BF9, 0x00FA, + 0x4040, 0x4CD5, 0x00FF, 0x48F8, 0x00FF, 0x8240, 0x3039, 0x00FA, 0x400C, 0xB079, 0x0007, 0x7FF0, 0x6700, 0x150E, 0x33C0, 0x0007, + 0x7FF0, 0x2A79, 0x0007, 0x7FEC, 0x4BED, 0x7CE0, 0x2C7C, 0x00FA, 0x8300, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, 0xFF78, 0x4CDE, 0x1EFF, 0x48E5, + 0xFF78, 0x4CDE, 0x00FF, 0x48ED, 0x00FF, 0x7CE0, 0x11F9, 0x0007, 0x7FEE, 0x8203, 0x200D, 0x0A80, 0x0000, 0x8000, 0x23C0, 0x0007, + 0x7FEC, 0x4A39, 0x00FB, 0x8400, 0x3039, 0x00FA, 0x4018, 0x0800, 0x000F, 0x666E, 0xB079, 0x0007, 0x7FF6, 0x670C, 0x33C0, 0x0007, + 0x7FF6, 0x4279, 0x0007, 0x7FF8, 0x3239, 0x0007, 0x7FF8, 0x6B52, 0x3439, 0x00FA, 0x401A, 0xB47C, 0x000C, 0x6302, 0x740C, 0xB242, + 0x642A, 0x0838, 0x0001, 0xFC00, 0x6738, 0x43F9, 0x00FA, 0x401C, 0x1431, 0x1000, 0xB079, 0x00FA, 0x4018, 0x6626, 0x4A02, 0x6704, + 0x11C2, 0xFC02, 0x5279, 0x0007, 0x7FF8, 0x6016, 0x33FC, 0xFFFF, 0x0007, 0x7FF8, 0xC07C, 0x00FF, 0x43F9, 0x00FB, 0x8700, 0x4A31, + 0x0000, 0x2039, 0x00FA, 0x4000, 0xB0BC, 0x0000, 0x0001, 0x6700, 0x00BA, 0xB0BC, 0x0000, 0x0002, 0x6600, 0xE9A2, 0x007C, 0x0700, + 0x4A79, 0x0007, 0x7F82, 0x6704, 0x4238, 0x8901, 0x2A79, 0x0007, 0x7FE8, 0x4BED, 0xFFE0, 0x4238, 0xFA1B, 0x4238, 0xFA19, 0x11ED, 0x0018, 0xFA07, 0x11ED, 0x0019, 0xFA09, 0x11ED, 0x001A, 0xFA13, 0x11ED, 0x001B, 0xFA15, 0x11ED, 0x001C, 0xFA17, 0x21ED, 0x0000, 0x0068, 0x21ED, 0x0004, 0x0110, 0x21ED, 0x0008, 0x0114, 0x21ED, 0x000C, 0x0118, 0x21ED, 0x0010, 0x0120, 0x21ED, 0x0014, 0x0134, 0x21F9, 0x0007, 0x7FE0, 0x0070, 0x0838, 0x0001, 0xFC00, 0x67F8, 0x11FC, 0x0014, 0xFC02, 0x0838, 0x0001, 0xFC00, 0x67F8, 0x11FC, 0x0008, 0xFC02, 0x11FC, 0x0095, 0xFC04, 0x46FC, 0x2300, 0x3F3C, 0xFFFF, 0x2F39, 0x0007, 0x7FE8, 0x2F39, 0x0007, 0x7FE8, 0x3F3C, - 0x0005, 0x4E4E, 0x4FEF, 0x000C, 0x4E75, 0x46FC, 0x2700, 0x43FA, 0x00AC, 0x45F9, 0x0007, 0x7F00, 0x700D, 0x34D9, 0x51C8, 0xFFFC, - 0x4EF9, 0x0007, 0x7F00, 0x46FC, 0x2700, 0x2F00, 0x7000, 0x1039, 0x0007, 0x7FFB, 0x207C, 0x00FB, 0x8300, 0x4A30, 0x0000, 0x3039, - 0x0007, 0x7FF4, 0x5240, 0xC07C, 0x0003, 0x33C0, 0x0007, 0x7FF4, 0x207C, 0x00FB, 0x8600, 0x4A30, 0x0000, 0xE148, 0x207C, 0x00FA, - 0x4100, 0xD0C0, 0x201F, 0x4279, 0x0007, 0x7FF2, 0x4E73, 0x11D8, 0x8802, 0x11FC, 0x0009, 0x8800, 0x11D8, 0x8802, 0x11FC, 0x0008, - 0x8800, 0x4E73, 0x2F00, 0x2F09, 0x7000, 0x1038, 0xFC00, 0x0800, 0x0000, 0x6720, 0x0800, 0x0005, 0x6706, 0x4A39, 0x00FB, 0x8500, - 0x1038, 0xFC02, 0x43F9, 0x00FB, 0x8200, 0x4A31, 0x0000, 0x5279, 0x0007, 0x7FFA, 0x225F, 0x201F, 0x4E73, 0x4E73, 0x203C, 0x000F, - 0xFFFF, 0x5380, 0x66FC, 0x42B8, 0x0420, 0x42B8, 0x043A, 0x42B8, 0x051A, 0x2078, 0x0004, 0x4ED0, 0x4E71, 0x4E71, 0x4E71, 0x4E71, - 0x4E71, 0x4E71, 0x4E71, 0x4E71 + 0x0005, 0x4E4E, 0x4FEF, 0x000C, 0x4E75, 0x46FC, 0x2700, 0x4A79, 0x0007, 0x7F82, 0x6704, 0x4238, 0x8901, 0x43FA, 0x00AC, 0x45F9, + 0x0007, 0x7F00, 0x700D, 0x34D9, 0x51C8, 0xFFFC, 0x4EF9, 0x0007, 0x7F00, 0x46FC, 0x2700, 0x2F00, 0x7000, 0x1039, 0x0007, 0x7FFB, + 0x207C, 0x00FB, 0x8300, 0x4A30, 0x0000, 0x3039, 0x0007, 0x7FF4, 0x5240, 0xC07C, 0x0003, 0x33C0, 0x0007, 0x7FF4, 0x207C, 0x00FB, + 0x8600, 0x4A30, 0x0000, 0xE148, 0x207C, 0x00FA, 0x4100, 0xD0C0, 0x201F, 0x4279, 0x0007, 0x7FF2, 0x4E73, 0x11D8, 0x8802, 0x11FC, + 0x0009, 0x8800, 0x11D8, 0x8802, 0x11FC, 0x0008, 0x8800, 0x4E73, 0x2F00, 0x2F09, 0x7000, 0x1038, 0xFC00, 0x0800, 0x0000, 0x6720, + 0x0800, 0x0005, 0x6706, 0x4A39, 0x00FB, 0x8500, 0x1038, 0xFC02, 0x43F9, 0x00FB, 0x8200, 0x4A31, 0x0000, 0x5279, 0x0007, 0x7FFA, + 0x225F, 0x201F, 0x4E73, 0x4E73, 0x203C, 0x000F, 0xFFFF, 0x5380, 0x66FC, 0x42B8, 0x0420, 0x42B8, 0x043A, 0x42B8, 0x051A, 0x2078, + 0x0004, 0x4ED0, 0x04E8, 0x0554, 0x0514, 0x0486, 0x0446, 0x0401, 0x4E71, 0x4E71, 0x4E71, 0x4E71, 0x4E71, 0x4E71, 0x4E71, 0x4E71 }; uint16_t target_firmware_length = sizeof(target_firmware) / sizeof(target_firmware[0]); diff --git a/rp/src/st_session.c b/rp/src/st_session.c index d08c9db..e616ec6 100644 --- a/rp/src/st_session.c +++ b/rp/src/st_session.c @@ -56,6 +56,8 @@ bool st_session_consume_boot(void) { uint8_t st_session_machine(void) { return s_machine; } +uint32_t st_session_hellos(void) { return stSessionHellos; } + uint16_t st_session_tos_version(void) { return s_tos_version; } void st_session_veto_boot(const char *reason) { diff --git a/target/atarist/src/inc/sidecart_layout.s b/target/atarist/src/inc/sidecart_layout.s index 3385048..6b04402 100644 --- a/target/atarist/src/inc/sidecart_layout.s +++ b/target/atarist/src/inc/sidecart_layout.s @@ -14,11 +14,12 @@ ; $FA400C FB_FRAME_COUNTER_ADDR 4 B bumped as the last write of a publish ; $FA4010 SHARED_VARIABLES 240 B (60 x 4-byte slots, the app's) ; $FA4018 IKBD_OUT_ADDR 16 B (slots 2..5: IKBD commands from the RP) +; $FA4028 AUDIO_OUT_ADDR 2 B (slot 6: the RP keeps the ST to the YM) ; $FA4040 PALETTE_ADDR 32 B (slots 12..19: 16 palette words) -; $FA4100 AUDIO_BUFFER_ADDR 1024 B (YM volume pairs) -; $FA4500 BOOT_STATUS_ADDR 2 B read once in pre_auto: 0 = start -; $FA4502 BOOT_MESSAGE_ADDR 126 B why the RP refused; printed before GEM -; $FA4580 APP_FREE_ADDR ~15.4 KB for the app, up to the framebuffer +; $FA4100 AUDIO_BUFFER_ADDR 2048 B (YM volume pairs, or DMA samples) +; $FA4900 BOOT_STATUS_ADDR 2 B read once in pre_auto: 0 = start +; $FA4902 BOOT_MESSAGE_ADDR 126 B why the RP refused; printed before GEM +; $FA4980 APP_FREE_ADDR ~14.4 KB for the app, up to the framebuffer ; $FA8300 FRAMEBUFFER_ADDR 32000 B (320x200 4bpp, flush at the top) ; $FAFFFF end of the window @@ -52,26 +53,40 @@ IKBD_OUT_BUSY_BIT equ 15 PALETTE_ADDR equ (SHARED_BLOCK_ADDR + $40) ; $FA4040 PALETTE_SIZE equ 32 ; 16 words -; Audio: (vA, vB) YM volume pairs, one pair per Timer-B interrupt, in -; AUDIO_SLICES slices of one VBL each. The VBL handler points A0 at the next -; slice and tells the RP which one (AUDIO_SLICE_WINDOW); the RP writes the -; slices ahead of it, never the one playing. +; Audio, two ways. On an STE or a Mega STE the DMA sound chip plays 8-bit +; samples at 12,517 Hz from a ring in ST RAM (userfw's UFW_DMA_RING), which +; userfw fills every VBL from the audio buffer, its mirror: the ST reports +; where the chip plays (DMA_POS_WINDOW), copies the mirror from its last copy +; to AUDIO_DMA_LEAD bytes ahead of that, and the RP writes the mirror further +; ahead still. Elsewhere Timer-B plays (vA, vB) YM volume pairs, one pair per +; interrupt, from the first AUDIO_SLICES slices of the buffer, one VBL each: +; the VBL handler points A0 at the next slice and tells the RP which one +; (AUDIO_SLICE_WINDOW); the RP writes the slices ahead of it. AUDIO_BUFFER_ADDR equ (SHARED_BLOCK_ADDR + $100) ; $FA4100 -AUDIO_BUFFER_SIZE equ 1024 -AUDIO_BUFFER_END equ (AUDIO_BUFFER_ADDR + AUDIO_BUFFER_SIZE) ; $FA4500 +AUDIO_BUFFER_SIZE equ 2048 +AUDIO_BUFFER_END equ (AUDIO_BUFFER_ADDR + AUDIO_BUFFER_SIZE) ; $FA4900 AUDIO_SLICES equ 4 AUDIO_SLICE_BYTES equ 256 ; one VBL is 224 of them at ~5,585 Hz AUDIO_SLICE_SHIFT equ 8 ; log2(AUDIO_SLICE_BYTES) +AUDIO_DMA_RING_BYTES equ AUDIO_BUFFER_SIZE ; 8 VBLs at 12,517 Hz +AUDIO_DMA_LEAD equ 768 ; 3 VBLs: a missed VBL still finds samples + +; Which way the ST plays the sound, as the RP asks (a word, read once at +; boot): AUDIO_OUT_AUTO takes the DMA chip when the machine has one, +; AUDIO_OUT_YM keeps to the YM. Slot 6 of SHARED_VARIABLES. +AUDIO_OUT_ADDR equ (SHARED_VARIABLES + (6 * 4)) ; $FA4028 +AUDIO_OUT_AUTO equ 0 +AUDIO_OUT_YM equ 1 ; Boot block. The RP can refuse to start the app (st_session_veto_boot()): a ; non-zero status makes pre_auto print the NUL-terminated message and return ; to GEM. -BOOT_STATUS_ADDR equ AUDIO_BUFFER_END ; $FA4500 -BOOT_MESSAGE_ADDR equ (BOOT_STATUS_ADDR + 2) ; $FA4502 +BOOT_STATUS_ADDR equ AUDIO_BUFFER_END ; $FA4900 +BOOT_MESSAGE_ADDR equ (BOOT_STATUS_ADDR + 2) ; $FA4902 BOOT_MESSAGE_SIZE equ 126 ; The app's own buffers, up to the framebuffer. -APP_FREE_ADDR equ (BOOT_MESSAGE_ADDR + BOOT_MESSAGE_SIZE) ; $FA4580 +APP_FREE_ADDR equ (BOOT_MESSAGE_ADDR + BOOT_MESSAGE_SIZE) ; $FA4980 ; 320x200 low resolution, 4 bitplanes, at the top of the window so that an ; overrun walks off its end rather than over the block above. @@ -107,3 +122,4 @@ ST_TOS_HI_WINDOW equ (ROMCMD_START_ADDR + $8900) ; + TOS version, high b ST_TOS_LO_WINDOW equ (ROMCMD_START_ADDR + $8A00) ; + TOS version, low byte FB_SLACK_HI_WINDOW equ (ROMCMD_START_ADDR + $8B00) ; + when the blit ended, high byte (FB_SLACK_REPORT) FB_SLACK_LO_WINDOW equ (ROMCMD_START_ADDR + $8C00) ; + when the blit ended, low byte +DMA_POS_WINDOW equ (ROMCMD_START_ADDR + $8D00) ; + where the DMA chip plays, / 8 diff --git a/target/atarist/src/userfw.s b/target/atarist/src/userfw.s index 3e6ecca..de02ca8 100644 --- a/target/atarist/src/userfw.s +++ b/target/atarist/src/userfw.s @@ -79,7 +79,8 @@ FBDRV_DEBUG_MARKS equ 0 ; ROM3 reads (FB_SLACK_HI_WINDOW, FB_SLACK_LO_WINDOW), $FFFF when the next ; VBL came first. Debug builds of the RP keep a histogram of the slack left ; before the VBL (fb.c). The report itself costs about 30 us after each -; blit: keep it 0 unless measuring. +; blit: keep it 0 unless measuring. It counts Timer-B's interrupts: the YM +; path only, not the DMA sound chip's. FB_SLACK_REPORT equ 0 ; Per-VBL state area at the end of SCREEN_A's 32 KB allocation. @@ -91,7 +92,13 @@ FB_SLACK_REPORT equ 0 ; screen page, allocation is 32 KB). ; UFW_RESET_STUB: .cold_reset copies userfw_reset_stub here and runs it, ; so the ST's last instructions before its cold reset come from RAM. -UFW_RESET_STUB equ $00077F00 ; up to $77FDF +UFW_RESET_STUB equ $00077F00 ; up to $77F7F +; The machine from the hello (family in bits 7..4), whether the DMA sound +; chip plays (-1) or Timer-B and the YM (0), and where the last copy into +; the DMA ring ended (see AUDIO_BUFFER_ADDR, UFW_DMA_RING). +UFW_MACHINE equ $00077F80 ; word +UFW_AUDIO_DMA equ $00077F82 ; word +UFW_DMA_FRONT equ $00077F84 ; word: an offset in the ring UFW_VBL_VEC_SAVE equ $00077FE0 ; longword: TOS VBL vector ($70) UFW_PHYSBASE_SAVE equ $00077FE8 ; longword: XBIOS Physbase result UFW_SCREEN_PAGE equ $00077FEC ; longword: current draw page address @@ -341,6 +348,30 @@ MFP_TBDR equ $FFFFFA21 ; Timer-B data register (8-bit coun TIMERB_PRESCALER equ 1 ; /4 (delay mode) TIMERB_COUNT equ 110 ; ~5,585 Hz (~112 samples/PAL VBL) +; STE / Mega STE DMA sound. The chip plays 8-bit samples from ST RAM only, +; so userfw keeps a ring of AUDIO_DMA_RING_BYTES there, the second use of +; Atari RAM after the screen pages and on the same assumption: the 2 KB just +; below screen page A. The chip loops over it at 12,517 Hz mono; every VBL +; userfw copies into it, from the audio buffer that mirrors it, what the RP +; has written ahead of the chip (see AUDIO_BUFFER_ADDR). +DMA_SND_CTRL equ $FFFF8901 ; bit 0 play, bit 1 loop +DMA_SND_START equ $FFFF8903 ; frame start: high, mid (+2), low (+4) +DMA_SND_COUNT_MID equ $FFFF890B ; where it plays (read only): middle byte +DMA_SND_COUNT_LOW equ $FFFF890D ; and low byte +DMA_SND_END equ $FFFF890F ; frame end: high, mid (+2), low (+4) +DMA_SND_MODE equ $FFFF8921 ; bits 1-0 rate, bit 7 mono +DMA_MODE_PLAY equ $81 ; mono, 12,517 Hz +DMA_CTRL_LOOP_PLAY equ 3 +UFW_DMA_RING equ $0006F800 ; AUDIO_DMA_RING_BYTES, up to $6FFFF +MACHINE_FAMILY_STE equ $1 ; the hello byte's family: STE, Mega STE +; The LMC1992 behind the Microwire: the DMA sound goes through it. userfw +; sets what TOS sets at boot, in case something changed it: master, left +; and right at 0 dB, bass and treble flat, the YM mixed in (GEM's bell). +MW_DATA equ $FFFF8922 +MW_MASK equ $FFFF8924 +MW_MASK_ALL equ $07FF ; also the mask at rest: a transfer ended +MW_WAIT_ITERS equ 2000 ; a transfer takes 16 us + ; IRQ vector slots we take over. $70 (VBL) already handled by the ; original userfw code path (D3 holds the save). VEC_HBL equ $68 @@ -460,6 +491,7 @@ userfw: .hello_send: lea ST_HELLO_WINDOW, a0 tst.b (a0, d0.w) + move.w d0, UFW_MACHINE ; Save the original screen base so we can restore it on ESC exit. move.w #2, -(sp) ; XBIOS Physbase @@ -601,8 +633,9 @@ userfw: ; --- Timer-B setup (audio @ ~5,585 Hz) ----------------------- ; Install our handler at $120 (overrides the dummy installed - ; above). Load count -> TBDR, then prescaler -> TBCR starts - ; the countdown. Enable + unmask Timer-B at the MFP. SR is + ; above). On the YM path (.sound_ym below) count -> TBDR, then + ; prescaler -> TBCR starts the countdown, and Timer-B is enabled and + ; unmasked at the MFP; the DMA path leaves it stopped. SR is ; still IPL=7 at this point (set by `ori.w #$0700, sr` at the ; very top of userfw), so no IRQ fires until SR is dropped to ; $2300 below. @@ -617,8 +650,6 @@ userfw: move.b 28(a5), d0 ; copy TOS's VR (saved above) andi.b #$F7, d0 ; clear bit 3 (S) -> auto-EOI move.b d0, MFP_VR.w - move.b #TIMERB_COUNT, MFP_TBDR.w - move.b #TIMERB_PRESCALER, MFP_TBCR.w ; Initialise A0 to the first audio slice for the Timer-B handler. ; A0 is NOT in the FBDRV_INLINE MOVEM list and no other code in @@ -629,8 +660,61 @@ userfw: clr.w UFW_AUDIO_SLICE movea.l #AUDIO_BUFFER_ADDR, a0 + ; The sound goes out through the DMA chip on an STE or a Mega STE, + ; unless the RP keeps us to the YM (AUDIO_OUT_ADDR); through Timer-B and + ; the YM elsewhere. The RP applies the same rule to what it writes. + clr.w UFW_AUDIO_DMA + cmpi.w #AUDIO_OUT_YM, AUDIO_OUT_ADDR + beq .sound_ym + move.w UFW_MACHINE, d0 + lsr.w #4, d0 + cmpi.w #MACHINE_FAMILY_STE, d0 + bne .sound_ym + + ; DMA: stopped while it is set up; the ring silent (signed 0); the + ; frame is the whole ring, looped; the mixer as TOS sets it; play. + clr.b DMA_SND_CTRL.w + lea UFW_DMA_RING, a1 + move.w #(AUDIO_DMA_RING_BYTES / 4) - 1, d0 +.dma_clear: + clr.l (a1)+ + dbf d0, .dma_clear + move.l #UFW_DMA_RING, d0 + move.b d0, DMA_SND_START+4.w + lsr.l #8, d0 + move.b d0, DMA_SND_START+2.w + lsr.l #8, d0 + move.b d0, DMA_SND_START.w + move.l #(UFW_DMA_RING + AUDIO_DMA_RING_BYTES), d0 + move.b d0, DMA_SND_END+4.w + lsr.l #8, d0 + move.b d0, DMA_SND_END+2.w + lsr.l #8, d0 + move.b d0, DMA_SND_END.w + move.b #DMA_MODE_PLAY, DMA_SND_MODE.w + lea userfw_mw_cmds(pc), a1 + moveq #((userfw_mw_cmds_end - userfw_mw_cmds) / 2) - 1, d1 +.mw_next: + move.w #MW_WAIT_ITERS, d2 +.mw_wait: + cmpi.w #MW_MASK_ALL, MW_MASK.w ; the last transfer is over + beq.s .mw_send + dbf d2, .mw_wait +.mw_send: + move.w #MW_MASK_ALL, MW_MASK.w + move.w (a1)+, MW_DATA.w + dbf d1, .mw_next + clr.w UFW_DMA_FRONT + move.b #DMA_CTRL_LOOP_PLAY, DMA_SND_CTRL.w + move.w #-1, UFW_AUDIO_DMA + bra.s .sound_done + +.sound_ym: + move.b #TIMERB_COUNT, MFP_TBDR.w + move.b #TIMERB_PRESCALER, MFP_TBCR.w bset #0, MFP_IERA.w ; Timer-B IRQ enable (IERA bit 0) bset #0, MFP_IMRA.w ; Timer-B IRQ unmask (IMRA bit 0) +.sound_done: ; Interrupts back on (caller's level, typically $2300). move.w (sp)+, sr @@ -675,6 +759,55 @@ userfw: move.w #BLIT_MARK_VSYNC, PALETTE_IDX0.w ; border = vsync mark endc + ; DMA sound (see AUDIO_BUFFER_ADDR): where the chip plays now, by + ; 8 bytes, for the RP; then the mirror copied into the ring from where + ; the last copy ended up to AUDIO_DMA_LEAD bytes ahead of the chip. The + ; counter moves while it is read: its middle byte is read again. A + ; frontier out of step (the first VBL, a long stall) copies the lead. + ; About 0.35 ms at 8 MHz; Timer-B, off on this path, took 1.6 ms. + tst.w UFW_AUDIO_DMA + beq .dma_done +.dma_pos: + moveq #0, d0 + move.b DMA_SND_COUNT_MID.w, d0 + move.b DMA_SND_COUNT_LOW.w, d1 + cmp.b DMA_SND_COUNT_MID.w, d0 + bne.s .dma_pos + lsl.w #8, d0 + move.b d1, d0 + sub.w #(UFW_DMA_RING & $FFFF), d0 + and.w #(AUDIO_DMA_RING_BYTES - 8), d0 + move.w d0, d1 + lsr.w #3, d1 + lea DMA_POS_WINDOW, a1 + tst.b (a1, d1.w) + add.w #AUDIO_DMA_LEAD, d0 + and.w #(AUDIO_DMA_RING_BYTES - 1), d0 ; copy up to here + move.w UFW_DMA_FRONT, d1 ; from where the last copy ended + move.w d0, UFW_DMA_FRONT + move.w d0, d2 + sub.w d1, d2 + and.w #(AUDIO_DMA_RING_BYTES - 1), d2 + cmp.w #(AUDIO_DMA_RING_BYTES / 2), d2 + bls.s .dma_count + move.w d0, d1 + sub.w #AUDIO_DMA_LEAD, d1 + and.w #(AUDIO_DMA_RING_BYTES - 1), d1 + move.w #AUDIO_DMA_LEAD, d2 +.dma_count: + lsr.w #3, d2 + subq.w #1, d2 + bmi.s .dma_done + lea AUDIO_BUFFER_ADDR, a2 + lea UFW_DMA_RING, a3 +.dma_copy: + move.l 0(a2, d1.w), 0(a3, d1.w) + move.l 4(a2, d1.w), 4(a3, d1.w) + addq.w #8, d1 + and.w #(AUDIO_DMA_RING_BYTES - 1), d1 + dbf d2, .dma_copy +.dma_done: + ; Publish RP-supplied palette to the shifter. 16 words ; from PALETTE_ADDR -> $FFFF8240..$FFFF825E via two MOVEMs. ; Cost: 76 (load) + 72 (store) + 16 (lea) = ~164 cyc / VBL = @@ -848,6 +981,12 @@ userfw: ; Mask interrupts before touching MFP / vectors. ori.w #$0700, sr + ; The DMA sound chip stops; the Microwire stays as TOS sets it. + tst.w UFW_AUDIO_DMA + beq.s .exit_sound + clr.b DMA_SND_CTRL.w +.exit_sound: + ; Recompute the save-area pointer from UFW_PHYSBASE_SAVE in ; case anything clobbered A5 during the run. movea.l UFW_PHYSBASE_SAVE, a5 @@ -904,6 +1043,10 @@ userfw: ; and every vector userfw took, so nothing is restored by hand. .cold_reset: move.w #$2700, sr + tst.w UFW_AUDIO_DMA + beq.s .reset_sound + clr.b DMA_SND_CTRL.w ; nothing plays from a ring TOS reuses +.reset_sound: lea userfw_reset_stub(pc), a1 lea UFW_RESET_STUB, a2 moveq #((userfw_reset_stub_end - userfw_reset_stub) / 2) - 1, d0 @@ -1058,6 +1201,17 @@ userfw_reset_stub: jmp (a0) userfw_reset_stub_end: +; The Microwire commands userfw sends on the DMA path (see MW_DATA): +; %10 then a 3-bit command and its value. +userfw_mw_cmds: + dc.w $04E8 ; master volume 0 dB + dc.w $0554 ; left 0 dB + dc.w $0514 ; right 0 dB + dc.w $0486 ; treble flat + dc.w $0446 ; bass flat + dc.w $0401 ; mix: the YM with the DMA sound +userfw_mw_cmds_end: + ; The NOP tail. This module is the last in the cartridge image, and ; firmware.py strips trailing zero bytes from it: the last word must be ; harmless to drop or to prefetch past. An app that includes diff --git a/tests/host/test_audio.c b/tests/host/test_audio.c index c2378aa..3e2181b 100644 --- a/tests/host/test_audio.c +++ b/tests/host/test_audio.c @@ -32,6 +32,10 @@ static int st_report = -1; /* the slice of the latest report, -1: none */ uint32_t time_us_32(void) { return now_us; } +/* A plain ST said hello: the YM output. */ +uint32_t st_session_hellos(void) { return 0; } +uint8_t st_session_machine(void) { return 0; } + bool add_repeating_timer_us(int64_t delay_us, repeating_timer_callback_t callback, void *user_data, repeating_timer_t *out) { out->delay_us = delay_us; diff --git a/tests/host/test_audio_dma.c b/tests/host/test_audio_dma.c new file mode 100644 index 0000000..663a762 --- /dev/null +++ b/tests/host/test_audio_dma.c @@ -0,0 +1,426 @@ +// units: rp/src/audio.c +/* The DMA sound output (audio.c) against a simulated STE, and the + * conversions between YM pairs and PCM. + * + * The STE plays its ring at 250.4 samples a VBL (12,517 Hz); at every VBL + * userfw reports where it plays, rounded to 8 bytes, and copies the cart + * buffer, the ring's mirror, up to CART_AUDIO_DMA_LEAD bytes ahead (byte i + * of the ring is byte i ^ 1 of the RP's buffer). Everything the chip plays + * must be the source in order, silence before it starts, or, when the FIFO + * ran dry, the last sample held: never a stale or a skipped sample. */ +#include + +#include "audio.h" +#include "cart_shared.h" +#include "ff.h" +#include "pico/time.h" +#include "test.h" + +#define VBL_US 20000u +#define WRITER_RUNS_PER_VBL 5 +#define RING CART_AUDIO_DMA_RING_BYTES +#define MASK (RING - 1u) +/* 12,517 Hz x 20.0 ms: the chip's samples per VBL, 16.16. */ +#define CHIP_PER_VBL_Q16 16410214u + +unsigned int __rom_in_ram_start__[0x10000 / sizeof(unsigned int)]; +extern uint32_t audioSlicesWritten, audioUnderruns, audioLateSlices, audioOutput; + +/* --- The RP's world ------------------------------------------------------- */ + +static uint32_t now_us; +static repeating_timer_t *writer; +static int dma_report = -1; /* the latest DMA position report / 8 */ +static int slice_report = -1; /* the latest audio slice report */ +static uint32_t hellos; +static uint8_t machine; + +uint32_t time_us_32(void) { return now_us; } +uint32_t st_session_hellos(void) { return hellos; } +uint8_t st_session_machine(void) { return machine; } + +bool add_repeating_timer_us(int64_t delay_us, repeating_timer_callback_t callback, + void *user_data, repeating_timer_t *out) { + out->delay_us = delay_us; + out->callback = callback; + out->user_data = user_data; + writer = out; + return true; +} + +bool commemul_latest(uint16_t mask, uint16_t match, uint32_t max_back, + uint16_t *sample) { + (void)max_back; + if ((match & mask) == CART_ROM3_DMA_POS_WINDOW && dma_report >= 0) { + *sample = (uint16_t)(CART_ROM3_DMA_POS_WINDOW | (uint16_t)dma_report); + return true; + } + if ((match & mask) == CART_ROM3_AUDIO_SLICE_WINDOW && slice_report >= 0) { + *sample = (uint16_t)(CART_ROM3_AUDIO_SLICE_WINDOW | (uint16_t)slice_report); + return true; + } + return false; +} + +FRESULT f_open(FIL *fp, const char *path, BYTE mode) { + (void)fp, (void)path, (void)mode; + return FR_NO_FILE; +} +FRESULT f_read(FIL *fp, void *buff, UINT btr, UINT *br) { + (void)fp, (void)buff, (void)btr; + *br = 0; + return FR_DISK_ERR; +} +FRESULT f_lseek(FIL *fp, FSIZE_t ofs) { + (void)fp, (void)ofs; + return FR_OK; +} +FRESULT f_close(FIL *fp) { + (void)fp; + return FR_OK; +} + +static uint8_t *cart_buffer(void) { + return (uint8_t *)__rom_in_ram_start__ + CART_AUDIO_BUFFER_OFFSET; +} + +/* --- The STE ------------------------------------------------------------------ */ + +static int8_t ring[RING]; /* ST RAM */ +static uint32_t chip_q16; /* samples the chip has played, 16.16 */ +static uint32_t st_front; /* UFW_DMA_FRONT */ +static uint32_t st_vbl; +static int8_t played[400000]; +static uint32_t n_played; + +/* A boot of the ST: hello, ring cleared, the chip restarted. */ +static void st_boot(uint8_t m) { + machine = m; + hellos++; + audio_render_frame(); /* the RP sees the hello */ + memset(ring, 0, sizeof ring); + chip_q16 = 0; + st_front = 0; + dma_report = -1; + slice_report = -1; +} + +/* userfw at a VBL on the DMA path: report the play position, copy the + * mirror ahead. `skip`: a blit that overran, and .vbl_loop missed this VBL + * entirely: no report, no copy. */ +static void st_vbl_dma(bool skip) { + st_vbl++; + if (skip) return; + slice_report = (int)(st_vbl % CART_AUDIO_SLICES); + uint32_t p = (chip_q16 >> 16) & MASK & ~7u; + dma_report = (int)(p / 8u); + uint32_t e = (p + CART_AUDIO_DMA_LEAD) & MASK; + uint32_t f = st_front; + uint32_t n = (e - f) & MASK; + if (n > RING / 2u) { + f = (e - CART_AUDIO_DMA_LEAD) & MASK; + n = CART_AUDIO_DMA_LEAD; + } + for (uint32_t i = 0; i < n; i++) { + uint32_t at = (f + i) & MASK; + ring[at] = (int8_t)cart_buffer()[at ^ 1u]; + } + st_front = e; +} + +/* The chip plays what the ring holds, up to `until_q16`. */ +static void chip_play(uint32_t until_q16) { + while ((chip_q16 >> 16) < (until_q16 >> 16)) { + if (n_played < sizeof played) played[n_played++] = ring[(chip_q16 >> 16) & MASK]; + chip_q16 += 1u << 16; + } +} + +/* One VBL: the ST's copy, then 20 ms with the writer every 4 ms, the chip + * playing, and the main loop's top-up once unless stalled. */ +static void frame(bool main_loop, bool st_skip) { + st_vbl_dma(st_skip); + uint32_t start = chip_q16; + for (int i = 1; i <= WRITER_RUNS_PER_VBL; i++) { + now_us += VBL_US / WRITER_RUNS_PER_VBL; + chip_play(start + (uint32_t)((uint64_t)CHIP_PER_VBL_Q16 * (uint32_t)i / + WRITER_RUNS_PER_VBL)); + writer->callback(writer); + if (i == 2 && main_loop) audio_render_frame(); + } +} + +/* --- The source ----------------------------------------------------------------- */ + +#define PATTERN 3001u +static int8_t pattern[PATTERN]; + +static void make_pattern(void) { + for (uint32_t i = 0; i < PATTERN; i++) { + int8_t v = (int8_t)((int32_t)((i * 37u + i / 91u) % 241u) - 120); + pattern[i] = v != 0 ? v : 1; /* never silence: the start is findable */ + } +} + +/* Where the source starts in what was played: the first non-silent sample. */ +static uint32_t first_sound(uint32_t from) { + for (uint32_t i = from; i < n_played; i++) { + if (played[i] != 0) return i; + } + return n_played; +} + +/* Played from `at` on: the pattern from `k` in order, where a sample may + * repeat the one before it only when `holds` (an underrun). Returns how many + * samples followed the rule. */ +static uint32_t follows(uint32_t at, uint32_t k, bool holds, uint32_t *repeats) { + uint32_t i = at; + *repeats = 0; + for (; i < n_played; i++) { + if (played[i] == pattern[k % PATTERN]) { + k++; + } else if (holds && i > at && played[i] == played[i - 1]) { + (*repeats)++; + } else { + break; + } + } + return i - at; +} + +/* A new session with `source` installed: no source across the boot (a + * source keeps what it put in the FIFO), then the main loop's top-up, as + * an app's loop runs long before the ST first reports. */ +static void boot_with(uint8_t m, void (*source)(void)) { + audio_set_fill_callback(NULL); + n_played = 0; + st_boot(m); + source(); + audio_render_frame(); +} + +static void pattern_source(void) { audio_play_pcm_loop(pattern, PATTERN, AUDIO_DMA_RATE_HZ); } + +static void start(uint8_t m) { boot_with(m, pattern_source); } + +/* --- The cases -------------------------------------------------------------------- */ + +static void test_steady(void) { + start(0x11); + CHECK_EQ(audioOutput, 2); + CHECK(audio_uses_dma()); + uint32_t late0 = audioLateSlices, under0 = audioUnderruns; + for (int i = 0; i < 1000; i++) frame(true, false); + uint32_t s = first_sound(0); + CHECK(s < 5u * 251u); /* the sound starts within five VBLs */ + uint32_t repeats; + CHECK_EQ(follows(s, 0, false, &repeats), n_played - s); + CHECK_EQ(audioLateSlices, late0); + CHECK_EQ(audioUnderruns, under0); +} + +/* The main loop away for three VBLs every so often: the FIFO and the + * samples written ahead cover it. */ +static void test_stalls_covered(void) { + start(0x11); + uint32_t under0 = audioUnderruns; + for (int i = 0; i < 1000; i++) frame(i % 25 >= 3 || i < 25, false); + uint32_t s = first_sound(0), repeats; + CHECK_EQ(follows(s, 0, false, &repeats), n_played - s); + CHECK_EQ(audioUnderruns, under0); +} + +/* Away for ten VBLs: the last sample is held, counted, and the source goes + * on where it was. */ +static void test_long_stall(void) { + start(0x11); + uint32_t under0 = audioUnderruns; + for (int i = 0; i < 400; i++) frame(i < 100 || i >= 110, false); + uint32_t s = first_sound(0), repeats; + CHECK_EQ(follows(s, 0, true, &repeats), n_played - s); + CHECK(repeats > 0u); + CHECK(audioUnderruns > under0); +} + +/* A blit that overran: the ST misses one copy; three VBLs of lead cover it. */ +static void test_missed_copy(void) { + start(0x11); + uint32_t late0 = audioLateSlices; + for (int i = 0; i < 600; i++) frame(true, i % 50 == 25); + uint32_t s = first_sound(0), repeats; + CHECK_EQ(follows(s, 0, false, &repeats), n_played - s); + CHECK_EQ(audioLateSlices, late0); +} + +/* The ST resets: its reports stop, then a hello and a new ring. The sound + * comes back, in order, with nothing counted late. */ +static void test_st_reset(void) { + start(0x11); + for (int i = 0; i < 200; i++) frame(true, false); + uint32_t late0 = audioLateSlices; + /* 300 ms of reboot: no reports, the chip silent. */ + for (int i = 0; i < 15; i++) { + now_us += VBL_US; + for (int j = 0; j < WRITER_RUNS_PER_VBL; j++) writer->callback(writer); + audio_render_frame(); + } + uint32_t before = n_played; + st_boot(0x11); + for (int i = 0; i < 300; i++) frame(true, false); + uint32_t s = first_sound(before); + CHECK(s - before < 5u * 251u); + /* The source went on while the ST rebooted: find where. */ + uint32_t k = 0; + while (k < PATTERN && played[s] != pattern[k]) k++; + uint32_t best = 0, repeats; + for (uint32_t kk = k; kk < PATTERN; kk++) { + if (played[s] != pattern[kk]) continue; + uint32_t n = follows(s, kk, false, &repeats); + if (n > best) best = n; + } + CHECK_EQ(best, n_played - s); + CHECK_EQ(audioLateSlices, late0); +} + +/* YM pairs on the DMA chip: a pair of the table plays as the sample its + * index stands for; a rising run of pairs plays rising. */ +static uint8_t ym_const[224]; +static uint8_t ym_ramp[2 * 64 * 20]; +static unsigned n_ramp; + +static void ym_ramp_source(void) { audio_play_loop(ym_ramp, n_ramp); } + +static void ym_const_source(void) { audio_play_loop(ym_const, sizeof ym_const); } + +static void test_ym_source_on_dma(void) { + /* (4, 9) is entry 21 only: 21 * 4 + 2 - 128 = -42. */ + for (unsigned i = 0; i < sizeof ym_const; i += 2) { + ym_const[i] = 4; + ym_const[i + 1] = 9; + } + boot_with(0x11, ym_const_source); + for (int i = 0; i < 100; i++) frame(true, false); + uint32_t s = first_sound(0); + bool all = s < n_played; + for (uint32_t i = s + 16u; i < n_played; i++) all = all && played[i] == -42; + CHECK(all); + + /* Entries 0..54 hold distinct, rising pairs; each for 20 samples. */ + static const uint8_t k_rising[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, + 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24}; + static const uint8_t k_pairs[][2] = { + {0, 0}, {0, 2}, {1, 2}, {2, 2}, {2, 3}, {1, 4}, {2, 4}, {2, 5}, {0, 6}, + {2, 6}, {3, 6}, {4, 6}, {2, 7}, {4, 7}, {5, 7}, {2, 8}, {3, 8}, {4, 8}, + {5, 8}, {2, 9}, {3, 9}, {4, 9}, {5, 9}, {6, 9}, {7, 9}}; + unsigned n = 0; + for (unsigned r = 0; r < sizeof k_rising; r++) { + for (unsigned j = 0; j < 20; j++) { + ym_ramp[n++] = k_pairs[k_rising[r]][0]; + ym_ramp[n++] = k_pairs[k_rising[r]][1]; + } + } + n_ramp = n; + boot_with(0x11, ym_ramp_source); + for (int i = 0; i < 100; i++) frame(true, false); + /* Somewhere a full rise plays: from the end of the -126 step up to -30, + * never falling on the way. */ + bool rose = false; + for (uint32_t i = 0; i + 1u < n_played && !rose; i++) { + if (played[i] != -126 || played[i + 1u] == -126) continue; + uint32_t j = i; + while (j + 1u < n_played && played[j + 1u] >= played[j]) j++; + rose = played[j] == 24 * 4 + 2 - 128; + } + CHECK(rose); +} + +/* PCM on the YM: the slices hold the table's pairs for the samples' upper + * 6 bits, in order (5,585 Hz: no resampling). */ +static void pattern_5585_source(void) { audio_play_pcm_loop(pattern, PATTERN, 5585); } + +static void test_pcm_source_on_ym(void) { + boot_with(0x00, pattern_5585_source); + CHECK_EQ(audioOutput, 1); + CHECK(!audio_uses_dma()); + const uint8_t *buf = cart_buffer(); + for (int i = 0; i < 40; i++) { + st_vbl++; + slice_report = (int)(st_vbl % CART_AUDIO_SLICES); + for (int j = 0; j < WRITER_RUNS_PER_VBL; j++) { + now_us += VBL_US / WRITER_RUNS_PER_VBL; + writer->callback(writer); + if (j == 2) audio_render_frame(); + } + } + /* The slice written ahead: 112 pairs for 112 samples of the pattern in a + * row. Find where it starts. */ + const uint8_t *slice = buf + ((st_vbl + 1u) % CART_AUDIO_SLICES) * CART_AUDIO_SLICE_BYTES; + static const uint8_t k_lut_a[64] = {0, 0, 1, 2, 2, 1, 2, 2, 0, 2, 3, 4, 2, 4, 5, 2, + 3, 4, 5, 2, 3, 4, 5, 6, 7, 3, 4, 5, 6, 7, 0, 1, + 2, 4, 5, 6, 7, 8, 8, 9, 9, 0, 1, 2, 3, 4, 5, 6, + 8, 8, 9, 9, 9, 10, 0, 2, 3, 4, 5, 6, 7, 8, 8, 9}; + static const uint8_t k_lut_b[64] = {0, 2, 2, 2, 3, 4, 4, 5, 6, 6, 6, 6, 7, + 7, 7, 8, 8, 8, 8, 9, 9, 9, 9, 9, 9, 10, + 10, 10, 10, 10, 11, 11, 11, 11, 11, 11, 11, 11, 11, + 11, 11, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, + 12, 12, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13}; + bool found = false; + for (uint32_t k = 0; k < PATTERN && !found; k++) { + bool ok = true; + for (uint32_t i = 0; i < 112u && ok; i++) { + uint32_t index = (uint32_t)(pattern[(k + i) % PATTERN] + 128) >> 2; + ok = slice[2u * i] == k_lut_a[index] && slice[2u * i + 1u] == k_lut_b[index]; + } + found = ok; + } + CHECK(found); +} + +/* The app keeps to the YM: the word the ST reads says so, and the next + * hello on an STE picks the YM; back to the DMA chip after. */ +static void test_prefer_ym(void) { + volatile uint16_t *out = (volatile uint16_t *)((uint8_t *)__rom_in_ram_start__ + + CART_AUDIO_OUT_OFFSET); + audio_prefer_ym(true); + CHECK_EQ(*out, CART_AUDIO_OUT_YM); + st_boot(0x10); + CHECK_EQ(audioOutput, 1); + audio_prefer_ym(false); + CHECK_EQ(*out, CART_AUDIO_OUT_AUTO); + CHECK_EQ(audioOutput, 1); /* until the ST boots again */ + st_boot(0x10); + CHECK_EQ(audioOutput, 2); + st_boot(0x20); /* a TT: no DMA path here */ + CHECK_EQ(audioOutput, 1); +} + +/* A PCM source at another rate: half the DMA rate plays every sample twice, + * give or take the interpolation. */ +static int8_t flat[500]; + +static void flat_source(void) { audio_play_pcm_loop(flat, sizeof flat, 6258); } + +static void test_resampling(void) { + memset(flat, 55, sizeof flat); + boot_with(0x11, flat_source); + for (int i = 0; i < 60; i++) frame(true, false); + uint32_t s = first_sound(0); + bool all = s < n_played; + for (uint32_t i = s; i < n_played; i++) all = all && played[i] == 55; + CHECK(all); +} + +int main(void) { + make_pattern(); + audio_init(); + test_steady(); + test_stalls_covered(); + test_long_stall(); + test_missed_copy(); + test_st_reset(); + test_ym_source_on_dma(); + test_pcm_source_on_ym(); + test_prefer_ym(); + test_resampling(); + TEST_END(); +} diff --git a/tests/host/test_layout.py b/tests/host/test_layout.py index 6681afa..92b351a 100644 --- a/tests/host/test_layout.py +++ b/tests/host/test_layout.py @@ -156,6 +156,13 @@ def rom3(st, name): ("audio slices", lambda st: st["AUDIO_SLICES"], lambda rp: rp["CART_AUDIO_SLICES"]), ("audio slice size", lambda st: st["AUDIO_SLICE_BYTES"], lambda rp: rp["CART_AUDIO_SLICE_BYTES"]), + ("DMA ring", lambda st: st["AUDIO_DMA_RING_BYTES"], + lambda rp: rp["CART_AUDIO_DMA_RING_BYTES"]), + ("DMA lead", lambda st: st["AUDIO_DMA_LEAD"], lambda rp: rp["CART_AUDIO_DMA_LEAD"]), + ("audio output word", lambda st: window(st, "AUDIO_OUT_ADDR"), + lambda rp: rp["CART_AUDIO_OUT_OFFSET"]), + ("audio output: auto", lambda st: st["AUDIO_OUT_AUTO"], lambda rp: rp["CART_AUDIO_OUT_AUTO"]), + ("audio output: YM", lambda st: st["AUDIO_OUT_YM"], lambda rp: rp["CART_AUDIO_OUT_YM"]), # The boot block the ST reads in pre_auto. ("boot status", lambda st: window(st, "BOOT_STATUS_ADDR"), lambda rp: rp["CART_BOOT_STATUS_OFFSET"]), @@ -202,6 +209,8 @@ def rom3(st, name): lambda rp: rp["CART_ROM3_FB_SLACK_HI_WINDOW"]), ("blit slack low byte window", lambda st: rom3(st, "FB_SLACK_LO_WINDOW"), lambda rp: rp["CART_ROM3_FB_SLACK_LO_WINDOW"]), + ("DMA position window", lambda st: rom3(st, "DMA_POS_WINDOW"), + lambda rp: rp["CART_ROM3_DMA_POS_WINDOW"]), # The sample rate: Timer-B plays what the RP converts at its rate. ("audio sample rate (Hz)", lambda st: round( MFP_CLOCK_HZ / (MFP_PRESCALER[st["TIMERB_PRESCALER"]] * st["TIMERB_COUNT"])), @@ -233,16 +242,30 @@ def test_audio_bytes_per_frame(self): self.assertLessEqual(rp["AUDIO_FILL_BYTES_PER_VBL"], rp["CART_AUDIO_BUFFER_SIZE"]) def test_audio_slices(self): - """The slices fill the audio buffer, a VBL of samples fits in one, + """The slices fit in the audio buffer, a VBL of samples fits in one, and the ST's shift is the slice size.""" st, rp = st_names(), rp_names() - self.assertEqual(rp["CART_AUDIO_SLICES"] * rp["CART_AUDIO_SLICE_BYTES"], - rp["CART_AUDIO_BUFFER_SIZE"]) + self.assertLessEqual(rp["CART_AUDIO_SLICES"] * rp["CART_AUDIO_SLICE_BYTES"], + rp["CART_AUDIO_BUFFER_SIZE"]) self.assertLessEqual(rp["AUDIO_FILL_BYTES_PER_VBL"], rp["CART_AUDIO_SLICE_BYTES"]) self.assertEqual(1 << st["AUDIO_SLICE_SHIFT"], st["AUDIO_SLICE_BYTES"]) slices = rp["CART_AUDIO_SLICES"] self.assertEqual(slices & (slices - 1), 0, "the ST masks the slice number") + def test_dma_ring(self): + """The DMA ring masks as a power of two, the lead and the ST's copies + go by 8 bytes and stay under half a ring, and the rate the ST sets is + the one the RP converts to.""" + st, rp = st_names(), rp_names() + ring, lead = rp["CART_AUDIO_DMA_RING_BYTES"], rp["CART_AUDIO_DMA_LEAD"] + self.assertEqual(ring & (ring - 1), 0) + self.assertEqual(lead % 8, 0) + self.assertLess(lead, ring // 2) + self.assertEqual(st["UFW_DMA_RING"] % ring, 0, "the ST masks ring offsets") + rates = [6258, 12517, 25033, 50066] + self.assertEqual(rates[st["DMA_MODE_PLAY"] & 3], rp["CART_AUDIO_DMA_RATE_HZ"]) + self.assertTrue(st["DMA_MODE_PLAY"] & 0x80, "mono") + def test_rom3_windows_distinct(self): """Every ROM3 signalling window has a high byte of its own.""" rp = rp_names() From 85b652c247231b763229692bdf9c632d1296a89d Mon Sep 17 00:00:00 2001 From: Diego Parrilla Date: Wed, 30 Sep 2026 13:03:26 +0200 Subject: [PATCH 2/4] Debug commands: the audio output and a test tone swd.py app audio_out 0|1 picks, from the ST's next boot, the DMA chip where there is one or the YM, so both outputs play on one STE with a reset in between. tone HZ plays a sine through the PCM path, whose clicks are easy to hear; tone 0 goes back to DEMO.YMS. swd.py counters shows the audio output. --- rp/src/demo_menu.c | 43 +++++++++++++++++++++++++++++++++++++++++++ rp/src/include/demo.h | 2 ++ tools/dev/swd.py | 6 ++++-- 3 files changed, 49 insertions(+), 2 deletions(-) diff --git a/rp/src/demo_menu.c b/rp/src/demo_menu.c index ee779e0..600ebd4 100644 --- a/rp/src/demo_menu.c +++ b/rp/src/demo_menu.c @@ -17,6 +17,7 @@ #include +#include "audio.h" #include "cart_shared.h" #include "debug.h" #include "fb.h" @@ -435,6 +436,36 @@ void demo_dispatcher_handle_key(const ikbd_key_event_t *k) { } #if defined(_DEBUG) && (_DEBUG != 0) +/* A test tone through the PCM path (DEVHOOKS_APP_TONE): a sine, whose + * clicks and jitter are easy to hear, on either output. */ +static const int8_t k_tone_sine[256] = { + 0, 2, 5, 7, 10, 12, 15, 17, 20, 22, 24, 27, 29, 31, 34, 36, + 38, 41, 43, 45, 47, 49, 51, 53, 56, 58, 60, 62, 63, 65, 67, 69, + 71, 72, 74, 76, 77, 79, 80, 82, 83, 84, 86, 87, 88, 89, 90, 91, + 92, 93, 94, 95, 96, 96, 97, 98, 98, 99, 99, 99, 100, 100, 100, 100, + 100, 100, 100, 100, 100, 99, 99, 99, 98, 98, 97, 96, 96, 95, 94, 93, + 92, 91, 90, 89, 88, 87, 86, 84, 83, 82, 80, 79, 77, 76, 74, 72, + 71, 69, 67, 65, 63, 62, 60, 58, 56, 53, 51, 49, 47, 45, 43, 41, + 38, 36, 34, 31, 29, 27, 24, 22, 20, 17, 15, 12, 10, 7, 5, 2, + 0, -2, -5, -7, -10, -12, -15, -17, -20, -22, -24, -27, -29, -31, -34, -36, + -38, -41, -43, -45, -47, -49, -51, -53, -56, -58, -60, -62, -63, -65, -67, -69, + -71, -72, -74, -76, -77, -79, -80, -82, -83, -84, -86, -87, -88, -89, -90, -91, + -92, -93, -94, -95, -96, -96, -97, -98, -98, -99, -99, -99, -100, -100, -100, -100, + -100, -100, -100, -100, -100, -99, -99, -99, -98, -98, -97, -96, -96, -95, -94, -93, + -92, -91, -90, -89, -88, -87, -86, -84, -83, -82, -80, -79, -77, -76, -74, -72, + -71, -69, -67, -65, -63, -62, -60, -58, -56, -53, -51, -49, -47, -45, -43, -41, + -38, -36, -34, -31, -29, -27, -24, -22, -20, -17, -15, -12, -10, -7, -5, -2, +}; +static uint32_t s_tone_phase; +static uint32_t s_tone_step; + +static void tone_cb(int8_t *buf, uint32_t samples) { + for (uint32_t i = 0; i < samples; i++) { + buf[i] = k_tone_sine[s_tone_phase >> 24]; + s_tone_phase += s_tone_step; + } +} + /* Extra time every frame takes, set from the host (DEVHOOKS_APP_SLOW_FRAME): * an app late with its frames, on demand. The sound and the publish * handshake must survive it. */ @@ -465,6 +496,18 @@ uint32_t demo_dispatcher_devhook(uint16_t commandId, const uint16_t *payload, DPRINTF("dispatcher: host -> input mode %u\n", (unsigned)arg); ikbd_set_input_mode((ikbd_input_mode_t)arg); return 1; + case DEVHOOKS_APP_AUDIO_OUT: + audio_prefer_ym(arg != 0u); + return 1; + case DEVHOOKS_APP_TONE: + if (arg == 0u) { + DPRINTF("dispatcher: host -> tone off, DEMO.YMS\n"); + return audio_play_yms_file("DEMO.YMS") == 0 ? 1u : 0u; + } + DPRINTF("dispatcher: host -> tone %u Hz\n", (unsigned)arg); + s_tone_step = (uint32_t)(((uint64_t)arg << 32) / AUDIO_DMA_RATE_HZ); + audio_set_pcm_callback(tone_cb, AUDIO_DMA_RATE_HZ); + return 1; case DEVHOOKS_APP_IKBD_CMD: { uint8_t cmd[CART_IKBD_OUT_MAX]; size_t n = payloadSize / 2u; diff --git a/rp/src/include/demo.h b/rp/src/include/demo.h index c1a50b3..c713cdf 100644 --- a/rp/src/include/demo.h +++ b/rp/src/include/demo.h @@ -83,6 +83,8 @@ extern bool g_show_timing; #define DEVHOOKS_APP_SLOW_FRAME 4 /* WORD: stall every frame this many ms */ #define DEVHOOKS_APP_INPUT_MODE 5 /* WORD 0..3: ikbd_set_input_mode() */ #define DEVHOOKS_APP_IKBD_CMD 6 /* WORD...: IKBD command bytes to send */ +#define DEVHOOKS_APP_AUDIO_OUT 7 /* WORD 0 the DMA chip if any, 1 the YM: next boot */ +#define DEVHOOKS_APP_TONE 8 /* WORD Hz: a sine through the PCM path; 0: DEMO.YMS */ uint32_t demo_dispatcher_devhook(uint16_t commandId, const uint16_t *payload, uint16_t payloadSize); diff --git a/tools/dev/swd.py b/tools/dev/swd.py index df120f7..7e30e2a 100755 --- a/tools/dev/swd.py +++ b/tools/dev/swd.py @@ -72,7 +72,8 @@ released, and the bytes enter where the ST's own do, so the app cannot tell the difference. `app` sends a command named by a DEVHOOKS_APP_ define in rp/src/include, with optional 16-bit words: the demo dispatcher has `demo N`, -`menu`, `overlay 0|1`, `slow_frame MS` and `input_mode 0-3` (demo.h). +`menu`, `overlay 0|1`, `slow_frame MS`, `input_mode 0-3`, `ikbd_cmd BYTE...`, +`audio_out 0|1` and `tone HZ` (demo.h). `fb` writes the framebuffer as the ST shows it, in colour, as a PNG: it waits for the next complete frame (a watchpoint on the frame counter stops core 0 @@ -149,7 +150,8 @@ ("commOverruns", "ROM3 ring overruns"), ("audioSlicesWritten", "audio slices written"), ("audioLateSlices", "audio slices late"), - ("audioUnderruns", "audio underruns")) + ("audioUnderruns", "audio underruns"), + ("audioOutput", "audio output (1 YM, 2 DMA)")) # Counters that --watch also prints as a rate. RATE_COUNTERS = ("fb_frame_tick", "s_vbl_seen", "audioSlicesWritten", "ikbdBytes") # Variables postmortem prints when the ELF has them; a build without one From a0ec0ecb0409d646556373bc4012f9d09252e4aa Mon Sep 17 00:00:00 2001 From: Diego Parrilla Date: Wed, 30 Sep 2026 13:03:26 +0200 Subject: [PATCH 3/4] wav_to_ym4.py: signed 8-bit PCM for the DMA chip --mode pcm writes a C header of signed 8-bit samples at the target rate for audio_play_pcm_loop() (12,517 Hz plays unchanged on the DMA chip; the RP converts it for the YM). The tests also check that audio.c's copies of the two-channel table and the YM volume curve match the tool's. --- tests/host/test_wav_to_ym4.py | 52 +++++++++++++++++++++++++++++++++ tools/wav_to_ym4.py | 54 +++++++++++++++++++++++++++-------- 2 files changed, 94 insertions(+), 12 deletions(-) diff --git a/tests/host/test_wav_to_ym4.py b/tests/host/test_wav_to_ym4.py index 7d9a4d8..9c9cbad 100644 --- a/tests/host/test_wav_to_ym4.py +++ b/tests/host/test_wav_to_ym4.py @@ -5,6 +5,7 @@ import importlib.util import math import os +import re import struct import subprocess import sys @@ -45,6 +46,57 @@ def test_silence_and_clipping(self): self.assertEqual(ym._float_to_ym_dual_ghost([-5.0]), list(ym.GHOSTBUSTERS_LUT[0])) +class Pcm(unittest.TestCase): + """--mode pcm: signed 8-bit samples for audio_play_pcm_loop().""" + + def test_values(self): + self.assertEqual(ym._float_to_pcm8([0.0, 1.0, -1.0, 0.5, 9.0, -9.0]), + [0, 127, -127, 64, 127, -127]) + + def test_header(self): + with tempfile.TemporaryDirectory() as tmp: + wav = os.path.join(tmp, "in.wav") + with wave.open(wav, "wb") as w: + w.setnchannels(1) + w.setsampwidth(1) + w.setframerate(12517) + w.writeframes(bytes([128, 255, 1, 192])) + out = os.path.join(tmp, "pcm.h") + subprocess.run([sys.executable, TOOL, wav, "--mode", "pcm", "--target-rate", + "12517", "--header-output", out, "--symbol", "tone"], + check=True, capture_output=True) + text = open(out).read() + self.assertIn("static const int8_t tone[] = {", text) + self.assertIn("#define AUDIO_SAMPLE_RATE_HZ 12517u", text) + body = text[text.index("{") + 1:text.index("};")] + self.assertEqual([int(v) for v in body.replace(",", " ").split()], [0, 126, -126, 64]) + refused = subprocess.run([sys.executable, TOOL, wav, "--mode", "pcm", + "--yms-output", os.path.join(tmp, "x.yms")], + capture_output=True) + self.assertNotEqual(refused.returncode, 0) + + +class RpTables(unittest.TestCase): + """audio.c converts at runtime with copies of the tool's tables.""" + + def _c_array(self, name): + text = open(os.path.join(RP_SRC, "audio.c")).read() + body = text[text.index(name):] + body = body[body.index("{") + 1:body.index("};")] + return [int(n) for n in re.findall(r"-?\d+", body)] + + def test_pairs(self): + flat = self._c_array("k_ghost_pairs[64][2]") + self.assertEqual(list(zip(flat[0::2], flat[1::2])), [tuple(p) for p in ym.GHOSTBUSTERS_LUT]) + + def test_ym_curve(self): + amps = self._c_array("k_ym_amp[16]") + want = [0] + [round(10000 * 2.0 ** ((v - 15) / 2.0)) for v in range(1, 16)] + for got, w in zip(amps, want): + self.assertLessEqual(abs(got - w), 1) + self.assertEqual(len(amps), 16) + + class Resample(unittest.TestCase): def test_length_follows_the_rate(self): diff --git a/tools/wav_to_ym4.py b/tools/wav_to_ym4.py index 3816af1..fdc4ee3 100644 --- a/tools/wav_to_ym4.py +++ b/tools/wav_to_ym4.py @@ -213,6 +213,17 @@ def _float_to_ym_raw_byte( return out +def _float_to_pcm8(samples: list[float], pcm_gain: float = 1.0) -> list[int]: + """Signed 8-bit PCM, one byte per sample (-128..127): what the STE's + DMA sound chip plays, and what audio_play_pcm_loop() takes on the RP, + which also converts it for the YM.""" + out: list[int] = [] + for s in samples: + v = int(round(max(-1.0, min(1.0, s * pcm_gain)) * 127.0)) + out.append(max(-128, min(127, v))) + return out + + def _pack_ym_bytes(va: int, vb: int) -> list[int]: """Pack one sample as 4 bytes for the m68k MOVEP.L emit. The cart bus byte-swaps within each 16-bit word, so to make m68k's @@ -388,6 +399,7 @@ def _write_c_header( symbol: str, count_symbol: str, target_rate: int, + pcm: bool = False, ) -> None: out_path.parent.mkdir(parents=True, exist_ok=True) guard = _header_guard_from_path(out_path) @@ -397,17 +409,25 @@ def _write_c_header( out.write(f"#define {guard}\n\n") out.write("#include \n\n") out.write(f"#define AUDIO_SAMPLE_RATE_HZ {target_rate}u\n\n") - out.write( - "/* Two bytes per sample: byte 2n = channel A volume, byte 2n+1\n" - " * = channel B volume. The (A, B) pairs are precomputed so the\n" - " * summed YM acoustic amplitudes best fit the linear-biased\n" - " * PCM value (Ghostbusters-style 2-channel pseudo-DAC). */\n" - ) - out.write(f"static const uint8_t {symbol}[] = {{\n") + if pcm: + out.write( + "/* Signed 8-bit PCM, one byte per sample: play it with\n" + " * audio_play_pcm_loop(data, count, AUDIO_SAMPLE_RATE_HZ). */\n" + ) + out.write(f"static const int8_t {symbol}[] = {{\n") + else: + out.write( + "/* Two bytes per sample: byte 2n = channel A volume, byte 2n+1\n" + " * = channel B volume. The (A, B) pairs are precomputed so the\n" + " * summed YM acoustic amplitudes best fit the linear-biased\n" + " * PCM value (Ghostbusters-style 2-channel pseudo-DAC). */\n" + ) + out.write(f"static const uint8_t {symbol}[] = {{\n") per_line = 16 for start in range(0, len(samples), per_line): row = samples[start : start + per_line] - out.write(" " + ", ".join(f"0x{v:X}" for v in row) + ",\n") + fmt = (lambda v: str(v)) if pcm else (lambda v: f"0x{v:X}") + out.write(" " + ", ".join(fmt(v) for v in row) + ",\n") out.write("};\n\n") out.write( f"static const uint32_t {count_symbol} = " @@ -429,7 +449,7 @@ def parse_args() -> argparse.Namespace: p.add_argument("--target-rate", type=int, default=15350) p.add_argument("--mode", choices=["nibble", "best-pair", "ghostbusters", - "dual-ghost", "single-a", "raw-byte"], + "dual-ghost", "single-a", "raw-byte", "pcm"], default="ghostbusters", help="PCM-to-YM mapping. 'nibble': split 8-bit sample " "into high/low nibbles -> (chA, chB). 'best-pair': " @@ -442,7 +462,10 @@ def parse_args() -> argparse.Namespace: "(ch B always 0; pair with mixer R7=$FE). " "'raw-byte': 1 byte/sample = unsigned 8-bit PCM " "verbatim (no LUT, no nibble split) -- m68k " - "extracts nibbles at runtime.") + "extracts nibbles at runtime. 'pcm': signed 8-bit " + "PCM for audio_play_pcm_loop() (the STE's DMA chip " + "at 12517 Hz; the RP converts it for the YM); C " + "header only.") p.add_argument("--lut-scale", type=float, default=1.0, help="Range compression for the (A,B) pair search: " "1.0 = full YM sum range [0, 2.0] (default); " @@ -479,6 +502,10 @@ def main() -> int: print("error: at least one of --header-output / --yms-output is required", file=sys.stderr) return 1 + if args.mode == "pcm" and args.yms_output: + print("error: .YMS files hold YM pairs: --mode pcm writes a C header only", + file=sys.stderr) + return 1 suffix = args.input_path.suffix.lower() if suffix == ".sam": @@ -496,12 +523,15 @@ def main() -> int: ym4 = _float_to_ym_single_a(resampled, args.pcm_gain) elif args.mode == "raw-byte": ym4 = _float_to_ym_raw_byte(resampled, args.pcm_gain) + elif args.mode == "pcm": + ym4 = _float_to_pcm8(resampled, args.pcm_gain) else: ym4 = _float_to_ym_pair(resampled, args.lut_scale, args.pcm_gain) if args.header_output: _write_c_header( - ym4, args.header_output, args.symbol, args.count_symbol, args.target_rate + ym4, args.header_output, args.symbol, args.count_symbol, args.target_rate, + pcm=args.mode == "pcm", ) if args.yms_output: _write_yms_file(ym4, args.yms_output, args.target_rate, args.mode) @@ -510,7 +540,7 @@ def main() -> int: # single-a, raw-byte -> 1 byte # dual-ghost -> 2 bytes (vA, vB) # other -> 4 bytes (MOVEP.L layout) - if args.mode in ("single-a", "raw-byte"): + if args.mode in ("single-a", "raw-byte", "pcm"): bytes_per_sample = 1 elif args.mode == "dual-ghost": bytes_per_sample = 2 From 5a03ab8077a450af65ebd1e6b0cee6c30caf80ce Mon Sep 17 00:00:00 2001 From: Diego Parrilla Date: Wed, 30 Sep 2026 13:03:26 +0200 Subject: [PATCH 4/4] Document the STE DMA sound CLAUDE.md: the audio pipeline for two outputs (the rule both sides apply, the ring, the per-VBL copy, the writer ahead of the chip, the conversions, latency and tuning), the Atari RAM the DMA path uses, the 2 KB audio buffer and the layout after it, $FB8Dxx, and the ST's budget on the DMA path. README's audio section (PCM, the YM preference), the framebuffer-app skill, tools/dev/README.md and the changelog. --- .claude/skills/framebuffer-app/SKILL.md | 8 +++-- CHANGELOG.md | 15 ++++++++- CLAUDE.md | 41 +++++++++++++++---------- README.md | 35 +++++++++++++++++---- tools/dev/README.md | 11 +++++-- 5 files changed, 82 insertions(+), 28 deletions(-) diff --git a/.claude/skills/framebuffer-app/SKILL.md b/.claude/skills/framebuffer-app/SKILL.md index be6101f..cecbcfd 100644 --- a/.claude/skills/framebuffer-app/SKILL.md +++ b/.claude/skills/framebuffer-app/SKILL.md @@ -30,7 +30,8 @@ lifting:** a dual (page-flipped) framebuffer on the Atari ST side blank; **~19 ms of compute every VBL** to draw your frame; chunked drawing on the RP2040 (one byte per pixel) with the chunked → ST planar conversion done for you in **~1 ms per VBL** (split across both cores); -**~6 kHz, 6-bit sampled sound** out the YM2149; and the Atari ST +sampled sound (8-bit 12.5 kHz through the DMA chip on an STE / Mega STE, +~6 kHz 6-bit out the YM2149 elsewhere); and the Atari ST keyboard, mouse and joysticks handled on the RP2040, delivered to your app as key events, mouse movement and stick states. @@ -97,8 +98,11 @@ ikbd_joystick_t j; ikbd_read_joystick(1, &j); // j.state / j.pressed: IKBD_JOY_ // audio: audio.h audio_play_loop(data, bytes); // loop a baked-in buffer, OR audio_set_fill_callback(cb); // cb(buf,bytes) per VBL of samples, live +audio_play_pcm_loop(pcm, n, rate); // or signed 8-bit PCM at any rate +audio_set_pcm_callback(cb, AUDIO_DMA_RATE_HZ); // (either kind plays on both outputs) audio_render_frame(); // every loop iteration: tops up the FIFO - // (80 ms of stall allowed, 120 ms latency) + // (80 ms of stall allowed; 120 ms latency + // on the YM, ~175 ms on the DMA chip) ``` **SD card:** the microSD is already mounted at boot diff --git a/CHANGELOG.md b/CHANGELOG.md index bd3f2c3..4b659f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,19 @@ (`AUDIO_FIFO_SLICES`). When nothing is ready the last sample is held, never stale sound, and `audioUnderruns` counts it. +### STE DMA sound + +- On an STE or a Mega STE the sound plays through the DMA sound chip: 8-bit at 12,517 Hz, no + interrupt per sample. The ST copies each VBL's samples into a 2 KB ring in its RAM (below the + screen pages); Timer-B stays off, which gives the ST 1.6 ms of every frame back: a mouse moved + fast no longer costs frames there. A plain ST or Mega ST plays through the YM as before, from + the same firmware. +- Apps give 8-bit PCM at any rate (`audio_play_pcm_loop()`, `audio_set_pcm_callback()`) or YM + pairs as before; either plays on both outputs, converted on the RP. `audio_prefer_ym()` keeps + an STE on the YM; `tools/wav_to_ym4.py --mode pcm` writes PCM headers. +- The audio buffer in the cartridge window grows to 2 KB: `APP_FREE` starts at `$FA4980` + (about 14.4 KB). + ### Mouse and joysticks - Apps get the mouse and both joysticks next to the keyboard, decoded on the RP: pick an @@ -45,7 +58,7 @@ - Host tests: `make -C tests/host test` checks the pure logic on the host, and CI runs it on every pull request: the blits' clipping, the framebuffer layout from the chunky buffer to the ST's screen, the agreement of the ST's and the RP's copies of every shared constant, the - sample converter, the audio slices and the IKBD decoder. + sample converter, the audio slices, the DMA sound output and the IKBD decoder. - The cartridge window's layout lives in one m68k include, `inc/sidecart_layout.s`, which `main.s` and `userfw.s` share; `userfw.s` no longer repeats its addresses. diff --git a/CLAUDE.md b/CLAUDE.md index 5926284..ed3a8a2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -34,7 +34,7 @@ A template for building **sub-20-millisecond audiovisual Sidecartridge Multi-dev - **~19 ms of compute every VBL** for your app to draw its frame. - **Chunked drawing on the RP2040** — you write one byte per pixel; the framework does the chunked → Atari ST planar conversion for you. - **~1 ms per VBL** for that chunky→planar conversion (split across both cores). -- **~6 kHz, 6-bit sampled sound** out the YM2149. +- **Sampled sound**: 8-bit at 12.5 kHz through the DMA sound chip on an STE or a Mega STE, ~6 kHz 6-bit out the YM2149 elsewhere; one asset plays on both. - **Atari ST keyboard, mouse and joysticks handled on the RP2040** — decoded scancodes, mouse movement and stick states delivered straight to your app. Network plumbing (WiFi / lwIP / mbedTLS / httpc) was deliberately stripped — apps that need it bring it back from `md-microfirmware-template` upstream. @@ -79,9 +79,9 @@ Build flow (orchestrated by `build.sh`): - `upload_s3.sh ` is a manual one-off uploader; needs `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY`. ### Tests -**Host tests** (`tests/host/`: `make -C tests/host test`, a second or two; CI runs them before the firmware build, and a failure fails the pull request) cover the pure logic. `test_fb_blit.c`: the blits' clipping at every edge, against a pixel-by-pixel reference. `test_chunk_layout.c`: what the app draws is what the ST shows, through the RP's transpose and chunk-reversed publish and a model of the m68k's MOVEM blit. `test_layout.py`: the ST's and the RP's copies of every shared constant agree (the window, the command values, the ROM3 windows, the blit's shape, the audio rate). `test_wav_to_ym4.py`: the sample converter and the `.YMS` files the RP's reader accepts. `test_audio.c`: the audio slices against a simulated ST. `test_ikbd_demux.c`: the IKBD decoder, every packet and loss rule, then a simulated IKBD and ST with bytes lost, dropped and read twice (reported losses never turn packet data into key presses; every kind comes back in step). The C tests build firmware units unchanged with the host compiler, under the address and undefined-behaviour sanitizers; `shim/` stands in for the few SDK headers they include (core 1 is a thread). To add one: a `tests/host/test_*.c` whose first line names the firmware sources it compiles (`// units: rp/src/fb_blit.c`), with `test.h`'s `CHECK` / `CHECK_EQ` / `TEST_END`, or a `test_*.py` unittest module. A new constant both sides share gets a row in `test_layout.py`'s `PAIRS`. Everything that needs the SDK, the bus or timing is verified on hardware: the build succeeds, the UF2 boots, and the app behaves on the ST. +**Host tests** (`tests/host/`: `make -C tests/host test`, a second or two; CI runs them before the firmware build, and a failure fails the pull request) cover the pure logic. `test_fb_blit.c`: the blits' clipping at every edge, against a pixel-by-pixel reference. `test_chunk_layout.c`: what the app draws is what the ST shows, through the RP's transpose and chunk-reversed publish and a model of the m68k's MOVEM blit. `test_layout.py`: the ST's and the RP's copies of every shared constant agree (the window, the command values, the ROM3 windows, the blit's shape, the audio rate). `test_wav_to_ym4.py`: the sample converter and the `.YMS` files the RP's reader accepts. `test_audio.c`: the audio slices against a simulated ST. `test_audio_dma.c`: the DMA sound output against a simulated STE (steady play, stalls, a missed VBL, an ST reset, YM data and PCM on both outputs). `test_ikbd_demux.c`: the IKBD decoder, every packet and loss rule, then a simulated IKBD and ST with bytes lost, dropped and read twice (reported losses never turn packet data into key presses; every kind comes back in step). The C tests build firmware units unchanged with the host compiler, under the address and undefined-behaviour sanitizers; `shim/` stands in for the few SDK headers they include (core 1 is a thread). To add one: a `tests/host/test_*.c` whose first line names the firmware sources it compiles (`// units: rp/src/fb_blit.c`), with `test.h`'s `CHECK` / `CHECK_EQ` / `TEST_END`, or a `test_*.py` unittest module. A new constant both sides share gets a row in `test_layout.py`'s `PAIRS`. Everything that needs the SDK, the bus or timing is verified on hardware: the build succeeds, the UF2 boots, and the app behaves on the ST. -**With the Debug Probe attached** (SWD and the debug UART), `tools/dev/` does most of that from the host; `tools/dev/README.md` has the details. `flash.sh ` builds out of tree, flashes and checks over SWD that the RP runs that build. `console.py watch` captures the debug console (921,600 baud; release builds have none) into `tools/dev/logs/console.log`. `swd.py` reads the running RP without its help: `counters` (frames published, blits acknowledged, IKBD bytes and packets, the decoder's resyncs and count mismatches, the IKBD's restarts, ACIA and ring overruns, audio slices; per second with `--watch`), `ikbd-log` (debug builds: every sample the IKBD decoder sees, recorded in order, with its own check of the ST's byte counts), `heap`, `shared`, `fb` (the framebuffer as the ST shows it, as a PNG), `crash`, `postmortem`, `select`, `reset`. In debug builds `key` and `app` drive the app through the devhooks mailbox (`rp/src/include/devhooks.h`): keystrokes as from the ST, and the demo dispatcher's `demo N`, `menu`, `overlay`, `slow_frame`, `input_mode N`, `ikbd_cmd BYTE...`. `tools_harness.py --build --flash --reset` checks all of it against the device. Flashing stops the cartridge bus, so reset the ST afterwards; never use OpenOCD's own `reset` or `program` (core 1 is the c2p worker and must start with core 0). +**With the Debug Probe attached** (SWD and the debug UART), `tools/dev/` does most of that from the host; `tools/dev/README.md` has the details. `flash.sh ` builds out of tree, flashes and checks over SWD that the RP runs that build. `console.py watch` captures the debug console (921,600 baud; release builds have none) into `tools/dev/logs/console.log`. `swd.py` reads the running RP without its help: `counters` (frames published, blits acknowledged, IKBD bytes and packets, the decoder's resyncs and count mismatches, the IKBD's restarts, ACIA and ring overruns, audio slices; per second with `--watch`), `ikbd-log` (debug builds: every sample the IKBD decoder sees, recorded in order, with its own check of the ST's byte counts), `heap`, `shared`, `fb` (the framebuffer as the ST shows it, as a PNG), `crash`, `postmortem`, `select`, `reset`. In debug builds `key` and `app` drive the app through the devhooks mailbox (`rp/src/include/devhooks.h`): keystrokes as from the ST, and the demo dispatcher's `demo N`, `menu`, `overlay`, `slow_frame`, `input_mode N`, `ikbd_cmd BYTE...`, `audio_out 0|1` (the DMA chip where there is one / the YM, from the next ST boot), `tone HZ` (a sine through the PCM path; 0: `DEMO.YMS`). `tools_harness.py --build --flash --reset` checks all of it against the device. Flashing stops the cartridge bus, so reset the ST afterwards; never use OpenOCD's own `reset` or `program` (core 1 is the c2p worker and must start with core 0). ## Architecture @@ -100,7 +100,7 @@ End-to-end, every visible pixel on the ST goes through this path each VBL: The number of lines copied by `FBDRV_INLINE` is parameterized by `FB_COPY_LINES` (default 200). Leaving rows untouched lets an app keep a static status bar in the destination ST page that the FB blit never overwrites. `FBDRV_TOTAL_BYTES = FB_COPY_LINES * FB_ROW_BYTES` is divided by `FBDRV_ITER_BYTES = 48` to derive `FBDRV_MAIN_ITERS`; trailing bytes (at most 47) are picked up by a small `d16(a5)` MOVEM tail block emitted via `ifne FBDRV_TAIL_BYTES`. The tail's MOVEM register list must hold exactly `FBDRV_TAIL_BYTES / 4` longwords — at FB_COPY_LINES=200 the tail is 32 B (`d0-d7`); at 199 it's 16 B (`d0-d3`); at 198 it's 0 B (block skipped). Adjust the register list manually when changing FB_COPY_LINES. -**The ST's budget.** The full-screen blit and the audio leave the ST about 0.27 ms of each VBL on a Mega ST (sometimes 0.07 ms), measured with `FB_SLACK_REPORT = 1` in `userfw.s`: after each blit the ST reports how long after the VBL it ended, in Timer-B counts (from the audio cursor's offset in its slice and `TBDR`), at `$FB8Bxx` / `$FB8Cxx` (`$FFFF`: the next VBL came first), and debug builds of the RP keep a histogram (`fbSlackHist`, `fbSlackMinUs`, `fbSlackLate` in `fb.c`). The report costs about 30 µs a blit, so it is 0 by default. Anything more the ST does per VBL comes out of that slack: every IKBD byte is an interrupt of 30–40 µs, so while the mouse moves fast (up to 9 bytes a VBL at the mouse modes' threshold of 4, 16 at threshold 1) some blits end after the next VBL and that frame waits one: 50 frames a second at rest, 30–40 with the mouse moved flat out; the sound stays clean. That is accepted; an app that needs a steady 50 fps with the mouse blits fewer lines (about 88 µs per line). +**The ST's budget.** The full-screen blit and the audio leave the ST about 0.27 ms of each VBL on a Mega ST (sometimes 0.07 ms), measured with `FB_SLACK_REPORT = 1` in `userfw.s`: after each blit the ST reports how long after the VBL it ended, in Timer-B counts (from the audio cursor's offset in its slice and `TBDR`), at `$FB8Bxx` / `$FB8Cxx` (`$FFFF`: the next VBL came first), and debug builds of the RP keep a histogram (`fbSlackHist`, `fbSlackMinUs`, `fbSlackLate` in `fb.c`). The report costs about 30 µs a blit, so it is 0 by default. Anything more the ST does per VBL comes out of that slack: every IKBD byte is an interrupt of 30–40 µs, so while the mouse moves fast (up to 9 bytes a VBL at the mouse modes' threshold of 4, 16 at threshold 1) some blits end after the next VBL and that frame waits one: 50 frames a second at rest, 30–40 with the mouse moved flat out; the sound stays clean. That is accepted; an app that needs a steady 50 fps with the mouse blits fewer lines (about 88 µs per line). On an STE or a Mega STE the sound goes through the DMA chip, Timer-B stays off and its 1.6 ms per VBL are free: measured on a Mega STE, the mouse moved flat out left the frame rate at 50 (30–38 on the same machine forced to the YM). ### IKBD pipeline (keyboard, mouse, joysticks + ESC exit) @@ -118,9 +118,16 @@ Measured on a Mega ST with the input test (menu entry 5) and `tools/dev/swd.py i The earlier ROM4 + DMA-IRQ-per-read capture route (a first attempt) saturated the Cortex-M0+ during fbdrv blits (~8M cart reads/sec) and silently dropped IKBD bytes; switching to the dedicated commemul DMA ring eliminated the loss. -### Audio pipeline (YM2149 dual-channel PCM) +### Audio pipeline (the STE's DMA chip, or the YM2149) -`userfw.s` plays continuous audio out the YM2149 via Timer-B IRQ: the RP writes pre-cooked (vA, vB) volume pairs into a 1 KB cart buffer of 4 slices, one VBL each; m68k Timer-B reads them at ~5,585 Hz and writes both YM volume registers per fire. The ST never plays a slice while it is written, and the sound does not depend on the app's frame rate. +Two outputs, one per machine. On an STE or a Mega STE the **DMA sound chip** plays 8-bit samples at 12,517 Hz, no interrupt per sample; elsewhere **Timer-B** plays YM volume pairs at 5,585 Hz. Both sides apply one rule: `userfw` takes the DMA path at boot when the machine's family (the hello byte, from `_MCH`) is the STE's and the RP has not asked for the YM (`AUDIO_OUT_ADDR` / `CART_AUDIO_OUT_OFFSET`, slot 6: `audio_prefer_ym()`); `audio.c` makes the same choice at each hello (`select_output`, `audioOutput` over SWD: 1 YM, 2 DMA). The app's sound is YM pairs (a loop, a `.YMS` file, a callback) or signed 8-bit PCM at any rate (`audio_play_pcm_loop()`, `audio_set_pcm_callback()`), and either plays on both outputs: the RP converts at runtime through the same 64-entry table both ways (a pair outside it by the YM's volume curve) and resamples linearly. In every case the RP writes ahead of what the ST plays, from a timer interrupt, out of a FIFO the main loop tops up, so the sound never tears and never repeats, whatever the app's frame rate. + +**The DMA path** (verified on a Mega STE, TOS 2.06; the same image plays through the YM on a Mega ST): +- The chip loops over a 2 KB ring in ST RAM, `UFW_DMA_RING` at `$6F800` (below screen page A, the second documented use of Atari RAM), mono 12,517 Hz (`DMA_MODE_PLAY` $81). At boot `userfw` clears it, sets the frame to the whole ring, sets the Microwire (LMC1992) as TOS does (master, left, right 0 dB, tone flat, the YM mixed in) and starts the chip; on the way out to GEM or on `CMD_RESET` it stops it. Timer-B is never started on this path. +- Every VBL, right after the wake, `userfw` reads where the chip plays (the frame counter's middle and low bytes, the middle one read again as it moves), reports it by 8 bytes at `$FB8Dxx`, and copies the cart buffer, the ring's mirror, from where its last copy ended up to `AUDIO_DMA_LEAD` (768 bytes, 3 VBLs) ahead of the chip: a missed VBL still finds samples. About 0.35 ms at 8 MHz, against Timer-B's 1.6 ms. +- `audio.c`'s writer keeps the mirror written `AUDIO_DMA_AHEAD` (928 bytes) ahead of where the chip plays now (the last report, plus the samples played since: a missed VBL sends no report), never inside what the ST copies; byte i of the ring is byte i ^ 1 of the RP's buffer. A late write is counted in `audioLateSlices`, an empty FIFO holds the last sample (`audioUnderruns`). The FIFO holds a VBL more than `AUDIO_FIFO_SLICES`, so the main loop may still go 80 ms between top-ups; a sample plays at most about 175 ms after its source made it. + +**The YM path:** 1. **Build-time PCM → YM conversion** (`tools/wav_to_ym4.py`). Source `.SAM` / `.WAV` is downsampled to the target rate and each 8-bit PCM byte is mapped through the **1988 Ghostbusters demo's 64-entry SAMPLE1 LUT** (default `--mode dual-ghost`): the top 6 bits of each PCM byte index a (chA, chB) pair, emitted as 2 bytes per sample. Output is `rp/src/include/audio_sample.h` — a C array embedded in the RP firmware. The Ghostbusters LUT cherry-picks 64 of the ~135 distinct acoustic sums achievable from the YM's log volume curve, giving ~6 effective bits of resolution vs the 4-bit ceiling of single-channel playback. Other modes available: `single-a` (1 byte/sample, ch A only, Ayumi log LUT), `best-pair` (exhaustive search over all 256 pairs), `nibble` (linear truncation), `raw-byte` (passthrough). 2. **m68k VBL handler moves to the next slice** (`userfw_vbl` in `userfw.s`). The buffer at `$FA4100` is `AUDIO_SLICES` (4) slices of `AUDIO_SLICE_BYTES` (256; a VBL plays 224). On every vsync the handler, with the interrupts masked (Timer-B at IPL 6 outranks it), advances `UFW_AUDIO_SLICE` (a word in the state area), points A0 at that slice and reports it with a ROM3 read at `$FB8600` + slice. A0 is the **dedicated Timer-B audio read cursor** — kept out of `FBDRV_INLINE`'s MOVEM list so the IRQ handler doesn't have to save/restore it; the VBL handler saves the D0 it uses (it interrupts the blit between a MOVEM load and its store). About 30 µs per VBL, with the IKBD byte count it reports first (see the IKBD pipeline). @@ -130,14 +137,16 @@ The earlier ROM4 + DMA-IRQ-per-read capture route (a first attempt) saturated th Tuning knobs: - `TIMERB_COUNT` in `userfw.s` controls the audio rate; the comment above it states the whole chain (Hz → bytes per VBL → slice size → the RP's rate). Raise to slow (longer per-VBL audio slack), lower to speed (better fidelity at higher cost). `AUDIO_FILL_BYTES_PER_VBL` and `AUDIO_NATIVE_RATE_HZ` in `rp/src/audio.c` and the jingle's `AUDIO_SAMPLE_RATE_HZ` follow it; `tests/host/test_layout.py` fails when they do not. -- `AUDIO_FIFO_SLICES` in `rp/src/audio.c` (default 4) trades latency for stall tolerance: fewer slices, less delay, a shorter stall allowed. +- `AUDIO_FIFO_SLICES` in `rp/src/audio.c` (default 4) trades latency for stall tolerance on both outputs: fewer slices, less delay, a shorter stall allowed. +- The DMA path's ring (`AUDIO_DMA_RING_BYTES`, the audio buffer's size) and lead (`AUDIO_DMA_LEAD`) are shared constants (`test_layout.py`); `AUDIO_DMA_AHEAD` in `audio.c` stays the lead plus the writer's period and how late it sees a report. A higher rate needs a bigger ring and buffer. +- PCM assets: `tools/wav_to_ym4.py --mode pcm --target-rate 12517 --header-output x.h ` writes signed 8-bit samples for `audio_play_pcm_loop()`. - Regenerate `audio_sample.h` at the matching rate via `python tools/wav_to_ym4.py --target-rate --mode dual-ghost ` whenever the Timer-B rate changes — otherwise the jingle plays back pitched up/down by the ratio. -- Audio uses one word of Atari RAM, `UFW_AUDIO_SLICE` in `userfw`'s state area; the cart buffer at `$FA4100` is the entire I/O path, and the Timer-B handler keeps its state in A0. +- Audio uses one word of Atari RAM on the YM path, `UFW_AUDIO_SLICE` in `userfw`'s state area, and on the DMA path the 2 KB ring at `$6F800` and three words at `$77F80` (the machine, the path, where the last copy ended); the cart buffer at `$FA4100` is the entire I/O path, and the Timer-B handler keeps its state in A0. ### Atari ST side (`target/atarist/`) - `src/main.s` — m68k cartridge header + boot dispatcher. Lives at `$FA0000` (ROM4 cartridge region). `pre_auto` (CA_INIT bit 27) runs in supervisor mode, copies `start_rom_code` below ST screen memory, jumps there, checks resolution (high-res falls to GEM with a warning), then **`jmp USERFW` directly** — no terminal command dispatch in the default path. The old `check_commands` / `rom_function` indirection is still defined (for apps that want CMD_START-style handoff from the RP) but unused. -- **No Atari RAM**: the cartridge code deliberately uses none of the ST's RAM (no `.bss`, no heap). Data lives in registers or the shared region. Adding naive BSS to `userfw.s` will silently corrupt system variables — keep state in `APP_FREE` / `SHARED_VARIABLES`, the cart code itself (which is **read-only** from m68k — writes bus-error), or registers. Key live registers across the `userfw.s` VBL loop: A0 = Timer-B audio read cursor (reset to `AUDIO_BUFFER_ADDR` by `userfw_vbl` each VBL, never in any MOVEM list), A5 = per-VBL FB destination scratch (predec target for `FBDRV_INLINE`), A6 = per-VBL FB source scratch (postinc reader of the cart FB), A7 = SP (also kept out of MOVEM so IRQs stay safe). D0-D7 / A1-A4 are scratch and are clobbered by the MOVEM macro. -- `src/userfw.s` — **the primary extension point for app-specific m68k code.** `src/userfw.ld` places `main.s` at offset `0x0000` (≤ 2 KB) and `userfw.s` at offset `0x0800` (`0x0800..0x3FFF`, 14 KB budget including the inline `FBDRV_INLINE` macro expansion). The previously-separate `fbdrv.s` module at offset `0x2000` was eliminated: the unrolled MOVEM block is now emitted inline at the userfw entry via the `FBDRV_INLINE` macro, so there's no jsr/rts overhead and no separate hand-written file to maintain. Total cart code ≤ 16 KB (`CARTRIDGE_CODE_SIZE`); current build is ~8.6 KB. `main.s` exposes the userfw entry as `USERFW equ (ROM4_ADDR + $800)`. The default `userfw.s` runs the FB blit + audio + IKBD VBL loop and exits on ESC; replace its body with your own per-frame logic, or extend it. +- **No Atari RAM**, but for three documented exceptions: the two screen pages at `$70000`/`$78000` with `userfw`'s state area in the tail of page A (`$77F00`–`$77FFF`), and on STE-class machines the DMA sound ring just below them (`$6F800`–`$6FFFF`). Otherwise the cartridge code deliberately uses none of the ST's RAM (no `.bss`, no heap). Data lives in registers or the shared region. Adding naive BSS to `userfw.s` will silently corrupt system variables — keep state in `APP_FREE` / `SHARED_VARIABLES`, the cart code itself (which is **read-only** from m68k — writes bus-error), or registers. Key live registers across the `userfw.s` VBL loop: A0 = Timer-B audio read cursor (reset to `AUDIO_BUFFER_ADDR` by `userfw_vbl` each VBL, never in any MOVEM list), A5 = per-VBL FB destination scratch (predec target for `FBDRV_INLINE`), A6 = per-VBL FB source scratch (postinc reader of the cart FB), A7 = SP (also kept out of MOVEM so IRQs stay safe). D0-D7 / A1-A4 are scratch and are clobbered by the MOVEM macro. +- `src/userfw.s` — **the primary extension point for app-specific m68k code.** `src/userfw.ld` places `main.s` at offset `0x0000` (≤ 2 KB) and `userfw.s` at offset `0x0800` (`0x0800..0x3FFF`, 14 KB budget including the inline `FBDRV_INLINE` macro expansion). The previously-separate `fbdrv.s` module at offset `0x2000` was eliminated: the unrolled MOVEM block is now emitted inline at the userfw entry via the `FBDRV_INLINE` macro, so there's no jsr/rts overhead and no separate hand-written file to maintain. Total cart code ≤ 16 KB (`CARTRIDGE_CODE_SIZE`); current build is ~8.9 KB. `main.s` exposes the userfw entry as `USERFW equ (ROM4_ADDR + $800)`. The default `userfw.s` runs the FB blit + audio + IKBD VBL loop and exits on ESC; replace its body with your own per-frame logic, or extend it. - **Every m68k module ends with a NOP tail**: `even`, eight `nop`s and a `_end:` label, after its last code (after `include "inc/sidecart_functions.s"` in a module that includes it). `firmware.py` strips trailing zero bytes from the image, `pre_auto` relocates `start_rom_code..end_rom_code` in whole longwords, and the write sender and the 68000's prefetch read past the senders' wait loop: the last words of a module must be harmless to drop or to read past. `main.s` and `userfw.s` both end this way. - Adding more m68k modules: add a new `.text_` section in `userfw.ld`, mirror the offset with an `equ (ROM4_ADDR + $????)` in `inc/sidecart_layout.s`, and add the `.o` target to `target/atarist/Makefile`. - Built via `stcmd make release` (m68k assembler in Docker). A 64 KB padded copy of the BOOT.BIN is then converted to `target_firmware.h` for inclusion in the RP build. @@ -151,15 +160,15 @@ The Atari ST sees a 64 KB window at `$FA0000`–`$FAFFFF` (mirrored RP-side at ` | `$FA4000` | `CMD_MAGIC_SENTINEL_ADDR` | 4 B | RP→m68k command word (`CMD_NOP`, `CMD_RESET`, `CMD_BOOT_GEM`, `CMD_START`). | | `$FA4004` | (reserved) | 8 B | Former RANDOM_TOKEN / RANDOM_TOKEN_SEED slots from the TPROTOCOL handshake; unused since that handshake was removed. | | `$FA400C` | `FB_FRAME_COUNTER_ADDR` | 4 B | RP-incremented frame counter, bumped as the last write of every publish; `userfw.s` blits only when its low word changes (see the framebuffer pipeline, step 5). | -| `$FA4010` | `SHARED_VARIABLES` | 240 B | 60 × 4 B indexed slots. Slots 2..5 (`$FA4018`, `IKBD_OUT_ADDR` / `CART_IKBD_OUT_OFFSET`) hold the RP's commands for the IKBD (see the IKBD pipeline). Slots 12..19 (`$FA4040`, `CART_PALETTE_OFFSET` / `PALETTE_ADDR`) hold the 16-word ST palette the RP publishes via `palette.c`. Other slots free for apps. | -| `$FA4100` | `AUDIO_BUFFER_ADDR` | 1024 B | Audio buffer: `AUDIO_SLICES` (4) slices of 256 B, one VBL each (224 B played at ~5,585 Hz, dual-channel Ghostbusters-LUT pairs). The ST's VBL handler moves to the next slice and reports it (`$FB86xx`); the RP writes only the slices ahead of it (see the audio pipeline). | -| `$FA4500` | `BOOT_STATUS_ADDR` | 2 B | Read once per ST boot by `pre_auto`, before it starts `userfw`: 0 (what the boot-time erase leaves) starts the app; anything else makes it print the message below and return to GEM. Written by `st_session_veto_boot()` / `st_session_allow_boot()`. | -| `$FA4502` | `BOOT_MESSAGE_ADDR` | 126 B | NUL-terminated reason `pre_auto` prints when the RP refuses to start the app (stored byte-swapped within each word, as the cart bus reads it). | -| `$FA4580` | `APP_FREE_ADDR` | ~15.4 KB | contiguous arena for app buffers, ends at FRAMEBUFFER. Apps that disable the audio pipeline can reclaim the 1 KB at `$FA4100`. | +| `$FA4010` | `SHARED_VARIABLES` | 240 B | 60 × 4 B indexed slots. Slots 2..5 (`$FA4018`, `IKBD_OUT_ADDR` / `CART_IKBD_OUT_OFFSET`) hold the RP's commands for the IKBD (see the IKBD pipeline). Slot 6 (`$FA4028`, `AUDIO_OUT_ADDR` / `CART_AUDIO_OUT_OFFSET`): the RP keeps the ST to the YM (see the audio pipeline). Slots 12..19 (`$FA4040`, `CART_PALETTE_OFFSET` / `PALETTE_ADDR`) hold the 16-word ST palette the RP publishes via `palette.c`. Other slots free for apps. | +| `$FA4100` | `AUDIO_BUFFER_ADDR` | 2048 B | Audio buffer. On the YM path its first `AUDIO_SLICES` (4) slices of 256 B, one VBL each (224 B played at ~5,585 Hz, dual-channel Ghostbusters-LUT pairs): the ST's VBL handler moves to the next slice and reports it (`$FB86xx`); the RP writes only the slices ahead of it. On the DMA path the whole buffer mirrors the chip's ring in ST RAM (see the audio pipeline). | +| `$FA4900` | `BOOT_STATUS_ADDR` | 2 B | Read once per ST boot by `pre_auto`, before it starts `userfw`: 0 (what the boot-time erase leaves) starts the app; anything else makes it print the message below and return to GEM. Written by `st_session_veto_boot()` / `st_session_allow_boot()`. | +| `$FA4902` | `BOOT_MESSAGE_ADDR` | 126 B | NUL-terminated reason `pre_auto` prints when the RP refuses to start the app (stored byte-swapped within each word, as the cart bus reads it). | +| `$FA4980` | `APP_FREE_ADDR` | ~14.4 KB | contiguous arena for app buffers, ends at FRAMEBUFFER. Apps that disable the audio pipeline can reclaim the 2 KB at `$FA4100`. | | `$FA8300` | `FRAMEBUFFER_ADDR` | 32000 B | 320×200×4bpp color framebuffer. Top of the region so an overrun walks off the end of the 64 KB window instead of corrupting the metadata block. Read by `FBDRV_INLINE` every VBL. | ### ROM3 signalling windows and ST sessions -The cartridge window is read-only from the ST, so the ST tells the RP things by *reading* ROM3 addresses (`$FB0000`–`$FBFFFF`), which the commemul ring captures: the high byte of the address says what the read means, the low byte carries a value. The windows are listed in `cart_shared.h` (`CART_ROM3_*`) and mirrored as equs in `inc/sidecart_layout.s`: `$FB82xx` an IKBD byte, `$FB83xx` the IKBD bytes read so far (low byte, every VBL), `$FB84xx` blit done, `$FB8500` the keyboard ACIA overran, `$FB86xx` the audio slice playing from this VBL, `$FB87xx` the IKBD commands of generation xx sent, `$FB88xx` the hello (xx = the machine), `$FB89xx` / `$FB8Axx` the TOS version's high and low byte, `$FB8Bxx` / `$FB8Cxx` when the blit ended (`FB_SLACK_REPORT` only). A new signal gets a window of its own. +The cartridge window is read-only from the ST, so the ST tells the RP things by *reading* ROM3 addresses (`$FB0000`–`$FBFFFF`), which the commemul ring captures: the high byte of the address says what the read means, the low byte carries a value. The windows are listed in `cart_shared.h` (`CART_ROM3_*`) and mirrored as equs in `inc/sidecart_layout.s`: `$FB82xx` an IKBD byte, `$FB83xx` the IKBD bytes read so far (low byte, every VBL), `$FB84xx` blit done, `$FB8500` the keyboard ACIA overran, `$FB86xx` the audio slice playing from this VBL, `$FB87xx` the IKBD commands of generation xx sent, `$FB88xx` the hello (xx = the machine), `$FB89xx` / `$FB8Axx` the TOS version's high and low byte, `$FB8Bxx` / `$FB8Cxx` when the blit ended (`FB_SLACK_REPORT` only), `$FB8Dxx` where the DMA sound chip plays, by 8 bytes (every VBL, DMA path). A new signal gets a window of its own. What each side may assume about the other: - **The ST and the RP reboot independently.** A power cycle restarts both; an ST reset restarts only the ST, and the RP keeps everything: the command sentinel, the app's state. An RP-only restart (a flash, SELECT) rebuilds the window while the ST keeps running. diff --git a/README.md b/README.md index 14b4c76..4692eaa 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,9 @@ at 50 Hz.** No m68k assembly, no bus timing, no double-buffering to manage. framework does the chunked → Atari ST planar conversion for you. - **~1 ms per VBL** for that chunky→planar conversion (split across both cores), so it barely eats into your frame budget. -- **~6 kHz, 6-bit sampled sound** out the YM2149. +- **Sampled sound**: 8-bit at 12.5 kHz through the DMA sound chip on an + STE or a Mega STE, ~6 kHz 6-bit out the YM2149 elsewhere — the same + asset plays on both. - **Atari ST keyboard, mouse and joysticks handled on the RP2040** — decoded scancodes, mouse movement and stick states delivered straight to your app. @@ -301,10 +303,13 @@ your own graphics. ## 5. Audio (`audio.h`) -The ST plays one VBL of samples from a 1 KB cart buffer into the YM2149 -every VBL, and the RP writes the next ones from a timer interrupt, so the -sound never tears and never repeats, whatever your frame rate. You supply -the bytes one of two ways. +On an STE or a Mega STE the sound goes through the DMA sound chip (8-bit, +12,517 Hz, no interrupt per sample); on a plain ST or Mega ST it goes +through the YM2149 (~6-bit, 5,585 Hz). The template picks the output at +every ST boot, and the RP writes the samples ahead of what the ST plays +from a timer interrupt, so the sound never tears and never repeats, +whatever your frame rate. Your audio is YM volume pairs or 8-bit PCM, and +either plays on both outputs: the RP converts at runtime. ### Loop a baked-in buffer @@ -317,6 +322,24 @@ audio_play_loop(audio_sample_data, sizeof(audio_sample_data)); // ... then call audio_render_frame() once per main-loop iteration. ``` +### 8-bit PCM + +For a recording at its best on the DMA chip, convert it to signed 8-bit +PCM at 12,517 Hz (`tools/wav_to_ym4.py --mode pcm --target-rate 12517 +--header-output my_sound.h my_sound.wav`), or generate PCM live at any +rate; on the YM the RP plays its upper 6 bits through the same table: + +```c +#include "my_sound.h" // audio_sample_data[], AUDIO_SAMPLE_RATE_HZ +audio_play_pcm_loop(audio_sample_data, audio_sample_count, AUDIO_SAMPLE_RATE_HZ); + +static void my_pcm(int8_t *buf, uint32_t samples) { /* ... */ } +audio_set_pcm_callback(my_pcm, AUDIO_DMA_RATE_HZ); +``` + +`audio_prefer_ym(true)` keeps an STE on the YM from its next boot; +`audio_uses_dma()` says which output plays. + ### Generate audio live (callback) For dynamic sound, install a fill callback. The library calls it for each @@ -334,7 +357,7 @@ Either way, **`audio_render_frame()` must be called each loop iteration**: it tops up a small FIFO (4 VBLs of samples) that the interrupt plays from. Your loop may take up to 80 ms between two calls without the sound noticing; a sample reaches the speaker at most 120 ms after your callback -made it. There's also `audio_play_yms_file(path)` to stream a `.YMS` file +made it on the YM, about 175 ms on the DMA chip. There's also `audio_play_yms_file(path)` to stream a `.YMS` file from SD — see §7. --- diff --git a/tools/dev/README.md b/tools/dev/README.md index eec4aa1..ea9d8c7 100644 --- a/tools/dev/README.md +++ b/tools/dev/README.md @@ -17,7 +17,7 @@ Python tools use the standard library only. `fb_frame_tick`, `s_vbl_seen` and `fbAckTimeouts` (fb.c), `ikbdOverruns`, `ikbdBytes`, `ikbdMousePackets`, `ikbdJoystickPackets`, `ikbdResyncs`, `ikbdCountMismatches` and `ikbdPowerUps` (ikbd.c), `commOverruns` (commemul.c), and `audioSlicesWritten`, - `audioLateSlices` and `audioUnderruns` (audio.c). Debug builds also keep `ikbdLog` / + `audioLateSlices`, `audioUnderruns` and `audioOutput` (audio.c). Debug builds also keep `ikbdLog` / `ikbdLogCount` (ikbd.c, for `ikbd-log`) and, when `userfw.s` reports it, the blit slack (`fbSlackHist`, `fbSlackMinUs`, `fbSlackLate` in fb.c). - Debug builds carry the devhooks mailbox (`rp/src/include/devhooks.h`, included once from @@ -122,8 +122,9 @@ python3 tools/dev/swd.py resume # release cores acknowledged, the publishes that gave up waiting for an acknowledgement (`fbAckTimeouts`: expected until the ST runs the app, never while it does), the keyboard ACIA's overruns, the IKBD bytes and the mouse and joystick packets decoded, the decoder's resyncs and how many were byte-count -mismatches, the IKBD's restarts (a keyboard plugged back in), the ROM3 ring's overruns, and the -audio slices written, late (`audioLateSlices`) and underrun (`audioUnderruns`). `--watch SECONDS` +mismatches, the IKBD's restarts (a keyboard plugged back in), the ROM3 ring's overruns, the +VBLs of sound written, late (`audioLateSlices`) and underrun (`audioUnderruns`), and the audio +output (`audioOutput`: 1 the YM, 2 the STE's DMA chip). `--watch SECONDS` prints what changed, with the frame, blit, audio slice and IKBD byte rates: frames and blits run at 50 a second while the ST runs the app and the app publishes every frame (a mouse moved fast costs the ST some: see CLAUDE.md, "The ST's budget"); audio slices run at 50 a second whatever the @@ -168,6 +169,10 @@ runs the command defined as `DEVHOOKS_APP_` in `rp/src/include`; the demo demo. - `ikbd_cmd BYTE...`: IKBD command bytes (at most 12; `0` waits a VBL), sent by the ST one per VBL: to try what an IKBD does, e.g. `ikbd_cmd 0x16` asks for both sticks' state. +- `audio_out 0|1`: from the ST's next boot, the DMA chip where there is one (0) or the YM (1): + both outputs on one STE with a reset in between. +- `tone HZ`: a sine through the PCM path, whose clicks are easy to hear; `tone 0` goes back to + `DEMO.YMS`. An app adds its own the same way: a `DEVHOOKS_APP_` define and a handler set with `devhooks_setAppHandler()`.