diff --git a/ext/minigb_apu/LICENSE b/ext/minigb_apu/LICENSE new file mode 100644 index 0000000..af254b2 --- /dev/null +++ b/ext/minigb_apu/LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2017 Alex Baines +Copyright (c) 2019 Mahyar Koshkouei + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/ext/minigb_apu/minigb_apu.c b/ext/minigb_apu/minigb_apu.c new file mode 100644 index 0000000..2a6ff66 --- /dev/null +++ b/ext/minigb_apu/minigb_apu.c @@ -0,0 +1,553 @@ +/** + * 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 + */ + +#include +#include +#include +#include + +#include "minigb_apu.h" + +#define AUDIO_MEM_SIZE (0xFF3F - 0xFF10 + 1) +#define AUDIO_ADDR_COMPENSATION 0xFF10 + +#define MAX(a, b) ( a > b ? a : b ) +#define MIN(a, b) ( a <= b ? a : b ) + +/** + * Memory holding audio registers between 0xFF10 and 0xFF3F inclusive. + */ +static uint8_t audio_mem[AUDIO_MEM_SIZE]; + +struct chan_len_ctr { + uint8_t load; + bool enabled; + float counter; + float inc; +}; + +struct chan_vol_env { + uint8_t step; + bool up; + float counter; + float inc; +}; + +struct chan_freq_sweep { + uint_fast16_t freq; + uint8_t rate; + uint8_t shift; + bool up; + float counter; + float inc; +}; + +static struct chan { + bool enabled; + bool powered; + bool on_left; + bool on_right; + bool muted; + + uint8_t volume; + uint8_t volume_init; + + uint16_t freq; + float freq_counter; + float freq_inc; + + int_fast8_t val; + + struct chan_len_ctr len; + struct chan_vol_env env; + struct chan_freq_sweep sweep; + + // square + uint8_t duty; + uint8_t duty_counter; + + // noise + uint16_t lfsr_reg; + uint8_t lfsr_wide; + uint8_t lfsr_div; + + // wave + uint8_t sample; + +#if ENABLE_HIPASS + float capacitor; +#endif +} chans[4]; + +static float vol_l, vol_r; + +static float hipass(struct chan *c, float sample) +{ +#if ENABLE_HIPASS + float out = sample - c->capacitor; + c->capacitor = sample - out * 0.996f; + return out; +#else + return sample; +#endif +} + +static void set_note_freq(struct chan *c, const uint_fast16_t freq) +{ + c->freq_inc = freq / AUDIO_SAMPLE_RATE; +} + +static void chan_enable(const uint_fast8_t i, const bool enable) +{ + chans[i].enabled = enable; + + uint8_t val = (audio_mem[0xFF26 - AUDIO_ADDR_COMPENSATION] & 0x80) | + (chans[3].enabled << 3) | (chans[2].enabled << 2) | + (chans[1].enabled << 1) | (chans[0].enabled << 0); + + audio_mem[0xFF26 - AUDIO_ADDR_COMPENSATION] = val; +} + +static void update_env(struct chan *c) +{ + c->env.counter += c->env.inc; + + while (c->env.counter > 1.0f) { + if (c->env.step) { + c->volume += c->env.up ? 1 : -1; + if (c->volume == 0 || c->volume == 15) { + c->env.inc = 0; + } + c->volume = MAX(0, MIN(15, c->volume)); + } + c->env.counter -= 1.0f; + } +} + +static void update_len(struct chan *c) +{ + if (c->len.enabled) { + c->len.counter += c->len.inc; + if (c->len.counter > 1.0f) { + chan_enable(c - chans, 0); + c->len.counter = 0.0f; + } + } +} + +static bool update_freq(struct chan *c, float *pos) +{ + float inc = c->freq_inc - *pos; + c->freq_counter += inc; + + if (c->freq_counter > 1.0f) { + *pos = c->freq_inc - (c->freq_counter - 1.0f); + c->freq_counter = 0.0f; + 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 > 1.0f) { + if (c->sweep.shift) { + uint16_t inc = (c->sweep.freq >> c->sweep.shift); + if (!c->sweep.up) + inc *= -1; + + c->freq += inc; + if (c->freq > 2047) { + c->enabled = 0; + } else { + set_note_freq(c, + 4194304 / ((2048 - c->freq)<< 5)); + c->freq_inc *= 8.0f; + } + } else if (c->sweep.rate) { + c->enabled = 0; + } + c->sweep.counter -= 1.0f; + } +} + +static void update_square(float *restrict samples, const bool ch2) +{ + struct chan *c = chans + ch2; + if (!c->powered) + return; + + set_note_freq(c, 4194304.0f / ((2048 - c->freq) << 5)); + c->freq_inc *= 8.0f; + + for (uint_fast16_t i = 0; i < AUDIO_NSAMPLES; i += 2) { + update_len(c); + + if (c->enabled) { + update_env(c); + if (!ch2) + update_sweep(c); + + float pos = 0.0f; + float prev_pos = 0.0f; + float sample = 0.0f; + + while (update_freq(c, &pos)) { + c->duty_counter = (c->duty_counter + 1) & 7; + sample += ((pos - prev_pos) / c->freq_inc) * + (float)c->val; + c->val = (c->duty & (1 << c->duty_counter)) ? + 1 : + -1; + prev_pos = pos; + } + sample += ((pos - prev_pos) / c->freq_inc) * + (float)c->val; + sample = hipass(c, sample * (c->volume / 15.0f)); + + if (!c->muted) { + samples[i + 0] += + sample * 0.25f * c->on_left * vol_l; + samples[i + 1] += + sample * 0.25f * c->on_right * vol_r; + } + } + } +} + +static uint8_t wave_sample(const unsigned int pos, const unsigned int volume) +{ + uint8_t sample = + 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(float *restrict samples) +{ + struct chan *c = chans + 2; + if (!c->powered) + return; + + uint_fast16_t freq = 4194304.0f / ((2048 - c->freq) << 5); + set_note_freq(c, freq); + + c->freq_inc *= 16.0f; + + for (uint_fast16_t i = 0; i < AUDIO_NSAMPLES; i += 2) { + update_len(c); + + if (c->enabled) { + float pos = 0.0f; + float prev_pos = 0.0f; + float sample = 0.0f; + + c->sample = wave_sample(c->val, c->volume); + + while (update_freq(c, &pos)) { + c->val = (c->val + 1) & 31; + sample += ((pos - prev_pos) / c->freq_inc) * + (float)c->sample; + c->sample = wave_sample(c->val, c->volume); + prev_pos = pos; + } + sample += ((pos - prev_pos) / c->freq_inc) * + (float)c->sample; + + if (c->volume > 0) { + float diff = (float[]){ 7.5f, 3.75f, + 1.5f }[c->volume - 1]; + sample = hipass(c, (sample - diff) / 7.5f); + + if (!c->muted) { + samples[i + 0] += sample * 0.25f * + c->on_left * vol_l; + samples[i + 1] += sample * 0.25f * + c->on_right * vol_r; + } + } + } + } +} + +static void update_noise(float *restrict samples) +{ + struct chan *c = chans + 3; + if (!c->powered) + return; + + uint_fast16_t freq = 4194304 / ((uint_fast8_t[]){ + 8, 16, 32, 48, 64, 80, 96, 112 + }[c->lfsr_div] << c->freq); + set_note_freq(c, freq); + + if (c->freq >= 14) + c->enabled = 0; + + for (uint_fast16_t i = 0; i < AUDIO_NSAMPLES; i += 2) { + update_len(c); + + if (c->enabled) { + update_env(c); + + float pos = 0.0f; + float prev_pos = 0.0f; + float sample = 0.0f; + + while (update_freq(c, &pos)) { + c->lfsr_reg = (c->lfsr_reg << 1) | + (c->val == 1); + + if (c->lfsr_wide) { + c->val = !(((c->lfsr_reg >> 14) & 1) ^ + ((c->lfsr_reg >> 13) & 1)) ? + 1 : + -1; + } else { + c->val = !(((c->lfsr_reg >> 6) & 1) ^ + ((c->lfsr_reg >> 5) & 1)) ? + 1 : + -1; + } + sample += ((pos - prev_pos) / c->freq_inc) * + c->val; + prev_pos = pos; + } + sample += ((pos - prev_pos) / c->freq_inc) * c->val; + sample = hipass(c, sample * (c->volume / 15.0f)); + + if (!c->muted) { + samples[i + 0] += + sample * 0.25f * c->on_left * vol_l; + samples[i + 1] += + sample * 0.25f * c->on_right * vol_r; + } + } + } +} + +/** + * SDL2 style audio callback function. + */ +void audio_callback(void *userdata, void *restrict stream, int len) +{ + /* Appease unused variable warning. */ + (void)userdata; + + memset(stream, 0, len); + + update_square(stream, 0); + update_square(stream, 1); + update_wave(stream); + update_noise(stream); +} + +static void chan_trigger(uint_fast8_t i) +{ + struct chan *c = chans + i; + + chan_enable(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 ? (64.0f / (float)c->env.step) / + AUDIO_SAMPLE_RATE : + 8.0f / AUDIO_SAMPLE_RATE; + c->env.counter = 0.0f; + } + + // freq sweep + if (i == 0) { + uint8_t val = audio_mem[0xFF10 - AUDIO_ADDR_COMPENSATION]; + + c->sweep.freq = c->freq; + c->sweep.rate = (val >> 4) & 0x07; + c->sweep.up = !(val & 0x08); + c->sweep.shift = (val & 0x07); + c->sweep.inc = c->sweep.rate ? + (128.0f / (float)(c->sweep.rate)) / + AUDIO_SAMPLE_RATE : + 0; + c->sweep.counter = nexttowardf(1.0f, 1.1f); + } + + int len_max = 64; + + if (i == 2) { // wave + len_max = 256; + c->val = 0; + } else if (i == 3) { // noise + c->lfsr_reg = 0xFFFF; + c->val = -1; + } + + c->len.inc = + (256.0f / (float)(len_max - c->len.load)) / AUDIO_SAMPLE_RATE; + c->len.counter = 0.0f; +} + +/** + * 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 audio_read(const uint16_t addr) +{ + static 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 }; + + if (addr > 0xFF26) + return audio_mem[addr - AUDIO_ADDR_COMPENSATION]; + + return audio_mem[addr - AUDIO_ADDR_COMPENSATION] | ortab[addr - 0xFF10]; +} + +/** + * 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 audio_write(const uint16_t addr, const uint8_t val) +{ + /* Find sound channel corresponding to register address. */ + uint_fast8_t i = (addr - 0xFF10) / 5; + audio_mem[addr - AUDIO_ADDR_COMPENSATION] = val; + + switch (addr) { + case 0xFF12: + case 0xFF17: + case 0xFF21: { + chans[i].volume_init = val >> 4; + 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 (val & 0x08) { + chans[i].volume++; + } else { + chans[i].volume += 2; + } + } else { + chans[i].volume = 16 - chans[i].volume; + } + + chans[i].volume &= 0x0F; + chans[i].env.step = val & 0x07; + } + } break; + + case 0xFF1C: + chans[i].volume = 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].duty = duty_lookup[val >> 6]; + break; + } + + case 0xFF1B: + chans[i].len.load = val; + break; + + case 0xFF13: + case 0xFF18: + case 0xFF1D: + chans[i].freq &= 0xFF00; + chans[i].freq |= val; + break; + + case 0xFF1A: + chans[i].powered = (val & 0x80) != 0; + chan_enable(i, val & 0x80); + break; + + case 0xFF14: + case 0xFF19: + case 0xFF1E: + chans[i].freq &= 0x00FF; + chans[i].freq |= ((val & 0x07) << 8); + /* Intentional fall-through. */ + case 0xFF23: + chans[i].len.enabled = val & 0x40 ? 1 : 0; + if (val & 0x80) + chan_trigger(i); + + break; + + case 0xFF22: + chans[3].freq = val >> 4; + chans[3].lfsr_wide = !(val & 0x08); + chans[3].lfsr_div = val & 0x07; + break; + + case 0xFF24: + vol_l = ((val >> 4) & 0x07) / 7.0f; + vol_r = (val & 0x07) / 7.0f; + break; + + case 0xFF25: + for (uint_fast8_t i = 0; i < 4; ++i) { + chans[i].on_left = (val >> (4 + i)) & 1; + chans[i].on_right = (val >> i) & 1; + } + break; + } +} + +void audio_init(void) +{ + /* Initialise channels and samples. */ + memset(chans, 0, sizeof(chans)); + chans[0].val = 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) + audio_write(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) + audio_write(0xFF30 + i, wave_init[i]); + } +} diff --git a/ext/minigb_apu/minigb_apu.h b/ext/minigb_apu/minigb_apu.h new file mode 100644 index 0000000..7cab051 --- /dev/null +++ b/ext/minigb_apu/minigb_apu.h @@ -0,0 +1,48 @@ +/** + * 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 + +#define DMG_CLOCK_FREQ 4194304.0 +#define SCREEN_REFRESH_CYCLES 70224.0 +#define VERTICAL_SYNC (DMG_CLOCK_FREQ/SCREEN_REFRESH_CYCLES) + +#ifndef ENABLE_HIPASS +# define ENABLE_HIPASS 1 +#endif + +#ifndef AUDIO_SAMPLE_RATE +# define AUDIO_SAMPLE_RATE 8000.0 +#endif + +#ifndef AUDIO_NSAMPLES +# define AUDIO_NSAMPLES 256 +//((unsigned)(AUDIO_SAMPLE_RATE / VERTICAL_SYNC) * 2) +#endif + +/** + * Fill allocated buffer "data" with "len" number of 32-bit floating point + * samples (native endian order) in stereo interleaved format. + */ +void audio_callback(void *ptr, void *data, int len); + +/** + * Read audio register at given address "addr". + */ +uint8_t audio_read(const uint16_t addr); + +/** + * Write "val" to audio register at given address "addr". + */ +void audio_write(const uint16_t addr, const uint8_t val); + +/** + * Initialise audio driver. + */ +void audio_init(void);