mirror of
https://github.com/usatiuk/cardboy.git
synced 2025-10-28 15:17:48 +01:00
Compare commits
1 Commits
5e63875d35
...
sound
| Author | SHA1 | Date | |
|---|---|---|---|
| 7a5d1c2819 |
@@ -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
|
||||
|
||||
@@ -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
|
||||
)
|
||||
542
Firmware/sdk/apps/gameboy/minigb_apu/minigb_apu.c
Normal file
542
Firmware/sdk/apps/gameboy/minigb_apu/minigb_apu.c
Normal file
@@ -0,0 +1,542 @@
|
||||
/**
|
||||
* Game Boy APU emulator.
|
||||
* Copyright (c) 2019 Mahyar Koshkouei <mk@deltabeard.com>
|
||||
* Copyright (c) 2017 Alex Baines <alex@abaines.me.uk>
|
||||
* 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 <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
|
||||
#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]);
|
||||
}
|
||||
}
|
||||
149
Firmware/sdk/apps/gameboy/minigb_apu/minigb_apu.h
Normal file
149
Firmware/sdk/apps/gameboy/minigb_apu/minigb_apu.h
Normal file
@@ -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 <stdint.h>
|
||||
|
||||
#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);
|
||||
@@ -39,6 +39,7 @@ void audio_write(uint16_t addr, uint8_t value) {
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <cctype>
|
||||
#include <cerrno>
|
||||
#include <chrono>
|
||||
@@ -47,10 +48,75 @@ void audio_write(uint16_t addr, uint8_t value) {
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <dirent.h>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <sys/stat.h>
|
||||
#include <vector>
|
||||
#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<std::size_t>(addr - kBaseAddr);
|
||||
return static_cast<uint8_t>(regs[idx] | kReadMask[idx]);
|
||||
return minigb_apu_audio_read(const_cast<minigb_apu_ctx*>(&ctx), addr);
|
||||
}
|
||||
|
||||
void write(uint16_t addr, uint8_t value) {
|
||||
if (!inRange(addr))
|
||||
return;
|
||||
const std::size_t idx = static_cast<std::size_t>(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<uint8_t>(value & 0x80U);
|
||||
if (!enabled) {
|
||||
std::array<uint8_t, kWaveRamSize> 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<uint8_t>(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<std::mutex> lock(bufferMutex);
|
||||
pendingReady = false;
|
||||
frameCursor = kFramesPerBuffer;
|
||||
}
|
||||
generateFrameAudio();
|
||||
frameCursor = 0;
|
||||
|
||||
sdm_config_t config{};
|
||||
config.gpio_num = static_cast<int>(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<uint8_t>((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<uint32_t>(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<int>(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<std::size_t>(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<std::size_t>(kWaveBase - kBaseAddr);
|
||||
static constexpr std::size_t kWaveRamSize = static_cast<std::size_t>(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<uint8_t, kRegisterCount> regs{};
|
||||
bool enabled = true;
|
||||
mutable uint8_t squareAlternate = 0;
|
||||
mutable uint8_t lastChannel = 0xFF;
|
||||
mutable double filteredFreqHz = 0.0;
|
||||
mutable std::array<uint16_t, 2> lastSquareRaw{};
|
||||
mutable std::array<uint8_t, 2> squareStable{};
|
||||
|
||||
static constexpr bool inRange(uint16_t addr) {
|
||||
return addr >= kBaseAddr && addr <= (kBaseAddr + static_cast<uint16_t>(kRegisterCount) - 1);
|
||||
}
|
||||
|
||||
[[nodiscard]] uint8_t reg(uint16_t addr) const { return regs[static_cast<std::size_t>(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<uint16_t>(((reg(freqHiAddr) & 0x07U) << 8) | reg(freqLoAddr));
|
||||
if (outRaw)
|
||||
*outRaw = raw;
|
||||
if (raw >= 2048U)
|
||||
return 0.0;
|
||||
const double denom = static_cast<double>(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<uint8_t>(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<std::size_t>(idx)] = 0;
|
||||
lastSquareRaw[static_cast<std::size_t>(idx)] = 0;
|
||||
return;
|
||||
}
|
||||
if (lastSquareRaw[static_cast<std::size_t>(idx)] == raw) {
|
||||
const uint8_t current = squareStable[static_cast<std::size_t>(idx)];
|
||||
if (current < 0xFD)
|
||||
squareStable[static_cast<std::size_t>(idx)] = static_cast<uint8_t>(current + 1);
|
||||
} else {
|
||||
lastSquareRaw[static_cast<std::size_t>(idx)] = raw;
|
||||
squareStable[static_cast<std::size_t>(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<uint8_t>(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<uint8_t>(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<uint16_t>(((reg(kCh3TriggerAddr) & 0x07U) << 8) | reg(kCh3FreqLoAddr));
|
||||
if (raw < 2048U) {
|
||||
const double denom = static_cast<double>(2048U - raw);
|
||||
const double freq = 2097152.0 / denom;
|
||||
const uint8_t loud = static_cast<uint8_t>(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<double>(div) * std::pow(2.0, static_cast<double>(shift + 1)));
|
||||
freq = snapNoiseFrequency(std::clamp(freq, 600.0, 3600.0));
|
||||
const uint8_t loud = static_cast<uint8_t>(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<std::size_t>(idx)] = 0;
|
||||
lastSquareRaw[static_cast<std::size_t>(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<int>(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<int>(squareCandidates[0]->loud) - static_cast<int>(squareCandidates[1]->loud);
|
||||
if (loudDiff < 0)
|
||||
loudDiff = -loudDiff;
|
||||
const int stable0 = static_cast<int>(squareStable[0]);
|
||||
const int stable1 = static_cast<int>(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<uint8_t>(Channel::Square1))
|
||||
bestSquare = squareCandidates[0];
|
||||
else if (lastChannel == static_cast<uint8_t>(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<int>(best->loud);
|
||||
int otherScore = static_cast<int>(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<uint8_t>(bestOther->channel) == lastChannel &&
|
||||
static_cast<uint8_t>(best->channel) != lastChannel))
|
||||
best = bestOther;
|
||||
}
|
||||
|
||||
if (waveBass && waveCandidate && best && best->channel != Channel::Wave) {
|
||||
int waveScore = static_cast<int>(waveCandidate->loud) + 3;
|
||||
int bestScore = static_cast<int>(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<uint8_t>(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<uint32_t>(clamped + 0.5);
|
||||
outLoudness = best->loud;
|
||||
lastChannel = static_cast<uint8_t>(best->channel);
|
||||
static IRAM_ATTR bool audioTimerThunk(gptimer_handle_t, const gptimer_alarm_event_data_t*, void* userData) {
|
||||
auto* self = static_cast<SimpleApu*>(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<audio_sample_t, kSamplesPerBuffer> 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<GameboyApp*>(ctx);
|
||||
return self ? self->audioReadRegister(addr) : 0xFF;
|
||||
@@ -1926,35 +1760,6 @@ private:
|
||||
drawLineOriginal(*self, pixels, static_cast<int>(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) {
|
||||
|
||||
Reference in New Issue
Block a user