From 7a5d1c281920f1f1a8f7a4c9a873a84180d91fd6 Mon Sep 17 00:00:00 2001 From: Stepan Usatiuk Date: Mon, 13 Oct 2025 20:13:32 +0200 Subject: [PATCH] dump --- .../include/cardboy/backend/esp/config.hpp | 2 +- Firmware/sdk/apps/gameboy/CMakeLists.txt | 7 + .../sdk/apps/gameboy/minigb_apu/minigb_apu.c | 542 +++++++++++++ .../sdk/apps/gameboy/minigb_apu/minigb_apu.h | 149 ++++ Firmware/sdk/apps/gameboy/src/gameboy_app.cpp | 727 +++++++----------- 5 files changed, 965 insertions(+), 462 deletions(-) create mode 100644 Firmware/sdk/apps/gameboy/minigb_apu/minigb_apu.c create mode 100644 Firmware/sdk/apps/gameboy/minigb_apu/minigb_apu.h diff --git a/Firmware/components/backend-esp/include/cardboy/backend/esp/config.hpp b/Firmware/components/backend-esp/include/cardboy/backend/esp/config.hpp index 16bcc91..91d1896 100644 --- a/Firmware/components/backend-esp/include/cardboy/backend/esp/config.hpp +++ b/Firmware/components/backend-esp/include/cardboy/backend/esp/config.hpp @@ -20,7 +20,7 @@ #define DISP_WIDTH cardboy::sdk::kDisplayWidth #define DISP_HEIGHT cardboy::sdk::kDisplayHeight -#define BUZZER_PIN GPIO_NUM_25 +#define BUZZER_PIN GPIO_NUM_22 #define PWR_INT GPIO_NUM_10 #define PWR_KILL GPIO_NUM_12 diff --git a/Firmware/sdk/apps/gameboy/CMakeLists.txt b/Firmware/sdk/apps/gameboy/CMakeLists.txt index 76b4b6f..83f82f5 100644 --- a/Firmware/sdk/apps/gameboy/CMakeLists.txt +++ b/Firmware/sdk/apps/gameboy/CMakeLists.txt @@ -1,10 +1,17 @@ target_sources(cardboy_apps PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/gameboy_app.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/minigb_apu/minigb_apu.c ${CMAKE_CURRENT_SOURCE_DIR}/include/cardboy/apps/peanut_gb.h ) target_include_directories(cardboy_apps PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include + ${CMAKE_CURRENT_SOURCE_DIR} ) + +target_compile_definitions(cardboy_apps + PRIVATE + MINIGB_APU_AUDIO_FORMAT_S16SYS=1 +) \ No newline at end of file diff --git a/Firmware/sdk/apps/gameboy/minigb_apu/minigb_apu.c b/Firmware/sdk/apps/gameboy/minigb_apu/minigb_apu.c new file mode 100644 index 0000000..a9671e1 --- /dev/null +++ b/Firmware/sdk/apps/gameboy/minigb_apu/minigb_apu.c @@ -0,0 +1,542 @@ +/** + * Game Boy APU emulator. + * Copyright (c) 2019 Mahyar Koshkouei + * Copyright (c) 2017 Alex Baines + * minigb_apu is released under the terms of the MIT license. + * + * minigb_apu emulates the audio processing unit (APU) of the Game Boy. This + * project is based on MiniGBS by Alex Baines: https://github.com/baines/MiniGBS + */ + +#include +#include +#include + +#include "minigb_apu.h" + +#define DMG_CLOCK_FREQ_U ((unsigned)DMG_CLOCK_FREQ) +#define AUDIO_NSAMPLES (AUDIO_SAMPLES_TOTAL) + +#define MAX(a, b) ( a > b ? a : b ) +#define MIN(a, b) ( a <= b ? a : b ) + +/* Factor in which values are multiplied to compensate for fixed-point + * arithmetic. Some hard-coded values in this project must be recreated. */ +#ifndef FREQ_INC_MULT +# define FREQ_INC_MULT 105 +#endif +/* Handles time keeping for sound generation. + * FREQ_INC_REF must be equal to, or larger than AUDIO_SAMPLE_RATE in order + * to avoid a division by zero error. + * Using a square of 2 simplifies calculations. */ +#define FREQ_INC_REF (AUDIO_SAMPLE_RATE * FREQ_INC_MULT) + +#define MAX_CHAN_VOLUME 15 + +static void set_note_freq(struct chan *c) +{ + /* Lowest expected value of freq is 64. */ + uint32_t freq = (DMG_CLOCK_FREQ_U / 4) / (2048 - c->freq); + c->freq_inc = freq * (uint32_t)(FREQ_INC_REF / AUDIO_SAMPLE_RATE); +} + +static void chan_enable(struct minigb_apu_ctx *ctx, + const uint_fast8_t i, const bool enable) +{ + uint8_t val; + + ctx->chans[i].enabled = enable; + val = (ctx->audio_mem[0xFF26 - AUDIO_ADDR_COMPENSATION] & 0x80) | + (ctx->chans[3].enabled << 3) | (ctx->chans[2].enabled << 2) | + (ctx->chans[1].enabled << 1) | (ctx->chans[0].enabled << 0); + + ctx->audio_mem[0xFF26 - AUDIO_ADDR_COMPENSATION] = val; +} + +static void update_env(struct chan *c) +{ + c->env.counter += c->env.inc; + + while (c->env.counter > FREQ_INC_REF) { + if (c->env.step) { + c->volume += c->env.up ? 1 : -1; + if (c->volume == 0 || c->volume == MAX_CHAN_VOLUME) { + c->env.inc = 0; + } + c->volume = MAX(0, MIN(MAX_CHAN_VOLUME, c->volume)); + } + c->env.counter -= FREQ_INC_REF; + } +} + +static void update_len(struct minigb_apu_ctx *ctx, struct chan *c) +{ + if (!c->len.enabled) + return; + + c->len.counter += c->len.inc; + if (c->len.counter > FREQ_INC_REF) { + chan_enable(ctx, c - ctx->chans, 0); + c->len.counter = 0; + } +} + +static bool update_freq(struct chan *c, uint32_t *pos) +{ + uint32_t inc = c->freq_inc - *pos; + c->freq_counter += inc; + + if (c->freq_counter > FREQ_INC_REF) { + *pos = c->freq_inc - (c->freq_counter - FREQ_INC_REF); + c->freq_counter = 0; + return true; + } else { + *pos = c->freq_inc; + return false; + } +} + +static void update_sweep(struct chan *c) +{ + c->sweep.counter += c->sweep.inc; + + while (c->sweep.counter > FREQ_INC_REF) { + if (c->sweep.shift) { + uint16_t inc = (c->sweep.freq >> c->sweep.shift); + if (c->sweep.down) + inc *= -1; + + c->freq = c->sweep.freq + inc; + if (c->freq > 2047) { + c->enabled = 0; + } else { + set_note_freq(c); + c->sweep.freq = c->freq; + } + } else if (c->sweep.rate) { + c->enabled = 0; + } + c->sweep.counter -= FREQ_INC_REF; + } +} + +static void update_square(struct minigb_apu_ctx *ctx, audio_sample_t *samples, + const bool ch2) +{ + struct chan *c = &ctx->chans[ch2]; + + if (!c->powered || !c->enabled) + return; + + set_note_freq(c); + + for (uint_fast16_t i = 0; i < AUDIO_NSAMPLES; i += 2) { + update_len(ctx, c); + if (!c->enabled) + return; + + update_env(c); + if (!c->volume) + continue; + + if (!ch2) + update_sweep(c); + + uint32_t pos = 0; + uint32_t prev_pos = 0; + int32_t sample = 0; + + while (update_freq(c, &pos)) { + c->square.duty_counter = (c->square.duty_counter + 1) & 7; + sample += ((pos - prev_pos) / c->freq_inc) * c->val; + c->val = (c->square.duty & (1 << c->square.duty_counter)) ? + VOL_INIT_MAX / MAX_CHAN_VOLUME : + VOL_INIT_MIN / MAX_CHAN_VOLUME; + prev_pos = pos; + } + + sample += c->val; + sample *= c->volume; + sample /= 4; + + samples[i + 0] += sample * c->on_left * ctx->vol_l; + samples[i + 1] += sample * c->on_right * ctx->vol_r; + } +} + +static uint8_t wave_sample(struct minigb_apu_ctx *ctx, + const unsigned int pos, const unsigned int volume) +{ + uint8_t sample; + + sample = ctx->audio_mem[(0xFF30 + pos / 2) - AUDIO_ADDR_COMPENSATION]; + if (pos & 1) { + sample &= 0xF; + } else { + sample >>= 4; + } + return volume ? (sample >> (volume - 1)) : 0; +} + +static void update_wave(struct minigb_apu_ctx *ctx, audio_sample_t *samples) +{ + struct chan *c = &ctx->chans[2]; + + if (!c->powered || !c->enabled || !c->volume) + return; + + set_note_freq(c); + c->freq_inc *= 2; + + for (uint_fast16_t i = 0; i < AUDIO_NSAMPLES; i += 2) { + update_len(ctx, c); + if (!c->enabled) + return; + + uint32_t pos = 0; + uint32_t prev_pos = 0; + audio_sample_t sample = 0; + + c->wave.sample = wave_sample(ctx, c->val, c->volume); + + while (update_freq(c, &pos)) { + c->val = (c->val + 1) & 31; + sample += ((pos - prev_pos) / c->freq_inc) * + ((audio_sample_t)c->wave.sample - 8) * + (AUDIO_SAMPLE_MAX/64); + c->wave.sample = wave_sample(ctx, c->val, c->volume); + prev_pos = pos; + } + + sample += ((audio_sample_t)c->wave.sample - 8) * + (audio_sample_t)(AUDIO_SAMPLE_MAX/64); + { + /* First element is unused. */ + audio_sample_t div[] = { AUDIO_SAMPLE_MAX, 1, 2, 4 }; + sample = sample / (div[c->volume]); + } + + sample /= 4; + samples[i + 0] += sample * c->on_left * ctx->vol_l; + samples[i + 1] += sample * c->on_right * ctx->vol_r; + } +} + +static void update_noise(struct minigb_apu_ctx *ctx, audio_sample_t *samples) +{ + struct chan *c = &ctx->chans[3]; + + if (c->freq >= 14) + c->enabled = 0; + + if (!c->powered || !c->enabled) + return; + + { + const uint32_t lfsr_div_lut[] = { + 8, 16, 32, 48, 64, 80, 96, 112 + }; + uint32_t freq; + + freq = DMG_CLOCK_FREQ_U / (lfsr_div_lut[c->noise.lfsr_div] << c->freq); + c->freq_inc = freq * (uint32_t)(FREQ_INC_REF / AUDIO_SAMPLE_RATE); + } + + for (uint_fast16_t i = 0; i < AUDIO_NSAMPLES; i += 2) { + update_len(ctx, c); + if (!c->enabled) + return; + + update_env(c); + if (!c->volume) + continue; + + uint32_t pos = 0; + uint32_t prev_pos = 0; + int32_t sample = 0; + + while (update_freq(c, &pos)) { + c->noise.lfsr_reg = (c->noise.lfsr_reg << 1) | + (c->val >= VOL_INIT_MAX/MAX_CHAN_VOLUME); + + if (c->noise.lfsr_wide) { + c->val = !(((c->noise.lfsr_reg >> 14) & 1) ^ + ((c->noise.lfsr_reg >> 13) & 1)) ? + VOL_INIT_MAX / MAX_CHAN_VOLUME : + VOL_INIT_MIN / MAX_CHAN_VOLUME; + } else { + c->val = !(((c->noise.lfsr_reg >> 6) & 1) ^ + ((c->noise.lfsr_reg >> 5) & 1)) ? + VOL_INIT_MAX / MAX_CHAN_VOLUME : + VOL_INIT_MIN / MAX_CHAN_VOLUME; + } + + sample += ((pos - prev_pos) / c->freq_inc) * c->val; + prev_pos = pos; + } + + sample += c->val; + sample *= c->volume; + sample /= 4; + + samples[i + 0] += sample * c->on_left * ctx->vol_l; + samples[i + 1] += sample * c->on_right * ctx->vol_r; + } +} + +/** + * SDL2 style audio callback function. + */ +void minigb_apu_audio_callback(struct minigb_apu_ctx *ctx, + audio_sample_t *stream) +{ + memset(stream, 0, AUDIO_SAMPLES_TOTAL * sizeof(audio_sample_t)); + update_square(ctx, stream, 0); + update_square(ctx, stream, 1); + update_wave(ctx, stream); + update_noise(ctx, stream); +} + +static void chan_trigger(struct minigb_apu_ctx *ctx, uint_fast8_t i) +{ + struct chan *c = &ctx->chans[i]; + + chan_enable(ctx, i, 1); + c->volume = c->volume_init; + + // volume envelope + { + /* LUT created in Julia with: + * `(FREQ_INC_MULT * 64)./vcat(8, 1:7)` + * Must be recreated when FREQ_INC_MULT modified. + */ + const uint32_t inc_lut[8] = { +#if FREQ_INC_MULT == 16 + 128, 1024, 512, 341, + 256, 205, 171, 146 +#elif FREQ_INC_MULT == 64 + 512, 4096, 2048, 1365, + 1024, 819, 683, 585 +#elif FREQ_INC_MULT == 105 + /* Multiples of 105 provide integer values. */ + 840, 6720, 3360, 2240, + 1680, 1344, 1120, 960 +#else +#error "LUT not calculated for this value of FREQ_INC_MULT" +#endif + }; + uint8_t val; + + val = ctx->audio_mem[(0xFF12 + (i * 5)) - AUDIO_ADDR_COMPENSATION]; + + c->env.step = val & 0x7; + c->env.up = val & 0x8; + c->env.inc = inc_lut[c->env.step]; + c->env.counter = 0; + } + + // freq sweep + if (i == 0) { + uint8_t val = ctx->audio_mem[0xFF10 - AUDIO_ADDR_COMPENSATION]; + + c->sweep.freq = c->freq; + c->sweep.rate = (val >> 4) & 0x07; + c->sweep.down = (val & 0x08); + c->sweep.shift = (val & 0x07); + c->sweep.inc = c->sweep.rate ? + ((128u * FREQ_INC_REF) / (c->sweep.rate * AUDIO_SAMPLE_RATE)) : 0; + c->sweep.counter = FREQ_INC_REF; + } + + int len_max = 64; + + if (i == 2) { // wave + len_max = 256; + c->val = 0; + } else if (i == 3) { // noise + c->noise.lfsr_reg = 0xFFFF; + c->val = VOL_INIT_MIN / MAX_CHAN_VOLUME; + } + + c->len.inc = (256u * FREQ_INC_REF) / (AUDIO_SAMPLE_RATE * (len_max - c->len.load)); + c->len.counter = 0; +} + +/** + * Read audio register. + * \param addr Address of audio register. Must be 0xFF10 <= addr <= 0xFF3F. + * This is not checked in this function. + * \return Byte at address. + */ +uint8_t minigb_apu_audio_read(struct minigb_apu_ctx *ctx, const uint16_t addr) +{ + static const uint8_t ortab[] = { + 0x80, 0x3f, 0x00, 0xff, 0xbf, + 0xff, 0x3f, 0x00, 0xff, 0xbf, + 0x7f, 0xff, 0x9f, 0xff, 0xbf, + 0xff, 0xff, 0x00, 0x00, 0xbf, + 0x00, 0x00, 0x70, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + }; + + return ctx->audio_mem[addr - AUDIO_ADDR_COMPENSATION] | + ortab[addr - AUDIO_ADDR_COMPENSATION]; +} + +/** + * Write audio register. + * \param addr Address of audio register. Must be 0xFF10 <= addr <= 0xFF3F. + * This is not checked in this function. + * \param val Byte to write at address. + */ +void minigb_apu_audio_write(struct minigb_apu_ctx *ctx, + const uint16_t addr, const uint8_t val) +{ + /* Find sound channel corresponding to register address. */ + uint_fast8_t i; + + if(addr == 0xFF26) + { + ctx->audio_mem[addr - AUDIO_ADDR_COMPENSATION] = val & 0x80; + /* On APU power off, clear all registers apart from wave + * RAM. */ + if((val & 0x80) == 0) + { + memset(ctx->audio_mem, + 0x00, 0xFF26 - AUDIO_ADDR_COMPENSATION); + ctx->chans[0].enabled = false; + ctx->chans[1].enabled = false; + ctx->chans[2].enabled = false; + ctx->chans[3].enabled = false; + } + + return; + } + + /* Ignore register writes if APU powered off. */ + if(ctx->audio_mem[0xFF26 - AUDIO_ADDR_COMPENSATION] == 0x00) + return; + + ctx->audio_mem[addr - AUDIO_ADDR_COMPENSATION] = val; + i = (addr - AUDIO_ADDR_COMPENSATION) / 5; + + switch (addr) { + case 0xFF12: + case 0xFF17: + case 0xFF21: { + ctx->chans[i].volume_init = val >> 4; + ctx->chans[i].powered = (val >> 3) != 0; + + // "zombie mode" stuff, needed for Prehistorik Man and probably + // others + if (ctx->chans[i].powered && ctx->chans[i].enabled) { + if ((ctx->chans[i].env.step == 0 && ctx->chans[i].env.inc != 0)) { + if (val & 0x08) { + ctx->chans[i].volume++; + } else { + ctx->chans[i].volume += 2; + } + } else { + ctx->chans[i].volume = 16 - ctx->chans[i].volume; + } + + ctx->chans[i].volume &= 0x0F; + ctx->chans[i].env.step = val & 0x07; + } + } break; + + case 0xFF1C: + ctx->chans[i].volume = ctx->chans[i].volume_init = (val >> 5) & 0x03; + break; + + case 0xFF11: + case 0xFF16: + case 0xFF20: { + const uint8_t duty_lookup[] = { 0x10, 0x30, 0x3C, 0xCF }; + ctx->chans[i].len.load = val & 0x3f; + ctx->chans[i].square.duty = duty_lookup[val >> 6]; + break; + } + + case 0xFF1B: + ctx->chans[i].len.load = val; + break; + + case 0xFF13: + case 0xFF18: + case 0xFF1D: + ctx->chans[i].freq &= 0xFF00; + ctx->chans[i].freq |= val; + break; + + case 0xFF1A: + ctx->chans[i].powered = (val & 0x80) != 0; + chan_enable(ctx, i, val & 0x80); + break; + + case 0xFF14: + case 0xFF19: + case 0xFF1E: + ctx->chans[i].freq &= 0x00FF; + ctx->chans[i].freq |= ((val & 0x07) << 8); + /* Intentional fall-through. */ + case 0xFF23: + ctx->chans[i].len.enabled = val & 0x40; + if (val & 0x80) + chan_trigger(ctx, i); + + break; + + case 0xFF22: + ctx->chans[3].freq = val >> 4; + ctx->chans[3].noise.lfsr_wide = !(val & 0x08); + ctx->chans[3].noise.lfsr_div = val & 0x07; + break; + + case 0xFF24: + { + ctx->vol_l = ((val >> 4) & 0x07); + ctx->vol_r = (val & 0x07); + break; + } + + case 0xFF25: + for (uint_fast8_t j = 0; j < 4; j++) { + ctx->chans[j].on_left = (val >> (4 + j)) & 1; + ctx->chans[j].on_right = (val >> j) & 1; + } + break; + } +} + +void minigb_apu_audio_init(struct minigb_apu_ctx *ctx) +{ + /* Initialise channels and samples. */ + memset(ctx->chans, 0, sizeof(ctx->chans)); + ctx->chans[0].val = ctx->chans[1].val = -1; + + /* Initialise IO registers. */ + { + const uint8_t regs_init[] = { 0x80, 0xBF, 0xF3, 0xFF, 0x3F, + 0xFF, 0x3F, 0x00, 0xFF, 0x3F, + 0x7F, 0xFF, 0x9F, 0xFF, 0x3F, + 0xFF, 0xFF, 0x00, 0x00, 0x3F, + 0x77, 0xF3, 0xF1 }; + + for(uint_fast8_t i = 0; i < sizeof(regs_init); ++i) + minigb_apu_audio_write(ctx, 0xFF10 + i, regs_init[i]); + } + + /* Initialise Wave Pattern RAM. */ + { + const uint8_t wave_init[] = { 0xac, 0xdd, 0xda, 0x48, + 0x36, 0x02, 0xcf, 0x16, + 0x2c, 0x04, 0xe5, 0x2c, + 0xac, 0xdd, 0xda, 0x48 }; + + for(uint_fast8_t i = 0; i < sizeof(wave_init); ++i) + minigb_apu_audio_write(ctx, 0xFF30 + i, wave_init[i]); + } +} diff --git a/Firmware/sdk/apps/gameboy/minigb_apu/minigb_apu.h b/Firmware/sdk/apps/gameboy/minigb_apu/minigb_apu.h new file mode 100644 index 0000000..9dd75a1 --- /dev/null +++ b/Firmware/sdk/apps/gameboy/minigb_apu/minigb_apu.h @@ -0,0 +1,149 @@ +/** + * minigb_apu is released under the terms listed within the LICENSE file. + * + * minigb_apu emulates the audio processing unit (APU) of the Game Boy. This + * project is based on MiniGBS by Alex Baines: https://github.com/baines/MiniGBS + */ + +#pragma once + +#include + +#ifndef AUDIO_SAMPLE_RATE +# define AUDIO_SAMPLE_RATE 20000 +#endif + +/* The audio output format is in platform native endian. */ +#if defined(MINIGB_APU_AUDIO_FORMAT_S16SYS) +typedef int16_t audio_sample_t; +# define AUDIO_SAMPLE_MAX INT16_MAX +# define AUDIO_SAMPLE_MIN INT16_MIN +# define VOL_INIT_MAX (AUDIO_SAMPLE_MAX/8) +# define VOL_INIT_MIN (AUDIO_SAMPLE_MIN/8) +#elif defined(MINIGB_APU_AUDIO_FORMAT_S32SYS) +typedef int32_t audio_sample_t; +# define AUDIO_SAMPLE_MAX INT32_MAX +# define AUDIO_SAMPLE_MIN INT32_MIN +# define VOL_INIT_MAX (INT32_MAX/8) +# define VOL_INIT_MIN (INT32_MIN/8) +#else +#error MiniGB APU: Invalid or unsupported audio format selected +#endif + +#define DMG_CLOCK_FREQ 4194304.0 +#define SCREEN_REFRESH_CYCLES 70224.0 +#define VERTICAL_SYNC (DMG_CLOCK_FREQ/SCREEN_REFRESH_CYCLES) + +/* Number of audio samples in each channel. */ +#define AUDIO_SAMPLES ((unsigned)(AUDIO_SAMPLE_RATE / VERTICAL_SYNC)) +/* Number of audio channels. The audio output is in interleaved stereo format.*/ +#define AUDIO_CHANNELS 2 +/* Number of audio samples output in each audio_callback call. */ +#define AUDIO_SAMPLES_TOTAL (AUDIO_SAMPLES * 2) + +#define AUDIO_MEM_SIZE (0xFF3F - 0xFF10 + 1) +#define AUDIO_ADDR_COMPENSATION 0xFF10 + +struct chan_len_ctr { + uint8_t load; + uint8_t enabled; + uint32_t counter; + uint32_t inc; +}; + +struct chan_vol_env { + uint8_t step; + uint8_t up; + uint32_t counter; + uint32_t inc; +}; + +struct chan_freq_sweep { + uint8_t rate; + uint8_t shift; + uint8_t down; + uint16_t freq; + uint32_t counter; + uint32_t inc; +}; + +struct chan { + uint8_t enabled; + uint8_t powered; + uint8_t on_left; + uint8_t on_right; + + uint8_t volume; + uint8_t volume_init; + + uint16_t freq; + uint32_t freq_counter; + uint32_t freq_inc; + + int32_t val; + + struct chan_len_ctr len; + struct chan_vol_env env; + struct chan_freq_sweep sweep; + + union { + struct { + uint8_t duty; + uint8_t duty_counter; + } square; + struct { + uint16_t lfsr_reg; + uint8_t lfsr_wide; + uint8_t lfsr_div; + } noise; + struct { + uint8_t sample; + } wave; + }; +}; + +struct minigb_apu_ctx { + struct chan chans[4]; + int32_t vol_l, vol_r; + + /** + * Memory holding audio registers between 0xFF10 and 0xFF3F inclusive. + */ + uint8_t audio_mem[AUDIO_MEM_SIZE]; +}; + +/** + * Fill allocated buffer "stream" with AUDIO_SAMPLES_TOTAL number of 16-bit + * signed samples (native endian order) in stereo interleaved format. + * Each call corresponds to the time taken for each VSYNC in the Game Boy. + * + * \param ctx Library context. Must be initialised with audio_init(). + * \param stream Allocated pointer to store audio samples. Must be at least + * AUDIO_SAMPLES_TOTAL in size. + */ +void minigb_apu_audio_callback(struct minigb_apu_ctx *ctx, + audio_sample_t *stream); + +/** + * Read audio register at given address "addr". + * \param ctx Library context. Must be initialised with audio_init(). + * \param addr Address of registers to read. Must be within 0xFF10 and 0xFF3F, + * inclusive. + */ +uint8_t minigb_apu_audio_read(struct minigb_apu_ctx *ctx, const uint16_t addr); + +/** + * Write "val" to audio register at given address "addr". + * \param ctx Library context. Must be initialised with audio_init(). + * \param addr Address of registers to read. Must be within 0xFF10 and 0xFF3F, + * inclusive. + * \param val Value to write to address. + */ +void minigb_apu_audio_write(struct minigb_apu_ctx *ctx, + const uint16_t addr, const uint8_t val); + +/** + * Initialise audio driver. + * \param ctx Library context. + */ +void minigb_apu_audio_init(struct minigb_apu_ctx *ctx); diff --git a/Firmware/sdk/apps/gameboy/src/gameboy_app.cpp b/Firmware/sdk/apps/gameboy/src/gameboy_app.cpp index c89f210..048599e 100644 --- a/Firmware/sdk/apps/gameboy/src/gameboy_app.cpp +++ b/Firmware/sdk/apps/gameboy/src/gameboy_app.cpp @@ -39,6 +39,7 @@ void audio_write(uint16_t addr, uint8_t value) { #include #include +#include #include #include #include @@ -47,10 +48,75 @@ void audio_write(uint16_t addr, uint8_t value) { #include #include #include +#include #include #include #include #include +#include "esp_pm.h" + +#include "driver/gpio.h" +#include "driver/gptimer.h" +#include "hal/sdm_types.h" +#include "esp_attr.h" +#include "esp_err.h" +#include "esp_heap_caps.h" +#include "esp_log.h" +#include "esp_check.h" +#include "esp_pm.h" +#include "esp_clk_tree.h" +#include "driver/gpio.h" +#include "driver/sdm.h" +#include "hal/gpio_hal.h" +#include "hal/sdm_hal.h" +#include "hal/sdm_ll.h" +#include "hal/hal_utils.h" +#include "soc/sdm_periph.h" + + +struct sdm_group_t { + int group_id; // Group ID, index from 0 + portMUX_TYPE spinlock; // to protect per-group register level concurrent access + sdm_hal_context_t hal; // hal context + sdm_channel_t *channels[SOC_SDM_CHANNELS_PER_GROUP]; // array of sdm channels + sdm_clock_source_t clk_src; // Clock source +}; +struct sdm_platform_t { + _lock_t mutex; // platform level mutex lock + sdm_group_t *groups[SOC_SDM_GROUPS]; // sdm group pool + int group_ref_counts[SOC_SDM_GROUPS]; // reference count used to protect group install/uninstall +}; + +typedef enum { + SDM_FSM_INIT, + SDM_FSM_ENABLE, +} sdm_fsm_t; +#define SDM_PM_LOCK_NAME_LEN_MAX 16 + +struct sdm_channel_t { + sdm_group_t *group; // which group the sdm channel belongs to + uint32_t chan_id; // allocated channel numerical ID + int gpio_num; // GPIO number + uint32_t sample_rate_hz; // Sample rate, in Hz + portMUX_TYPE spinlock; // to protect per-channels resources concurrently accessed by task and ISR handler + esp_pm_lock_handle_t pm_lock; // PM lock, for glitch filter, as that module can only be functional under APB + sdm_fsm_t fsm; // FSM state +#if CONFIG_PM_ENABLE + char pm_lock_name[SDM_PM_LOCK_NAME_LEN_MAX]; // pm lock name +#endif +}; + + +#include "hal/sdm_hal.h" +#include "hal/sdm_ll.h" +#include "driver/sdm.h" +#include "esp_err.h" +#include "esp_log.h" +#include "esp_random.h" + +extern "C" { +#include "minigb_apu/minigb_apu.h" +} #define GAMEBOY_PERF_METRICS 0 @@ -66,6 +132,12 @@ void audio_write(uint16_t addr, uint8_t value) { static constexpr uint8_t kPattern2x2[4] = {0, 2, 3, 1}; // [ (y&1)<<1 | (x&1) ] +static constexpr bool kEnableGbPwmPrototype = true; +static constexpr gpio_num_t kGbPwmPrototypePin = GPIO_NUM_25; +static constexpr uint32_t kGbPwmSampleRateHz = AUDIO_SAMPLE_RATE; +static constexpr uint32_t kGbPwmCarrierHz = 8000000; +static constexpr int kGbSdmHardwareChannels = 4; // ESP32-H2 SDM offers up to four channels per TRM Section 4.4 + // exact predicate per your shouldPixelBeOn static constexpr uint8_t on_bit(uint8_t v, uint8_t threshold) { if (v >= 3) @@ -306,6 +378,8 @@ public: gb_run_frame(&gb); GB_PERF_ONLY(perf.runUs = nowMicros() - runStartUs;) + apu.generateFrameAudio(); + GB_PERF_ONLY(const uint64_t renderStartUs = nowMicros();) renderGameFrame(); GB_PERF_ONLY(perf.renderUs = nowMicros() - renderStartUs;) @@ -326,455 +400,218 @@ public: void audioWriteRegister(uint16_t addr, uint8_t value) { apu.write(addr, value); } private: -public: class SimpleApu { public: + SimpleApu() {} + void attach(GameboyApp* ownerInstance) { owner = ownerInstance; } void reset() { - regs.fill(0); - for (std::size_t i = 0; i < kInitialRegistersCount; ++i) - regs[i] = kInitialRegisters[i]; - for (std::size_t i = 0; i < kInitialWaveCount; ++i) - regs[kWaveOffset + i] = kInitialWave[i]; - regs[kPowerIndex] = 0x80; - enabled = true; - squareAlternate = 0; - lastChannel = 0xFF; - filteredFreqHz = 0.0; - lastSquareRaw = {0, 0}; - squareStable = {0, 0}; + stopAudio(); + minigb_apu_audio_init(&ctx); + frameCursor = kFramesPerBuffer; + lastDensity = 0; } [[nodiscard]] uint8_t read(uint16_t addr) const { - if (!inRange(addr)) - return 0xFF; - const std::size_t idx = static_cast(addr - kBaseAddr); - return static_cast(regs[idx] | kReadMask[idx]); + return minigb_apu_audio_read(const_cast(&ctx), addr); } - void write(uint16_t addr, uint8_t value) { - if (!inRange(addr)) - return; - const std::size_t idx = static_cast(addr - kBaseAddr); + void write(uint16_t addr, uint8_t value) { minigb_apu_audio_write(&ctx, addr, value); } - if (addr == kPowerAddr) { - enabled = (value & 0x80U) != 0; - regs[idx] = static_cast(value & 0x80U); - if (!enabled) { - std::array wave{}; - for (std::size_t i = 0; i < kWaveRamSize; ++i) - wave[i] = regs[kWaveOffset + i]; - regs.fill(0); - for (std::size_t i = 0; i < kWaveRamSize; ++i) - regs[kWaveOffset + i] = wave[i]; - regs[kPowerIndex] = static_cast(value & 0x80U); - squareAlternate = 0; - lastChannel = 0xFF; - filteredFreqHz = 0.0; - lastSquareRaw = {0, 0}; - squareStable = {0, 0}; - } + void generateFrameAudio() { + if (!kEnableGbPwmPrototype) + return; + minigb_apu_audio_callback(&ctx, playbackBuf.data()); + // pendingReady = true; + } + + void startAudio() { + if (!kEnableGbPwmPrototype) + return; + if (audioRunning) + return; + stopAudio(); + { + // std::lock_guard lock(bufferMutex); + pendingReady = false; + frameCursor = kFramesPerBuffer; + } + generateFrameAudio(); + frameCursor = 0; + + sdm_config_t config{}; + config.gpio_num = static_cast(kGbPwmPrototypePin); + config.clk_src = SDM_CLK_SRC_DEFAULT; + config.sample_rate_hz = kGbPwmCarrierHz; + + esp_err_t err = sdm_new_channel(&config, &sdmChannel); + if (err != ESP_OK) { + ESP_LOGE(kAudioLogTag, "sdm_new_channel failed (%s)", esp_err_to_name(err)); + sdmChannel = nullptr; + return; + } + err = sdm_channel_enable(sdmChannel); + if (err != ESP_OK) { + ESP_LOGE(kAudioLogTag, "sdm_channel_enable failed (%s)", esp_err_to_name(err)); + sdm_del_channel(sdmChannel); + sdmChannel = nullptr; return; } - if (!enabled) { - if (addr >= kWaveBase && addr <= kWaveEnd) - regs[idx] = value; + auto cleanupTimer = [&]() { + if (!audioTimer) + return; + gptimer_stop(audioTimer); + gptimer_disable(audioTimer); + gptimer_del_timer(audioTimer); + audioTimer = nullptr; + }; + + gptimer_config_t timerConfig{}; + timerConfig.clk_src = GPTIMER_CLK_SRC_DEFAULT; + timerConfig.direction = GPTIMER_COUNT_UP; + timerConfig.resolution_hz = kTimerResolutionHz; + + err = gptimer_new_timer(&timerConfig, &audioTimer); + if (err != ESP_OK) { + ESP_LOGE(kAudioLogTag, "gptimer_new_timer failed (%s)", esp_err_to_name(err)); + sdm_channel_disable(sdmChannel); + sdm_del_channel(sdmChannel); + sdmChannel = nullptr; return; } - regs[idx] = value; - - if ((addr == kCh1TriggerAddr && (value & 0x80U)) || (addr == kCh2TriggerAddr && (value & 0x80U))) { - const int channelIndex = (addr == kCh1TriggerAddr) ? 0 : 1; - // Reflect channel enable in NR52 status bits (no immediate beep; handled in per-frame mixer) - regs[kPowerIndex] = static_cast((regs[kPowerIndex] & 0xF0U) | 0x80U | (1U << channelIndex)); + gptimer_event_callbacks_t callbacks{}; + callbacks.on_alarm = &SimpleApu::audioTimerThunk; + err = gptimer_register_event_callbacks(audioTimer, &callbacks, this); + if (err != ESP_OK) { + ESP_LOGE(kAudioLogTag, "gptimer_register_event_callbacks failed (%s)", esp_err_to_name(err)); + cleanupTimer(); + sdm_channel_disable(sdmChannel); + sdm_del_channel(sdmChannel); + sdmChannel = nullptr; + return; } + + const uint32_t ticksPerSample = std::max(1, timerConfig.resolution_hz / kGbPwmSampleRateHz); + + gptimer_alarm_config_t alarmConfig{}; + alarmConfig.reload_count = 0; + alarmConfig.alarm_count = ticksPerSample; + alarmConfig.flags.auto_reload_on_alarm = true; + + err = gptimer_set_alarm_action(audioTimer, &alarmConfig); + if (err != ESP_OK) { + ESP_LOGE(kAudioLogTag, "gptimer_set_alarm_action failed (%s)", esp_err_to_name(err)); + cleanupTimer(); + sdm_channel_disable(sdmChannel); + sdm_del_channel(sdmChannel); + sdmChannel = nullptr; + return; + } + + err = gptimer_enable(audioTimer); + if (err != ESP_OK) { + ESP_LOGE(kAudioLogTag, "gptimer_enable failed (%s)", esp_err_to_name(err)); + cleanupTimer(); + sdm_channel_disable(sdmChannel); + sdm_del_channel(sdmChannel); + sdmChannel = nullptr; + return; + } + + err = gptimer_start(audioTimer); + if (err != ESP_OK) { + ESP_LOGE(kAudioLogTag, "gptimer_start failed (%s)", esp_err_to_name(err)); + cleanupTimer(); + sdm_channel_disable(sdmChannel); + sdm_del_channel(sdmChannel); + sdmChannel = nullptr; + return; + } + + audioRunning = true; + lastDensity = 0; + sdm_channel_set_pulse_density(sdmChannel, 0); + ESP_LOGI(kAudioLogTag, "sigma-delta audio active on GPIO%d (%u Hz sample, %u Hz carrier)", + static_cast(kGbPwmPrototypePin), kGbPwmSampleRateHz, kGbPwmCarrierHz); + } + + void stopAudio() { + if (audioTimer) { + gptimer_stop(audioTimer); + gptimer_disable(audioTimer); + gptimer_del_timer(audioTimer); + audioTimer = nullptr; + } + if (sdmChannel) { + sdm_channel_set_pulse_density(sdmChannel, 0); + sdm_channel_disable(sdmChannel); + sdm_del_channel(sdmChannel); + sdmChannel = nullptr; + } + audioRunning = false; + frameCursor = kFramesPerBuffer; + lastDensity = 0; + } + + bool computeEffectiveTone(uint32_t& outFreqHz, uint8_t& outLoudness) const { + (void) outFreqHz; + (void) outLoudness; + return false; } private: - static constexpr uint16_t kBaseAddr = 0xFF10; - static constexpr std::size_t kRegisterCount = 0x30; - static constexpr uint16_t kPowerAddr = 0xFF26; - static constexpr std::size_t kPowerIndex = static_cast(kPowerAddr - kBaseAddr); - static constexpr uint16_t kCh1LenAddr = 0xFF11; - static constexpr uint16_t kCh1EnvAddr = 0xFF12; - static constexpr uint16_t kCh1FreqLoAddr = 0xFF13; - static constexpr uint16_t kCh1TriggerAddr = 0xFF14; - static constexpr uint16_t kCh2LenAddr = 0xFF16; - static constexpr uint16_t kCh2EnvAddr = 0xFF17; - static constexpr uint16_t kCh2FreqLoAddr = 0xFF18; - static constexpr uint16_t kCh2TriggerAddr = 0xFF19; - static constexpr uint16_t kCh3EnableAddr = 0xFF1A; - static constexpr uint16_t kCh3LevelAddr = 0xFF1C; - static constexpr uint16_t kCh3FreqLoAddr = 0xFF1D; - static constexpr uint16_t kCh3TriggerAddr = 0xFF1E; - static constexpr uint16_t kCh4EnvAddr = 0xFF21; - static constexpr uint16_t kCh4PolyAddr = 0xFF22; - static constexpr uint16_t kCh4TriggerAddr = 0xFF23; - static constexpr uint16_t kVolumeAddr = 0xFF24; - static constexpr uint16_t kRoutingAddr = 0xFF25; - static constexpr uint16_t kWaveBase = 0xFF30; - static constexpr uint16_t kWaveEnd = 0xFF3F; - static constexpr std::size_t kWaveOffset = static_cast(kWaveBase - kBaseAddr); - static constexpr std::size_t kWaveRamSize = static_cast(kWaveEnd - kWaveBase + 1); - static constexpr uint8_t kReadMask[kRegisterCount] = { - 0x80, 0x3F, 0x00, 0xFF, 0xBF, 0xFF, 0x3F, 0x00, 0xFF, 0xBF, 0x7F, 0xFF, 0x9F, 0xFF, 0xBF, 0xFF, - 0xFF, 0x00, 0x00, 0xBF, 0x00, 0x00, 0x70, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; - static constexpr std::size_t kInitialRegistersCount = 0x17; - static constexpr uint8_t kInitialRegisters[kInitialRegistersCount] = { - 0x80, 0xBF, 0xF3, 0xFF, 0x3F, 0xFF, 0x3F, 0x00, 0xFF, 0x3F, 0x7F, 0xFF, - 0x9F, 0xFF, 0x3F, 0xFF, 0xFF, 0x00, 0x00, 0x3F, 0x77, 0xF3, 0xF1}; - static constexpr std::size_t kInitialWaveCount = 16; - static constexpr uint8_t kInitialWave[kInitialWaveCount] = {0xAC, 0xDD, 0xDA, 0x48, 0x36, 0x02, 0xCF, 0x16, - 0x2C, 0x04, 0xE5, 0x2C, 0xAC, 0xDD, 0xDA, 0x48}; + static constexpr const char* kAudioLogTag = "gb_sdm"; + static constexpr uint16_t kBaseAddr = 0xFF10; + static constexpr uint16_t kEndAddr = 0xFF3F; + static constexpr std::size_t kFramesPerBuffer = AUDIO_SAMPLES; + static constexpr std::size_t kSamplesPerBuffer = AUDIO_SAMPLES_TOTAL; + static constexpr uint32_t kTimerResolutionHz = 1'000'000; - enum class Channel : uint8_t { Square1 = 0, Square2 = 1, Wave = 2, Noise = 3 }; + static constexpr bool inRange(uint16_t addr) { return addr >= kBaseAddr && addr <= kEndAddr; } - GameboyApp* owner = nullptr; - std::array regs{}; - bool enabled = true; - mutable uint8_t squareAlternate = 0; - mutable uint8_t lastChannel = 0xFF; - mutable double filteredFreqHz = 0.0; - mutable std::array lastSquareRaw{}; - mutable std::array squareStable{}; - - static constexpr bool inRange(uint16_t addr) { - return addr >= kBaseAddr && addr <= (kBaseAddr + static_cast(kRegisterCount) - 1); - } - - [[nodiscard]] uint8_t reg(uint16_t addr) const { return regs[static_cast(addr - kBaseAddr)]; } - - [[nodiscard]] double squareFrequency(int channelIndex, uint16_t* outRaw = nullptr) const { - const uint16_t freqLoAddr = (channelIndex == 0) ? kCh1FreqLoAddr : kCh2FreqLoAddr; - const uint16_t freqHiAddr = (channelIndex == 0) ? kCh1TriggerAddr : kCh2TriggerAddr; - const uint16_t raw = static_cast(((reg(freqHiAddr) & 0x07U) << 8) | reg(freqLoAddr)); - if (outRaw) - *outRaw = raw; - if (raw >= 2048U) - return 0.0; - const double denom = static_cast(2048U - raw); - if (denom <= 0.0) - return 0.0; - return 131072.0 / denom; - } - - [[nodiscard]] static double snapNoiseFrequency(double freq) { - static constexpr double kNoisePreferredHz[] = {650.0, 820.0, 990.0, 1200.0, 1500.0, - 1850.0, 2200.0, 2600.0, 3100.0, 3600.0}; - if (!(freq > 0.0)) - return freq; - double bestFreq = freq; - double bestDiff = 1.0e9; - for (double target: kNoisePreferredHz) { - double diff = freq - target; - if (diff < 0.0) - diff = -diff; - if (diff < bestDiff) { - bestDiff = diff; - bestFreq = target; - } - } - if (bestDiff <= 500.0) - return bestFreq; - return freq; - } - - // Mixer: compute best single-tone approximation for the buzzer. - // Returns true if a tone is suggested, with outFreqHz set. - public: - bool computeEffectiveTone(uint32_t& outFreqHz, uint8_t& outLoudness) const { - const uint8_t nr50 = reg(kVolumeAddr); - const uint8_t master = static_cast(std::max(nr50 & 0x07U, (nr50 >> 4) & 0x07U)); - if (master == 0) { - filteredFreqHz = 0.0; - lastChannel = 0xFF; - return false; - } - const uint8_t routing = reg(kRoutingAddr); - - struct Candidate { - double freq; - uint8_t loud; - int prio; - Channel channel; - }; - - Candidate candidates[4]; - std::size_t candidateCount = 0; - constexpr std::size_t kMaxCandidates = sizeof(candidates) / sizeof(candidates[0]); - - // Track how stable each square channel's raw frequency is so we can bias selection. - auto updateSquareHistory = [&](int idx, uint16_t raw) { - if (raw == 0 || raw >= 2048U) { - squareStable[static_cast(idx)] = 0; - lastSquareRaw[static_cast(idx)] = 0; - return; - } - if (lastSquareRaw[static_cast(idx)] == raw) { - const uint8_t current = squareStable[static_cast(idx)]; - if (current < 0xFD) - squareStable[static_cast(idx)] = static_cast(current + 1); - } else { - lastSquareRaw[static_cast(idx)] = raw; - squareStable[static_cast(idx)] = 0; - } - }; - - bool squareActive[2] = {false, false}; - - auto pushCandidate = [&](double freq, uint8_t loud, int prio, Channel channel) { - if (candidateCount >= kMaxCandidates) - return; - if (!std::isfinite(freq) || freq <= 10.0 || loud == 0) - return; - candidates[candidateCount++] = Candidate{freq, loud, prio, channel}; - }; - -#if GB_BUZZER_ENABLE_CH1 - if ((reg(kPowerAddr) & 0x01U) != 0) { - const uint8_t env = reg(kCh1EnvAddr); - const uint8_t vol4 = (env >> 4) & 0x0FU; - const bool routed = ((routing & 0x11U) != 0); - if (vol4 && routed) { - uint16_t raw = 0; - const double freq = squareFrequency(0, &raw); - const uint8_t loud = static_cast(vol4 * master); - if (freq > 0.0) { - squareActive[0] = true; - updateSquareHistory(0, raw); - pushCandidate(freq, loud, 3, Channel::Square1); - } else { - squareStable[0] = 0; - lastSquareRaw[0] = 0; - } - } - } -#endif -#if GB_BUZZER_ENABLE_CH2 - if ((reg(kPowerAddr) & 0x02U) != 0) { - const uint8_t env = reg(kCh2EnvAddr); - const uint8_t vol4 = (env >> 4) & 0x0FU; - const bool routed = ((routing & 0x22U) != 0); - if (vol4 && routed) { - uint16_t raw = 0; - const double freq = squareFrequency(1, &raw); - const uint8_t loud = static_cast(vol4 * master); - if (freq > 0.0) { - squareActive[1] = true; - updateSquareHistory(1, raw); - pushCandidate(freq, loud, 3, Channel::Square2); - } else { - squareStable[1] = 0; - lastSquareRaw[1] = 0; - } - } - } -#endif -#if GB_BUZZER_ENABLE_CH3 - if ((reg(kPowerAddr) & 0x04U) != 0 && (reg(kCh3EnableAddr) & 0x80U) != 0) { - const uint8_t levelSel = (reg(kCh3LevelAddr) >> 5) & 0x03U; - const bool routed = ((routing & 0x44U) != 0); - uint8_t loudBase = 0; - if (levelSel == 1) - loudBase = 16; - else if (levelSel == 2) - loudBase = 8; - else if (levelSel == 3) - loudBase = 4; - if (levelSel != 0 && routed && loudBase != 0) { - const uint16_t raw = - static_cast(((reg(kCh3TriggerAddr) & 0x07U) << 8) | reg(kCh3FreqLoAddr)); - if (raw < 2048U) { - const double denom = static_cast(2048U - raw); - const double freq = 2097152.0 / denom; - const uint8_t loud = static_cast(loudBase * master); - pushCandidate(freq, loud, 2, Channel::Wave); - } - } - } -#endif -#if GB_BUZZER_ENABLE_CH4 - if ((reg(kPowerAddr) & 0x08U) != 0) { - const bool routed = ((routing & 0x88U) != 0); - const uint8_t env = reg(kCh4EnvAddr); - const uint8_t vol4 = (env >> 4) & 0x0FU; - if (vol4 && routed) { - const uint8_t nr43 = reg(kCh4PolyAddr); - const uint8_t shift = (nr43 >> 4) & 0x0FU; - const uint8_t dividerId = nr43 & 0x07U; - static const int divLut[8] = {8, 16, 32, 48, 64, 80, 96, 112}; - const int div = divLut[dividerId]; - double freq = - 4194304.0 / (static_cast(div) * std::pow(2.0, static_cast(shift + 1))); - freq = snapNoiseFrequency(std::clamp(freq, 600.0, 3600.0)); - const uint8_t loud = static_cast(vol4 * master); - pushCandidate(freq, loud, 1, Channel::Noise); - } - } -#endif - - if (candidateCount == 0) { - lastChannel = 0xFF; - filteredFreqHz = 0.0; - return false; - } - - for (int idx = 0; idx < 2; ++idx) { - if (!squareActive[idx]) { - squareStable[static_cast(idx)] = 0; - lastSquareRaw[static_cast(idx)] = 0; - } - } - - const Candidate* squareCandidates[2] = {nullptr, nullptr}; - const Candidate* waveCandidate = nullptr; - bool waveBass = false; - const Candidate* bestOther = nullptr; - int bestOtherScore = -1; - - for (std::size_t i = 0; i < candidateCount; ++i) { - const Candidate* cand = &candidates[i]; - if (cand->channel == Channel::Square1) - squareCandidates[0] = cand; - else if (cand->channel == Channel::Square2) - squareCandidates[1] = cand; - else { - if (cand->channel == Channel::Wave) { - waveCandidate = cand; - waveBass = (cand->freq > 0.0 && cand->freq < 220.0); - } - int score = static_cast(cand->loud); - if (waveBass) { - if (cand->channel == Channel::Wave) - score += 3; - else if (cand->channel == Channel::Noise) - score -= 1; - } - if (score < 0) - score = 0; - if (!bestOther || score > bestOtherScore || - (score == bestOtherScore && cand->prio > bestOther->prio) || - (score == bestOtherScore && cand->prio == bestOther->prio && cand->freq > bestOther->freq)) { - bestOther = cand; - bestOtherScore = score; - } - } - } - - const Candidate* bestSquare = nullptr; - if (squareCandidates[0] && squareCandidates[1]) { - int loudDiff = - static_cast(squareCandidates[0]->loud) - static_cast(squareCandidates[1]->loud); - if (loudDiff < 0) - loudDiff = -loudDiff; - const int stable0 = static_cast(squareStable[0]); - const int stable1 = static_cast(squareStable[1]); - const int stableMargin = waveBass ? 0 : 2; - if (stable0 > stable1 + stableMargin) - bestSquare = squareCandidates[0]; - else if (stable1 > stable0 + stableMargin) - bestSquare = squareCandidates[1]; - else if (loudDiff > 2) { - bestSquare = (squareCandidates[0]->loud > squareCandidates[1]->loud) ? squareCandidates[0] - : squareCandidates[1]; - } else { - if (waveBass && stable0 != stable1) - bestSquare = (stable0 >= stable1) ? squareCandidates[0] : squareCandidates[1]; - if (!bestSquare && stable0 <= 1 && stable1 <= 1) { - if (lastChannel == static_cast(Channel::Square1)) - bestSquare = squareCandidates[0]; - else if (lastChannel == static_cast(Channel::Square2)) - bestSquare = squareCandidates[1]; - } - if (!bestSquare) { - const Candidate* preferred = (squareAlternate & 1U) ? squareCandidates[1] : squareCandidates[0]; - bestSquare = preferred; - squareAlternate ^= 1U; - } - } - } else if (squareCandidates[0] || squareCandidates[1]) { - bestSquare = squareCandidates[0] ? squareCandidates[0] : squareCandidates[1]; - } - - const Candidate* best = bestSquare; - if (!best) - best = bestOther; - else if (bestOther) { - int bestScore = static_cast(best->loud); - int otherScore = static_cast(bestOther->loud); - if (waveBass) { - if (best->channel == Channel::Wave) - bestScore += 3; - else if (best->channel == Channel::Noise) - bestScore -= 1; - else if (best->channel == Channel::Square1 || best->channel == Channel::Square2) - bestScore -= 2; - if (bestOther->channel == Channel::Wave) - otherScore += 3; - else if (bestOther->channel == Channel::Noise) - otherScore -= 1; - else if (bestOther->channel == Channel::Square1 || bestOther->channel == Channel::Square2) - otherScore -= 2; - } - if (bestScore < 0) - bestScore = 0; - if (otherScore < 0) - otherScore = 0; - if (otherScore > bestScore || (otherScore == bestScore && bestOther->prio > best->prio) || - (otherScore == bestScore && bestOther->prio == best->prio && - static_cast(bestOther->channel) == lastChannel && - static_cast(best->channel) != lastChannel)) - best = bestOther; - } - - if (waveBass && waveCandidate && best && best->channel != Channel::Wave) { - int waveScore = static_cast(waveCandidate->loud) + 3; - int bestScore = static_cast(best->loud); - if (best->channel == Channel::Noise) - bestScore -= 1; - else if (best->channel == Channel::Square1 || best->channel == Channel::Square2) - bestScore -= 2; - if (waveScore < 0) - waveScore = 0; - if (bestScore < 0) - bestScore = 0; - if (waveScore >= bestScore) - best = waveCandidate; - } - - if (!best) - return false; - - double selectedFreq = best->freq; - if (!(selectedFreq > 0.0) || !std::isfinite(selectedFreq)) - return false; - - const double prevFiltered = filteredFreqHz; - if (!(prevFiltered > 0.0) || !std::isfinite(prevFiltered) || - static_cast(best->channel) != lastChannel) { - filteredFreqHz = selectedFreq; - } else { - double diff = selectedFreq - prevFiltered; - if (diff < 0.0 && -diff > 1200.0) - filteredFreqHz = selectedFreq; - else if (diff > 1200.0) - filteredFreqHz = selectedFreq; - else { - double alpha = (best->channel == Channel::Noise) ? 0.45 : 0.35; - filteredFreqHz = prevFiltered + (diff * alpha); - } - } - - const double clamped = std::clamp(filteredFreqHz, 40.0, 5500.0); - outFreqHz = static_cast(clamped + 0.5); - outLoudness = best->loud; - lastChannel = static_cast(best->channel); + static IRAM_ATTR bool audioTimerThunk(gptimer_handle_t, const gptimer_alarm_event_data_t*, void* userData) { + auto* self = static_cast(userData); + if (self) + self->handleAudioTick(); return true; } + + IRAM_ATTR void handleAudioTick() { + if (!audioRunning || !sdmChannel) + return; + + if (frameCursor >= kFramesPerBuffer) { + frameCursor = 0; + } + + const std::size_t offset = frameCursor * 2; + ++frameCursor; + + const audio_sample_t sample = playbackBuf[offset]; + + int32_t density = sample >> 7; + if (density > 127) + density = 127; + else if (density < -128) + density = -128; + sdm_group_t *group = sdmChannel->group; + int chan_id = sdmChannel->chan_id; + + sdm_ll_set_pulse_density(group->hal.dev, chan_id, density); + } + + GameboyApp* owner = nullptr; + minigb_apu_ctx ctx{}; + std::array playbackBuf; + bool pendingReady = false; + std::size_t frameCursor = kFramesPerBuffer; + sdm_channel_handle_t sdmChannel = nullptr; + gptimer_handle_t audioTimer = nullptr; + bool audioRunning = false; + int8_t lastDensity = 0; }; enum class Mode { Browse, Running }; @@ -1448,6 +1285,8 @@ public: frameDirty = true; activeRomName = rom.name.empty() ? "Game" : rom.name; + apu.startAudio(); + std::string statusText = "Running " + activeRomName; if (!fsReady) statusText.append(" (no save)"); @@ -1464,12 +1303,15 @@ public: cartRam.clear(); activeRomName.clear(); activeRomSavePath.clear(); + apu.stopAudio(); return; } maybeSaveRam(); resetFpsStats(); + apu.stopAudio(); + gbReady = false; romData.clear(); romDataView = nullptr; @@ -1687,14 +1529,6 @@ public: setStatus("Partial save loaded"); } } - - void playTone(uint32_t freqHz, uint32_t durationMs, uint32_t gapMs) { - if (freqHz == 0 || durationMs == 0) - return; - if (auto* buzzer = context.buzzer()) - buzzer->tone(freqHz, durationMs, gapMs); - } - static uint8_t audioReadThunk(void* ctx, uint16_t addr) { auto* self = static_cast(ctx); return self ? self->audioReadRegister(addr) : 0xFF; @@ -1926,35 +1760,6 @@ private: drawLineOriginal(*self, pixels, static_cast(line)); break; } - - // Simple per-scanline hook: at end of last line, decide tone for the frame. - if (line + 1 == LCD_HEIGHT) { - uint32_t freqHz = 0; - uint8_t loud = 0; - if (self->apu.computeEffectiveTone(freqHz, loud)) { - // Basic smoothing: if freq didn't change much, keep it; otherwise snap quickly - const uint32_t prev = self->lastFreqHz; - if (prev != 0 && freqHz != 0) { - const uint32_t diff = (prev > freqHz) ? (prev - freqHz) : (freqHz - prev); - if (diff < 15) { - freqHz = prev; // minor jitter suppression - ++self->stableFrames; - } else { - self->stableFrames = 0; - } - } else { - self->stableFrames = 0; - } - self->lastFreqHz = freqHz; - self->lastLoud = loud; - const uint32_t durMs = 17; - self->playTone(freqHz, durMs, 0); - } else { - self->lastFreqHz = 0; - self->lastLoud = 0; - // Don't enqueue anything; queue naturally drains and buzzer stops - } - } } static const char* initErrorToString(enum gb_init_error_e err) {