From 6bbeb33321e34953adc9bce183f69de35b6565eb Mon Sep 17 00:00:00 2001 From: Mahyar Koshkouei Date: Sat, 1 Feb 2025 12:32:39 +0000 Subject: [PATCH] sdl: update minigb-apu Signed-off-by: Mahyar Koshkouei --- examples/sdl2/CMakeLists.txt | 2 +- examples/sdl2/Makefile | 2 +- examples/sdl2/minigb_apu/minigb_apu.c | 377 +++++++++++--------------- examples/sdl2/minigb_apu/minigb_apu.h | 124 ++++++++- examples/sdl2/peanut_sdl.c | 26 +- 5 files changed, 299 insertions(+), 232 deletions(-) diff --git a/examples/sdl2/CMakeLists.txt b/examples/sdl2/CMakeLists.txt index a9c3d48..1e509e2 100644 --- a/examples/sdl2/CMakeLists.txt +++ b/examples/sdl2/CMakeLists.txt @@ -92,7 +92,7 @@ ADD_COMPILE_DEFINITIONS(ICON_FILE=${CMAKE_SOURCE_DIR}/meta/icon.ico) IF(${ENABLE_SOUND}) ADD_COMPILE_DEFINITIONS(ENABLE_SOUND=1) - ADD_COMPILE_DEFINITIONS(ENABLE_SOUND_MINIGB) + ADD_COMPILE_DEFINITIONS(ENABLE_SOUND_MINIGB MINIGB_APU_AUDIO_FORMAT_S16SYS) ELSE() ADD_COMPILE_DEFINITIONS(ENABLE_SOUND=0) ENDIF() diff --git a/examples/sdl2/Makefile b/examples/sdl2/Makefile index 9721393..26270ae 100644 --- a/examples/sdl2/Makefile +++ b/examples/sdl2/Makefile @@ -10,7 +10,7 @@ CPPFLAGS := -DCOMPANY=Deltabeard \ -DLICENSE="$(LICENSE_SPDX)" \ -DNAME="$(NAME)" \ -DICON_FILE=./meta/icon.ico \ - -DENABLE_SOUND -DENABLE_SOUND_MINIGB + -DENABLE_SOUND -DENABLE_SOUND_MINIGB -DMINIGB_APU_AUDIO_FORMAT_S16SYS OPT := -O2 -Wall -Wextra CFLAGS := $(OPT) $(shell sdl2-config --cflags) diff --git a/examples/sdl2/minigb_apu/minigb_apu.c b/examples/sdl2/minigb_apu/minigb_apu.c index 74eb7d0..8065903 100644 --- a/examples/sdl2/minigb_apu/minigb_apu.c +++ b/examples/sdl2/minigb_apu/minigb_apu.c @@ -1,5 +1,8 @@ /** - * minigb_apu is released under the terms listed within the LICENSE file. + * 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 @@ -12,108 +15,42 @@ #include "minigb_apu.h" #define DMG_CLOCK_FREQ_U ((unsigned)DMG_CLOCK_FREQ) -#define AUDIO_NSAMPLES (AUDIO_SAMPLES * 2u) - -#define AUDIO_MEM_SIZE (0xFF3F - 0xFF10 + 1) -#define AUDIO_ADDR_COMPENSATION 0xFF10 +#define AUDIO_NSAMPLES (AUDIO_SAMPLES_TOTAL) #define MAX(a, b) ( a > b ? a : b ) #define MIN(a, b) ( a <= b ? a : b ) -#define VOL_INIT_MAX (INT16_MAX/8) -#define VOL_INIT_MIN (INT16_MIN/8) - +/* 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 * 16) +#define FREQ_INC_REF (AUDIO_SAMPLE_RATE * FREQ_INC_MULT) #define MAX_CHAN_VOLUME 15 -/** - * Memory holding audio registers between 0xFF10 and 0xFF3F inclusive. - */ -static uint8_t audio_mem[AUDIO_MEM_SIZE]; - -struct chan_len_ctr { - uint8_t load; - unsigned enabled : 1; - uint32_t counter; - uint32_t inc; -}; - -struct chan_vol_env { - uint8_t step; - unsigned up : 1; - uint32_t counter; - uint32_t inc; -}; - -struct chan_freq_sweep { - uint16_t freq; - uint8_t rate; - uint8_t shift; - unsigned up : 1; - uint32_t counter; - uint32_t inc; -}; - -static struct chan { - unsigned enabled : 1; - unsigned powered : 1; - unsigned on_left : 1; - unsigned on_right : 1; - unsigned muted : 1; - - uint8_t volume; - uint8_t volume_init; - - uint16_t freq; - uint32_t freq_counter; - uint32_t freq_inc; - - int_fast16_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; - }; -} chans[4]; - -static int32_t vol_l, vol_r; - -static void set_note_freq(struct chan *c, const uint32_t freq) +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(const uint_fast8_t i, const bool enable) +static void chan_enable(struct minigb_apu_ctx *ctx, + const uint_fast8_t i, const bool enable) { uint8_t val; - chans[i].enabled = enable; - val = (audio_mem[0xFF26 - AUDIO_ADDR_COMPENSATION] & 0x80) | - (chans[3].enabled << 3) | (chans[2].enabled << 2) | - (chans[1].enabled << 1) | (chans[0].enabled << 0); + 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); - audio_mem[0xFF26 - AUDIO_ADDR_COMPENSATION] = val; - //audio_mem[0xFF26 - AUDIO_ADDR_COMPENSATION] |= 0x80 | ((uint8_t)enable) << i; + ctx->audio_mem[0xFF26 - AUDIO_ADDR_COMPENSATION] = val; } static void update_env(struct chan *c) @@ -132,14 +69,14 @@ static void update_env(struct chan *c) } } -static void update_len(struct chan *c) +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(c - chans, 0); + chan_enable(ctx, c - ctx->chans, 0); c->len.counter = 0; } } @@ -166,16 +103,15 @@ static void update_sweep(struct chan *c) while (c->sweep.counter > FREQ_INC_REF) { if (c->sweep.shift) { uint16_t inc = (c->sweep.freq >> c->sweep.shift); - if (!c->sweep.up) + if (c->sweep.down) inc *= -1; - c->freq += inc; + c->freq = c->sweep.freq + inc; if (c->freq > 2047) { c->enabled = 0; } else { - set_note_freq(c, - DMG_CLOCK_FREQ_U / ((2048 - c->freq)<< 5)); - c->freq_inc *= 8; + set_note_freq(c); + c->sweep.freq = c->freq; } } else if (c->sweep.rate) { c->enabled = 0; @@ -184,25 +120,25 @@ static void update_sweep(struct chan *c) } } -static void update_square(int16_t* samples, const bool ch2) +static void update_square(struct minigb_apu_ctx *ctx, audio_sample_t *samples, + const bool ch2) { - uint32_t freq; - struct chan* c = chans + ch2; + struct chan *c = &ctx->chans[ch2]; if (!c->powered || !c->enabled) return; - freq = DMG_CLOCK_FREQ_U / ((2048 - c->freq) << 5); - set_note_freq(c, freq); - c->freq_inc *= 8; + set_note_freq(c); for (uint_fast16_t i = 0; i < AUDIO_NSAMPLES; i += 2) { - update_len(c); - + update_len(ctx, c); if (!c->enabled) - continue; + return; update_env(c); + if (!c->volume) + continue; + if (!ch2) update_sweep(c); @@ -219,23 +155,21 @@ static void update_square(int16_t* samples, const bool ch2) prev_pos = pos; } - if (c->muted) - continue; - sample += c->val; sample *= c->volume; sample /= 4; - samples[i + 0] += sample * c->on_left * vol_l; - samples[i + 1] += sample * c->on_right * vol_r; + 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(const unsigned int pos, const unsigned int volume) +static uint8_t wave_sample(struct minigb_apu_ctx *ctx, + const unsigned int pos, const unsigned int volume) { uint8_t sample; - sample = audio_mem[(0xFF30 + pos / 2) - AUDIO_ADDR_COMPENSATION]; + sample = ctx->audio_mem[(0xFF30 + pos / 2) - AUDIO_ADDR_COMPENSATION]; if (pos & 1) { sample &= 0xF; } else { @@ -244,65 +178,58 @@ static uint8_t wave_sample(const unsigned int pos, const unsigned int volume) return volume ? (sample >> (volume - 1)) : 0; } -static void update_wave(int16_t *samples) +static void update_wave(struct minigb_apu_ctx *ctx, audio_sample_t *samples) { - uint32_t freq; - struct chan *c = chans + 2; + struct chan *c = &ctx->chans[2]; - if (!c->powered || !c->enabled) + if (!c->powered || !c->enabled || !c->volume) return; - freq = (DMG_CLOCK_FREQ_U / 64) / (2048 - c->freq); - set_note_freq(c, freq); - - c->freq_inc *= 32; + set_note_freq(c); + c->freq_inc *= 2; for (uint_fast16_t i = 0; i < AUDIO_NSAMPLES; i += 2) { - update_len(c); - + update_len(ctx, c); if (!c->enabled) - continue; + return; - uint32_t pos = 0; + uint32_t pos = 0; uint32_t prev_pos = 0; - int32_t sample = 0; + audio_sample_t sample = 0; - c->wave.sample = wave_sample(c->val, c->volume); + 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) * - ((int)c->wave.sample - 8) * (INT16_MAX/64); - c->wave.sample = wave_sample(c->val, c->volume); + ((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 += ((int)c->wave.sample - 8) * (int)(INT16_MAX/64); - - if (c->volume == 0) - continue; - + sample += ((audio_sample_t)c->wave.sample - 8) * + (audio_sample_t)(AUDIO_SAMPLE_MAX/64); { /* First element is unused. */ - int16_t div[] = { INT16_MAX, 1, 2, 4 }; + audio_sample_t div[] = { AUDIO_SAMPLE_MAX, 1, 2, 4 }; sample = sample / (div[c->volume]); } - if (c->muted) - continue; - sample /= 4; - - samples[i + 0] += sample * c->on_left * vol_l; - samples[i + 1] += sample * c->on_right * vol_r; + samples[i + 0] += sample * c->on_left * ctx->vol_l; + samples[i + 1] += sample * c->on_right * ctx->vol_r; } } -static void update_noise(int16_t *samples) +static void update_noise(struct minigb_apu_ctx *ctx, audio_sample_t *samples) { - struct chan *c = chans + 3; + struct chan *c = &ctx->chans[3]; + + if (c->freq >= 14) + c->enabled = 0; - if (!c->powered) + if (!c->powered || !c->enabled) return; { @@ -312,19 +239,17 @@ static void update_noise(int16_t *samples) uint32_t freq; freq = DMG_CLOCK_FREQ_U / (lfsr_div_lut[c->noise.lfsr_div] << c->freq); - set_note_freq(c, freq); + c->freq_inc = freq * (uint32_t)(FREQ_INC_REF / AUDIO_SAMPLE_RATE); } - if (c->freq >= 14) - c->enabled = 0; - for (uint_fast16_t i = 0; i < AUDIO_NSAMPLES; i += 2) { - update_len(c); - + update_len(ctx, c); if (!c->enabled) - continue; + return; update_env(c); + if (!c->volume) + continue; uint32_t pos = 0; uint32_t prev_pos = 0; @@ -350,66 +275,76 @@ static void update_noise(int16_t *samples) prev_pos = pos; } - if (c->muted) - continue; - sample += c->val; sample *= c->volume; sample /= 4; - samples[i + 0] += sample * c->on_left * vol_l; - samples[i + 1] += sample * c->on_right * vol_r; + 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 audio_callback(void *userdata, uint8_t *stream, int len) +void minigb_apu_audio_callback(struct minigb_apu_ctx *restrict ctx, + audio_sample_t *restrict stream) { - int16_t *samples = (int16_t *)stream; - - /* Appease unused variable warning. */ - (void)userdata; - - memset(stream, 0, len); - - update_square(samples, 0); - update_square(samples, 1); - update_wave(samples); - update_noise(samples); + 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(uint_fast8_t i) +static void chan_trigger(struct minigb_apu_ctx *ctx, uint_fast8_t i) { - struct chan *c = chans + i; + struct chan *c = &ctx->chans[i]; - chan_enable(i, 1); + chan_enable(ctx, i, 1); c->volume = c->volume_init; // volume envelope { - uint8_t val = - audio_mem[(0xFF12 + (i * 5)) - AUDIO_ADDR_COMPENSATION]; - - c->env.step = val & 0x07; - c->env.up = val & 0x08 ? 1 : 0; - c->env.inc = c->env.step ? - (FREQ_INC_REF * 64ul) / ((uint32_t)c->env.step * AUDIO_SAMPLE_RATE) : - (8ul * FREQ_INC_REF) / AUDIO_SAMPLE_RATE ; + /* 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 = audio_mem[0xFF10 - AUDIO_ADDR_COMPENSATION]; + uint8_t val = ctx->audio_mem[0xFF10 - AUDIO_ADDR_COMPENSATION]; c->sweep.freq = c->freq; c->sweep.rate = (val >> 4) & 0x07; - c->sweep.up = !(val & 0x08); + c->sweep.down = (val & 0x08); c->sweep.shift = (val & 0x07); c->sweep.inc = c->sweep.rate ? - ((128 * FREQ_INC_REF) / (c->sweep.rate * AUDIO_SAMPLE_RATE)) : 0; + ((128u * FREQ_INC_REF) / (c->sweep.rate * AUDIO_SAMPLE_RATE)) : 0; c->sweep.counter = FREQ_INC_REF; } @@ -423,7 +358,7 @@ static void chan_trigger(uint_fast8_t i) c->val = VOL_INIT_MIN / MAX_CHAN_VOLUME; } - c->len.inc = (256 * FREQ_INC_REF) / (AUDIO_SAMPLE_RATE * (len_max - c->len.load)); + c->len.inc = (256u * FREQ_INC_REF) / (AUDIO_SAMPLE_RATE * (len_max - c->len.load)); c->len.counter = 0; } @@ -433,7 +368,7 @@ static void chan_trigger(uint_fast8_t i) * This is not checked in this function. * \return Byte at address. */ -uint8_t audio_read(const uint16_t addr) +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, @@ -446,7 +381,7 @@ uint8_t audio_read(const uint16_t addr) 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; - return audio_mem[addr - AUDIO_ADDR_COMPENSATION] | + return ctx->audio_mem[addr - AUDIO_ADDR_COMPENSATION] | ortab[addr - AUDIO_ADDR_COMPENSATION]; } @@ -456,129 +391,131 @@ uint8_t audio_read(const uint16_t addr) * This is not checked in this function. * \param val Byte to write at address. */ -void audio_write(const uint16_t addr, const uint8_t val) +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) { - audio_mem[addr - AUDIO_ADDR_COMPENSATION] = val & 0x80; + 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(audio_mem, 0x00, 0xFF26 - AUDIO_ADDR_COMPENSATION); - chans[0].enabled = false; - chans[1].enabled = false; - chans[2].enabled = false; - chans[3].enabled = false; + 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(audio_mem[0xFF26 - AUDIO_ADDR_COMPENSATION] == 0x00) + if(ctx->audio_mem[0xFF26 - AUDIO_ADDR_COMPENSATION] == 0x00) return; - audio_mem[addr - AUDIO_ADDR_COMPENSATION] = val; + ctx->audio_mem[addr - AUDIO_ADDR_COMPENSATION] = val; i = (addr - AUDIO_ADDR_COMPENSATION) / 5; switch (addr) { case 0xFF12: case 0xFF17: case 0xFF21: { - chans[i].volume_init = val >> 4; - chans[i].powered = (val >> 3) != 0; + 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 (chans[i].powered && chans[i].enabled) { - if ((chans[i].env.step == 0 && chans[i].env.inc != 0)) { + 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) { - chans[i].volume++; + ctx->chans[i].volume++; } else { - chans[i].volume += 2; + ctx->chans[i].volume += 2; } } else { - chans[i].volume = 16 - chans[i].volume; + ctx->chans[i].volume = 16 - ctx->chans[i].volume; } - chans[i].volume &= 0x0F; - chans[i].env.step = val & 0x07; + ctx->chans[i].volume &= 0x0F; + ctx->chans[i].env.step = val & 0x07; } } break; case 0xFF1C: - chans[i].volume = chans[i].volume_init = (val >> 5) & 0x03; + 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 }; - chans[i].len.load = val & 0x3f; - chans[i].square.duty = duty_lookup[val >> 6]; + ctx->chans[i].len.load = val & 0x3f; + ctx->chans[i].square.duty = duty_lookup[val >> 6]; break; } case 0xFF1B: - chans[i].len.load = val; + ctx->chans[i].len.load = val; break; case 0xFF13: case 0xFF18: case 0xFF1D: - chans[i].freq &= 0xFF00; - chans[i].freq |= val; + ctx->chans[i].freq &= 0xFF00; + ctx->chans[i].freq |= val; break; case 0xFF1A: - chans[i].powered = (val & 0x80) != 0; - chan_enable(i, val & 0x80); + ctx->chans[i].powered = (val & 0x80) != 0; + chan_enable(ctx, i, val & 0x80); break; case 0xFF14: case 0xFF19: case 0xFF1E: - chans[i].freq &= 0x00FF; - chans[i].freq |= ((val & 0x07) << 8); + ctx->chans[i].freq &= 0x00FF; + ctx->chans[i].freq |= ((val & 0x07) << 8); /* Intentional fall-through. */ case 0xFF23: - chans[i].len.enabled = val & 0x40 ? 1 : 0; + ctx->chans[i].len.enabled = val & 0x40; if (val & 0x80) - chan_trigger(i); + chan_trigger(ctx, i); break; case 0xFF22: - chans[3].freq = val >> 4; - chans[3].noise.lfsr_wide = !(val & 0x08); - chans[3].noise.lfsr_div = val & 0x07; + 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: { - vol_l = ((val >> 4) & 0x07); - vol_r = (val & 0x07); + ctx->vol_l = ((val >> 4) & 0x07); + ctx->vol_r = (val & 0x07); break; } case 0xFF25: for (uint_fast8_t j = 0; j < 4; j++) { - chans[j].on_left = (val >> (4 + j)) & 1; - chans[j].on_right = (val >> j) & 1; + ctx->chans[j].on_left = (val >> (4 + j)) & 1; + ctx->chans[j].on_right = (val >> j) & 1; } break; } } -void audio_init(void) +void minigb_apu_audio_init(struct minigb_apu_ctx *ctx) { /* Initialise channels and samples. */ - memset(chans, 0, sizeof(chans)); - chans[0].val = chans[1].val = -1; + memset(ctx->chans, 0, sizeof(ctx->chans)); + ctx->chans[0].val = ctx->chans[1].val = -1; /* Initialise IO registers. */ { @@ -589,7 +526,7 @@ void audio_init(void) 0x77, 0xF3, 0xF1 }; for(uint_fast8_t i = 0; i < sizeof(regs_init); ++i) - audio_write(0xFF10 + i, regs_init[i]); + minigb_apu_audio_write(ctx, 0xFF10 + i, regs_init[i]); } /* Initialise Wave Pattern RAM. */ @@ -600,6 +537,6 @@ void audio_init(void) 0xac, 0xdd, 0xda, 0x48 }; for(uint_fast8_t i = 0; i < sizeof(wave_init); ++i) - audio_write(0xFF30 + i, wave_init[i]); + minigb_apu_audio_write(ctx, 0xFF30 + i, wave_init[i]); } } diff --git a/examples/sdl2/minigb_apu/minigb_apu.h b/examples/sdl2/minigb_apu/minigb_apu.h index 1ab92d4..426cff5 100644 --- a/examples/sdl2/minigb_apu/minigb_apu.h +++ b/examples/sdl2/minigb_apu/minigb_apu.h @@ -9,31 +9,141 @@ #include -#define AUDIO_SAMPLE_RATE 32768 +#ifndef AUDIO_SAMPLE_RATE +# define AUDIO_SAMPLE_RATE 32768 +#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 "data" with "len" number of 32-bit floating point - * samples (native endian order) in stereo interleaved format. + * 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 audio_callback(void *ptr, uint8_t *data, int len); +void minigb_apu_audio_callback(struct minigb_apu_ctx *restrict ctx, + audio_sample_t *restrict 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 audio_read(const uint16_t addr); +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 audio_write(const uint16_t addr, const uint8_t val); +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 audio_init(void); +void minigb_apu_audio_init(struct minigb_apu_ctx *ctx); diff --git a/examples/sdl2/peanut_sdl.c b/examples/sdl2/peanut_sdl.c index 003277c..6bacf24 100644 --- a/examples/sdl2/peanut_sdl.c +++ b/examples/sdl2/peanut_sdl.c @@ -17,6 +17,9 @@ # include "minigb_apu/minigb_apu.h" #endif +uint8_t audio_read(uint16_t addr); +void audio_write(uint16_t addr, uint8_t val); + #include "../../peanut_gb.h" enum { @@ -40,6 +43,8 @@ struct priv_t uint16_t fb[LCD_HEIGHT][LCD_WIDTH]; }; +static struct minigb_apu_ctx apu; + /** * Returns a byte from the ROM file at the given address. */ @@ -74,6 +79,21 @@ uint8_t gb_bootrom_read(struct gb_s *gb, const uint_fast16_t addr) return p->bootrom[addr]; } +uint8_t audio_read(uint16_t addr) +{ + return minigb_apu_audio_read(&apu, addr); +} + +void audio_write(uint16_t addr, uint8_t val) +{ + minigb_apu_audio_write(&apu, addr, val); +} + +void audio_callback(void *ptr, uint8_t *data, int len) +{ + minigb_apu_audio_callback(&apu, (void *)data); +} + void read_cart_ram_file(const char *save_file_name, uint8_t **dest, const size_t len) { @@ -620,9 +640,9 @@ int main(int argc, char **argv) SDL_LogSetPriority(LOG_CATERGORY_PEANUTSDL, SDL_LOG_PRIORITY_INFO); /* Enable Hi-DPI to stop blurry game image. */ - #ifdef SDL_HINT_WINDOWS_DPI_AWARENESS +#ifdef SDL_HINT_WINDOWS_DPI_AWARENESS SDL_SetHint(SDL_HINT_WINDOWS_DPI_AWARENESS, "permonitorv2"); - #endif +#endif /* Initialise frontend implementation, in this case, SDL2. */ if(SDL_Init(SDL_INIT_VIDEO | SDL_INIT_GAMECONTROLLER | SDL_INIT_AUDIO) < 0) @@ -877,7 +897,7 @@ int main(int argc, char **argv) exit(EXIT_FAILURE); } - audio_init(); + minigb_apu_audio_init(&apu); SDL_PauseAudioDevice(dev, 0); } #endif