Hi Guys,
So over the Xmas period I took the plunge to try my hand at an emulator and like a lot of people chose the Gameboy as my first.
I have managed to implement quite an accurate emulator so far (built as a C# class library to run in many different engines like XNA, Monogame & Unity) and it passes more tests then a lot of other C# implementations out there. All the CPU, Memory and GPU side of things came easy to me and I had no real problems getting stuff going.
But audio has been a whole different ball game! I'm struggling to find documentation that goes into enough depth and I feel like I'm missing something! I spent about 2 weeks on the the rest of the emulator and have now spent about 3 week just researching and trying to get my head around the audio side of things. Other open source projects I have been looking at do things in wildly different ways and are never commented/documented well enough for me to fill in the gaps in my knowledge.
So I have done my best to get where I am now which is having a barely working Square Wave Generator for Channel 1. Compared to other emulators I seem to have got the timings correct but the sound coming out is just pops and cracks versus something correct. A saving grace I think is that the pops/cracks play at the right time so I can hear a resemblance to the correct tunes, so there must just be something wrong with how I'm converting the final bytes into the required audio formats (I'm hoping).
Eventually as mentioned I'm hoping to have this running in multiple game engines so I want my implementation to be generic enough that the sound output can be adapted to different sound libraries ie Unity sound or NAudio.
Any help trying to get me first to a working implementation with NAudio would be amazing and then I can try and adapt it for Unity later on.
Below are my WIP implementations of the APU and the SquareWaveGenerator plus info on how I'm interfacing with NAudio:
APU:
SquareWaveGenerator:
NAudio Integration:
So basically I call GetSoundSamples() at 60 FPS which returns a byte array that I pass to NAudio like so:
Constants:
The APU gets updated about every 4 CPU ticks along with the GPU, timer etc
As part of my implementation I'm trying to make the code as readable as possible so other can follow along after me as well, so If you need the values of any of the constants that I haven't provided just let me know
I look forward to hearing you guys thoughts
So over the Xmas period I took the plunge to try my hand at an emulator and like a lot of people chose the Gameboy as my first.
I have managed to implement quite an accurate emulator so far (built as a C# class library to run in many different engines like XNA, Monogame & Unity) and it passes more tests then a lot of other C# implementations out there. All the CPU, Memory and GPU side of things came easy to me and I had no real problems getting stuff going.
But audio has been a whole different ball game! I'm struggling to find documentation that goes into enough depth and I feel like I'm missing something! I spent about 2 weeks on the the rest of the emulator and have now spent about 3 week just researching and trying to get my head around the audio side of things. Other open source projects I have been looking at do things in wildly different ways and are never commented/documented well enough for me to fill in the gaps in my knowledge.
So I have done my best to get where I am now which is having a barely working Square Wave Generator for Channel 1. Compared to other emulators I seem to have got the timings correct but the sound coming out is just pops and cracks versus something correct. A saving grace I think is that the pops/cracks play at the right time so I can hear a resemblance to the correct tunes, so there must just be something wrong with how I'm converting the final bytes into the required audio formats (I'm hoping).
Eventually as mentioned I'm hoping to have this running in multiple game engines so I want my implementation to be generic enough that the sound output can be adapted to different sound libraries ie Unity sound or NAudio.
Any help trying to get me first to a working implementation with NAudio would be amazing and then I can try and adapt it for Unity later on.
Below are my WIP implementations of the APU and the SquareWaveGenerator plus info on how I'm interfacing with NAudio:
APU:
Code:
using System;
namespace GBZEmuLibrary
{
internal class APU
{
private const int FRAME_SEQUENCER_UPDATE_THRESHOLD = Sound.SAMPLE_RATE / APUSchema.FRAME_SEQUENCER_RATE;
private readonly byte[] _memory = new byte[MemorySchema.APU_REGISTERS_END - MemorySchema.APU_REGISTERS_START];
private readonly SquareWaveGenerator _channel1;
private bool _powered = true;
private readonly int _maxCyclesPerSample;
private int _cycleCounter;
private int _frameSequenceTimer;
private byte[] _buffer = new byte[(Sound.SAMPLE_RATE / GameBoySchema.TARGET_FRAMERATE) * 2];
private int _currentByte = 0;
public APU()
{
_maxCyclesPerSample = GameBoySchema.MAX_DMG_CLOCK_CYCLES / Sound.SAMPLE_RATE;
_channel1 = new SquareWaveGenerator();
}
public byte[] GetSoundSamples()
{
//TODO may need to reset buffer
_currentByte = 0;
return _buffer;
}
public void Reset()
{
WriteByte(0x80, 0xFF10);
WriteByte(0xBF, 0xFF11);
WriteByte(0xF3, 0xFF12);
WriteByte(0xBF, 0xFF14);
WriteByte(0x3F, 0xFF16);
WriteByte(0x00, 0xFF17);
WriteByte(0xBF, 0xFF19);
WriteByte(0x7F, 0xFF1A);
WriteByte(0xFF, 0xFF1B);
WriteByte(0x9F, 0xFF1C);
WriteByte(0xBF, 0xFF1E);
WriteByte(0xFF, 0xFF20);
WriteByte(0x00, 0xFF21);
WriteByte(0x00, 0xFF22);
WriteByte(0xBF, 0xFF23);
WriteByte(0x77, 0xFF24);
WriteByte(0xF3, 0xFF25);
WriteByte(0xF1, 0xFF26);
}
public void WriteByte(byte data, int address)
{
int freqLowerBits, freqHighBits;
switch (address)
{
case APUSchema.SQUARE_1_SWEEP_PERIOD:
// Register Format -PPP NSSS Sweep period, negate, shift
_channel1.SetSweep(data);
break;
case APUSchema.SQUARE_1_DUTY_LENGTH_LOAD:
// Register Format DDLL LLLL Duty, Length load (64-L)
_channel1.SetLength(data);
_channel1.SetDutyCycle(data);
break;
case APUSchema.SQUARE_1_VOLUME_ENVELOPE:
// Register Format VVVV APPP Starting volume, Envelope add mode, period
_channel1.SetEnvelope(data);
break;
case APUSchema.SQUARE_1_FREQUENCY_LSB:
// Register Format FFFF FFFF Frequency LSB
freqLowerBits = data;
freqHighBits = Helpers.GetBits(ReadByte(APUSchema.SQUARE_1_FREQUENCY_MSB), 3) << 8;
_channel1.SetFrequency(freqHighBits + freqLowerBits);
break;
case APUSchema.SQUARE_1_FREQUENCY_MSB:
// Register Format TL-- -FFF Trigger, Length enable, Frequency MSB
freqLowerBits = ReadByte(APUSchema.SQUARE_1_FREQUENCY_LSB);
freqHighBits = Helpers.GetBits(data, 3) << 8;
_channel1.SetFrequency(freqHighBits + freqLowerBits);
if (!Helpers.TestBit(data, 6))
{
_channel1.SetLength(0);
}
//Trigger Enabled
if (Helpers.TestBit(data, 7))
{
_channel1.Inited = true;
//TODO handle trigger
if (_channel1.Length == 0)
{
_channel1.SetLength(64);
}
_channel1.SetVolume(_channel1.InitialVolume);
}
break;
case APUSchema.SQUARE_2_DUTY_LENGTH_LOAD:
break;
case APUSchema.SQUARE_2_VOLUME_ENVELOPE:
break;
case APUSchema.SQUARE_2_FREQUENCY_LSB:
break;
case APUSchema.SQUARE_2_FREQUENCY_MSB:
break;
case APUSchema.VIN_VOL_CONTROL:
// Register Format ALLL BRRR Vin L enable, Left vol, Vin R enable, Right vol
break;
case APUSchema.STEREO_SELECT:
// Register Format 8 bits
// Lower 4 bits represent Right Channel for Channels 1-4
// Higher 4 bits represent Left Channel for Channels 1-4
StereoSelect(data);
break;
case APUSchema.SOUND_ENABLED:
HandlePowerToggle(Helpers.TestBit(data, 7));
break;
}
_memory[address - MemorySchema.APU_REGISTERS_START] = data;
}
public byte ReadByte(int address)
{
// TODO NRx3 & NRx4 return 0 upon reading
return _memory[address - MemorySchema.APU_REGISTERS_START];
}
public void Update(int cycles)
{
if (!_powered)
{
return;
}
_cycleCounter += cycles;
//Check if ready to get sample
if (_cycleCounter < _maxCyclesPerSample)
{
return;
}
_cycleCounter -= _maxCyclesPerSample;
_frameSequenceTimer++;
if (_frameSequenceTimer >= FRAME_SEQUENCER_UPDATE_THRESHOLD)
{
_channel1.Update();
}
byte leftChannel = 0;
byte rightChannel = 0;
if (_channel1.Enabled)
{
var sample = _channel1.GetCurrentSample();
if ((_channel1.ChannelState & APUSchema.CHANNEL_LEFT) != 0)
{
leftChannel += sample;
}
if ((_channel1.ChannelState & APUSchema.CHANNEL_RIGHT) != 0)
{
rightChannel += sample;
}
}
//TODO need to determine best way to handle overflow
if (_currentByte * 2 < _buffer.Length - 1)
{
_buffer[_currentByte * 2] = (byte)(leftChannel);
_buffer[_currentByte * 2 + 1] = (byte)(rightChannel);
_currentByte++;
}
}
private void StereoSelect(byte val)
{
_channel1.ChannelState = GetChannelState(val, 1);
}
private int GetChannelState(byte val, int channel)
{
var channelState = 0;
// Testing bits 0-3
if (Helpers.TestBit(val, channel - 1))
{
channelState |= APUSchema.CHANNEL_RIGHT;
}
// Testing bits 4-7
if (Helpers.TestBit(val, channel + 3))
{
channelState |= APUSchema.CHANNEL_LEFT;
}
return channelState;
}
private void HandlePowerToggle(bool newState)
{
if (!newState && _powered)
{
//Reset registers (except length counters on DMG)
}
else if (newState && !_powered)
{
//Reset frame sequencer
}
}
}
}
namespace GBZEmuLibrary
{
internal class APU
{
private const int FRAME_SEQUENCER_UPDATE_THRESHOLD = Sound.SAMPLE_RATE / APUSchema.FRAME_SEQUENCER_RATE;
private readonly byte[] _memory = new byte[MemorySchema.APU_REGISTERS_END - MemorySchema.APU_REGISTERS_START];
private readonly SquareWaveGenerator _channel1;
private bool _powered = true;
private readonly int _maxCyclesPerSample;
private int _cycleCounter;
private int _frameSequenceTimer;
private byte[] _buffer = new byte[(Sound.SAMPLE_RATE / GameBoySchema.TARGET_FRAMERATE) * 2];
private int _currentByte = 0;
public APU()
{
_maxCyclesPerSample = GameBoySchema.MAX_DMG_CLOCK_CYCLES / Sound.SAMPLE_RATE;
_channel1 = new SquareWaveGenerator();
}
public byte[] GetSoundSamples()
{
//TODO may need to reset buffer
_currentByte = 0;
return _buffer;
}
public void Reset()
{
WriteByte(0x80, 0xFF10);
WriteByte(0xBF, 0xFF11);
WriteByte(0xF3, 0xFF12);
WriteByte(0xBF, 0xFF14);
WriteByte(0x3F, 0xFF16);
WriteByte(0x00, 0xFF17);
WriteByte(0xBF, 0xFF19);
WriteByte(0x7F, 0xFF1A);
WriteByte(0xFF, 0xFF1B);
WriteByte(0x9F, 0xFF1C);
WriteByte(0xBF, 0xFF1E);
WriteByte(0xFF, 0xFF20);
WriteByte(0x00, 0xFF21);
WriteByte(0x00, 0xFF22);
WriteByte(0xBF, 0xFF23);
WriteByte(0x77, 0xFF24);
WriteByte(0xF3, 0xFF25);
WriteByte(0xF1, 0xFF26);
}
public void WriteByte(byte data, int address)
{
int freqLowerBits, freqHighBits;
switch (address)
{
case APUSchema.SQUARE_1_SWEEP_PERIOD:
// Register Format -PPP NSSS Sweep period, negate, shift
_channel1.SetSweep(data);
break;
case APUSchema.SQUARE_1_DUTY_LENGTH_LOAD:
// Register Format DDLL LLLL Duty, Length load (64-L)
_channel1.SetLength(data);
_channel1.SetDutyCycle(data);
break;
case APUSchema.SQUARE_1_VOLUME_ENVELOPE:
// Register Format VVVV APPP Starting volume, Envelope add mode, period
_channel1.SetEnvelope(data);
break;
case APUSchema.SQUARE_1_FREQUENCY_LSB:
// Register Format FFFF FFFF Frequency LSB
freqLowerBits = data;
freqHighBits = Helpers.GetBits(ReadByte(APUSchema.SQUARE_1_FREQUENCY_MSB), 3) << 8;
_channel1.SetFrequency(freqHighBits + freqLowerBits);
break;
case APUSchema.SQUARE_1_FREQUENCY_MSB:
// Register Format TL-- -FFF Trigger, Length enable, Frequency MSB
freqLowerBits = ReadByte(APUSchema.SQUARE_1_FREQUENCY_LSB);
freqHighBits = Helpers.GetBits(data, 3) << 8;
_channel1.SetFrequency(freqHighBits + freqLowerBits);
if (!Helpers.TestBit(data, 6))
{
_channel1.SetLength(0);
}
//Trigger Enabled
if (Helpers.TestBit(data, 7))
{
_channel1.Inited = true;
//TODO handle trigger
if (_channel1.Length == 0)
{
_channel1.SetLength(64);
}
_channel1.SetVolume(_channel1.InitialVolume);
}
break;
case APUSchema.SQUARE_2_DUTY_LENGTH_LOAD:
break;
case APUSchema.SQUARE_2_VOLUME_ENVELOPE:
break;
case APUSchema.SQUARE_2_FREQUENCY_LSB:
break;
case APUSchema.SQUARE_2_FREQUENCY_MSB:
break;
case APUSchema.VIN_VOL_CONTROL:
// Register Format ALLL BRRR Vin L enable, Left vol, Vin R enable, Right vol
break;
case APUSchema.STEREO_SELECT:
// Register Format 8 bits
// Lower 4 bits represent Right Channel for Channels 1-4
// Higher 4 bits represent Left Channel for Channels 1-4
StereoSelect(data);
break;
case APUSchema.SOUND_ENABLED:
HandlePowerToggle(Helpers.TestBit(data, 7));
break;
}
_memory[address - MemorySchema.APU_REGISTERS_START] = data;
}
public byte ReadByte(int address)
{
// TODO NRx3 & NRx4 return 0 upon reading
return _memory[address - MemorySchema.APU_REGISTERS_START];
}
public void Update(int cycles)
{
if (!_powered)
{
return;
}
_cycleCounter += cycles;
//Check if ready to get sample
if (_cycleCounter < _maxCyclesPerSample)
{
return;
}
_cycleCounter -= _maxCyclesPerSample;
_frameSequenceTimer++;
if (_frameSequenceTimer >= FRAME_SEQUENCER_UPDATE_THRESHOLD)
{
_channel1.Update();
}
byte leftChannel = 0;
byte rightChannel = 0;
if (_channel1.Enabled)
{
var sample = _channel1.GetCurrentSample();
if ((_channel1.ChannelState & APUSchema.CHANNEL_LEFT) != 0)
{
leftChannel += sample;
}
if ((_channel1.ChannelState & APUSchema.CHANNEL_RIGHT) != 0)
{
rightChannel += sample;
}
}
//TODO need to determine best way to handle overflow
if (_currentByte * 2 < _buffer.Length - 1)
{
_buffer[_currentByte * 2] = (byte)(leftChannel);
_buffer[_currentByte * 2 + 1] = (byte)(rightChannel);
_currentByte++;
}
}
private void StereoSelect(byte val)
{
_channel1.ChannelState = GetChannelState(val, 1);
}
private int GetChannelState(byte val, int channel)
{
var channelState = 0;
// Testing bits 0-3
if (Helpers.TestBit(val, channel - 1))
{
channelState |= APUSchema.CHANNEL_RIGHT;
}
// Testing bits 4-7
if (Helpers.TestBit(val, channel + 3))
{
channelState |= APUSchema.CHANNEL_LEFT;
}
return channelState;
}
private void HandlePowerToggle(bool newState)
{
if (!newState && _powered)
{
//Reset registers (except length counters on DMG)
}
else if (newState && !_powered)
{
//Reset frame sequencer
}
}
}
}
SquareWaveGenerator:
Code:
using System;
namespace GBZEmuLibrary
{
// Ref 1 - https://emu-docs.org/Game%20Boy/gb_sound.txt
internal class SquareWaveGenerator : IGenerator
{
private const int MAX_11_BIT_VALUE = 2048; //2^11
private const int MAX_4_BIT_VALUE = 16; //2^4
public int Length => _totalLength;
public int InitialVolume => _initialVolume;
public bool Inited { get; set; }
public bool Enabled => _totalLength > 0 && Inited;
public int ChannelState { get; set; }
private int _initialSweepPeriod;
private int _sweepPeriod;
private int _shiftSweep;
private bool _negateSweep;
private int _totalLength;
private float _dutyCycle;
private bool _dutyState;
private int _initialVolume;
private int _volume;
private int _envelopePeriod;
private int _initialEnvelopePeriod;
private bool _addEnvelope;
private int _originalFrequency;
private int _frequency;
private int _frequencyCount;
private int _sequenceTimer;
public void Update()
{
//256Hz
if (_sequenceTimer % 2 == 0)
{
_totalLength = Math.Max(0, _totalLength - 1);
}
//128Hz
if ((_sequenceTimer + 2) % 4 == 0)
{
_sweepPeriod--;
if (_shiftSweep != 0 && _sweepPeriod == 0)
{
_sweepPeriod = _initialSweepPeriod;
var sweepFreq = _originalFrequency + (_negateSweep ? -1 : 1) * (_originalFrequency >> _shiftSweep);
if (sweepFreq >= MAX_11_BIT_VALUE)
{
//TODO may need an actual enabled flag
_totalLength = 0;
}
else if (sweepFreq > 0)
{
SetFrequency(sweepFreq);
}
}
}
//64Hz
if (_sequenceTimer % 7 == 0)
{
_envelopePeriod--;
if (_envelopePeriod == 0)
{
_envelopePeriod = _initialEnvelopePeriod;
_volume += _addEnvelope ? 1 : -1;
_volume = Math.Max(_volume, 0);
_volume = Math.Min(_volume, MAX_4_BIT_VALUE - 1);
}
}
_sequenceTimer = (_sequenceTimer + 1) % 8;
}
public byte GetCurrentSample()
{
byte sample = 0;
_frequencyCount++;
if (_frequencyCount > _frequency * (_dutyState ? _dutyCycle : 1 - _dutyCycle))
{
_frequencyCount = 0;
sample = (byte)(_dutyState ? _volume : -_volume);
_dutyState = !_dutyState;
}
return sample;
}
public void SetSweep(byte data)
{
// Val Format -PPP NSSS
_shiftSweep = Helpers.GetBitsIsolated(data, 0, 3);
_negateSweep = Helpers.TestBit(data, 4);
_initialSweepPeriod = Helpers.GetBitsIsolated(data, 4, 3);
_sweepPeriod = _initialSweepPeriod;
}
public void SetLength(byte data)
{
// Val Format --LL LLLL
_totalLength = 64 - Helpers.GetBits(data, 6);
}
public void SetLength(int length)
{
_totalLength = length;
}
public void SetDutyCycle(byte data)
{
// Val Format DD-- ----
_dutyCycle = Helpers.GetBitsIsolated(data, 6, 2) * 0.25f;
_dutyCycle = Math.Max(0.125f, _dutyCycle);
}
public void SetEnvelope(byte data)
{
// Val Format VVVV APPP
_initialEnvelopePeriod = Helpers.GetBits(data, 3);
_envelopePeriod = _initialEnvelopePeriod;
_addEnvelope = Helpers.TestBit(data, 3);
_initialVolume = Helpers.GetBitsIsolated(data, 4, 4);
SetVolume(_initialVolume);
}
public void SetVolume(int volume)
{
_volume = volume;
}
public void SetFrequency(int freq)
{
_originalFrequency = freq;
_frequency = Sound.SAMPLE_RATE / (GameBoySchema.MAX_DMG_CLOCK_CYCLES / ((MAX_11_BIT_VALUE - (freq % MAX_11_BIT_VALUE)) << 5));
}
}
}
namespace GBZEmuLibrary
{
// Ref 1 - https://emu-docs.org/Game%20Boy/gb_sound.txt
internal class SquareWaveGenerator : IGenerator
{
private const int MAX_11_BIT_VALUE = 2048; //2^11
private const int MAX_4_BIT_VALUE = 16; //2^4
public int Length => _totalLength;
public int InitialVolume => _initialVolume;
public bool Inited { get; set; }
public bool Enabled => _totalLength > 0 && Inited;
public int ChannelState { get; set; }
private int _initialSweepPeriod;
private int _sweepPeriod;
private int _shiftSweep;
private bool _negateSweep;
private int _totalLength;
private float _dutyCycle;
private bool _dutyState;
private int _initialVolume;
private int _volume;
private int _envelopePeriod;
private int _initialEnvelopePeriod;
private bool _addEnvelope;
private int _originalFrequency;
private int _frequency;
private int _frequencyCount;
private int _sequenceTimer;
public void Update()
{
//256Hz
if (_sequenceTimer % 2 == 0)
{
_totalLength = Math.Max(0, _totalLength - 1);
}
//128Hz
if ((_sequenceTimer + 2) % 4 == 0)
{
_sweepPeriod--;
if (_shiftSweep != 0 && _sweepPeriod == 0)
{
_sweepPeriod = _initialSweepPeriod;
var sweepFreq = _originalFrequency + (_negateSweep ? -1 : 1) * (_originalFrequency >> _shiftSweep);
if (sweepFreq >= MAX_11_BIT_VALUE)
{
//TODO may need an actual enabled flag
_totalLength = 0;
}
else if (sweepFreq > 0)
{
SetFrequency(sweepFreq);
}
}
}
//64Hz
if (_sequenceTimer % 7 == 0)
{
_envelopePeriod--;
if (_envelopePeriod == 0)
{
_envelopePeriod = _initialEnvelopePeriod;
_volume += _addEnvelope ? 1 : -1;
_volume = Math.Max(_volume, 0);
_volume = Math.Min(_volume, MAX_4_BIT_VALUE - 1);
}
}
_sequenceTimer = (_sequenceTimer + 1) % 8;
}
public byte GetCurrentSample()
{
byte sample = 0;
_frequencyCount++;
if (_frequencyCount > _frequency * (_dutyState ? _dutyCycle : 1 - _dutyCycle))
{
_frequencyCount = 0;
sample = (byte)(_dutyState ? _volume : -_volume);
_dutyState = !_dutyState;
}
return sample;
}
public void SetSweep(byte data)
{
// Val Format -PPP NSSS
_shiftSweep = Helpers.GetBitsIsolated(data, 0, 3);
_negateSweep = Helpers.TestBit(data, 4);
_initialSweepPeriod = Helpers.GetBitsIsolated(data, 4, 3);
_sweepPeriod = _initialSweepPeriod;
}
public void SetLength(byte data)
{
// Val Format --LL LLLL
_totalLength = 64 - Helpers.GetBits(data, 6);
}
public void SetLength(int length)
{
_totalLength = length;
}
public void SetDutyCycle(byte data)
{
// Val Format DD-- ----
_dutyCycle = Helpers.GetBitsIsolated(data, 6, 2) * 0.25f;
_dutyCycle = Math.Max(0.125f, _dutyCycle);
}
public void SetEnvelope(byte data)
{
// Val Format VVVV APPP
_initialEnvelopePeriod = Helpers.GetBits(data, 3);
_envelopePeriod = _initialEnvelopePeriod;
_addEnvelope = Helpers.TestBit(data, 3);
_initialVolume = Helpers.GetBitsIsolated(data, 4, 4);
SetVolume(_initialVolume);
}
public void SetVolume(int volume)
{
_volume = volume;
}
public void SetFrequency(int freq)
{
_originalFrequency = freq;
_frequency = Sound.SAMPLE_RATE / (GameBoySchema.MAX_DMG_CLOCK_CYCLES / ((MAX_11_BIT_VALUE - (freq % MAX_11_BIT_VALUE)) << 5));
}
}
}
NAudio Integration:
Code:
_bufferedWaveProvider = new BufferedWaveProvider(new WaveFormat(Sound.SAMPLE_RATE, 16, 1));
_waveOut = new WaveOut();
_waveOut.Init(_bufferedWaveProvider);
_waveOut.Play(); //TODO sound may need to be delayed
_waveOut = new WaveOut();
_waveOut.Init(_bufferedWaveProvider);
_waveOut.Play(); //TODO sound may need to be delayed
So basically I call GetSoundSamples() at 60 FPS which returns a byte array that I pass to NAudio like so:
Code:
var buffer = _emulator.GetSoundSamples();
_bufferedWaveProvider.AddSamples(buffer, 0, buffer.Length);
_bufferedWaveProvider.AddSamples(buffer, 0, buffer.Length);
Constants:
Code:
public class Sound
{
public const int SAMPLE_RATE = 44100;
}
internal class APUSchema
{
public const int CHANNEL_LEFT = 1;
public const int CHANNEL_RIGHT = 2;
public const int CHANNEL_MONO = 4;
public const int FRAME_SEQUENCER_RATE = 512;
public const int LENGTH_RATE = 256;
public const int SQUARE_1_SWEEP_PERIOD = 0xFF10;
public const int SQUARE_1_DUTY_LENGTH_LOAD = 0xFF11;
public const int SQUARE_1_VOLUME_ENVELOPE = 0xFF12;
public const int SQUARE_1_FREQUENCY_LSB = 0xFF13;
public const int SQUARE_1_FREQUENCY_MSB = 0xFF14;
public const int SQUARE_2_DUTY_LENGTH_LOAD = 0xFF16;
public const int SQUARE_2_VOLUME_ENVELOPE = 0xFF17;
public const int SQUARE_2_FREQUENCY_LSB = 0xFF18;
public const int SQUARE_2_FREQUENCY_MSB = 0xFF19;
public const int VIN_VOL_CONTROL = 0xFF24;
public const int STEREO_SELECT = 0xFF25;
public const int SOUND_ENABLED = 0xFF26;
}
{
public const int SAMPLE_RATE = 44100;
}
internal class APUSchema
{
public const int CHANNEL_LEFT = 1;
public const int CHANNEL_RIGHT = 2;
public const int CHANNEL_MONO = 4;
public const int FRAME_SEQUENCER_RATE = 512;
public const int LENGTH_RATE = 256;
public const int SQUARE_1_SWEEP_PERIOD = 0xFF10;
public const int SQUARE_1_DUTY_LENGTH_LOAD = 0xFF11;
public const int SQUARE_1_VOLUME_ENVELOPE = 0xFF12;
public const int SQUARE_1_FREQUENCY_LSB = 0xFF13;
public const int SQUARE_1_FREQUENCY_MSB = 0xFF14;
public const int SQUARE_2_DUTY_LENGTH_LOAD = 0xFF16;
public const int SQUARE_2_VOLUME_ENVELOPE = 0xFF17;
public const int SQUARE_2_FREQUENCY_LSB = 0xFF18;
public const int SQUARE_2_FREQUENCY_MSB = 0xFF19;
public const int VIN_VOL_CONTROL = 0xFF24;
public const int STEREO_SELECT = 0xFF25;
public const int SOUND_ENABLED = 0xFF26;
}
The APU gets updated about every 4 CPU ticks along with the GPU, timer etc
As part of my implementation I'm trying to make the code as readable as possible so other can follow along after me as well, so If you need the values of any of the constants that I haven't provided just let me know
I look forward to hearing you guys thoughts