using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using Essgee.Exceptions; using Essgee.EventArguments; using Essgee.Utilities; namespace Essgee.Emulation.Audio { public partial class DMGAudio : IAudio { // https://gbdev.gg8.se/wiki/articles/Gameboy_sound_hardware // http://emudev.de/gameboy-emulator/bleeding-ears-time-to-add-audio/ // https://github.com/GhostSonic21/GhostBoy/blob/master/GhostBoy/APU.cpp protected const int numChannels = 4; protected const string channel1OptionName = "AudioEnableCh1Square"; protected const string channel2OptionName = "AudioEnableCh2Square"; protected const string channel3OptionName = "AudioEnableCh3Wave"; protected const string channel4OptionName = "AudioEnableCh4Noise"; protected IDMGAudioChannel channel1, channel2, channel3, channel4; // FF24 - NR50 byte[] volumeRightLeft; bool[] vinEnableRightLeft; // FF25 - NR51 bool[] channel1Enable, channel2Enable, channel3Enable, channel4Enable; // FF26 - NR52 bool isSoundHwEnabled; protected int frameSequencerReload, frameSequencerCounter, frameSequencer; protected List[] channelSampleBuffer; protected List mixedSampleBuffer; public virtual event EventHandler EnqueueSamples; public virtual void OnEnqueueSamples(EnqueueSamplesEventArgs e) { EnqueueSamples?.Invoke(this, e); } protected int sampleRate, numOutputChannels; // double clockRate, refreshRate; protected int samplesPerFrame, cyclesPerFrame, cyclesPerSample; [StateRequired] int sampleCycleCount, frameCycleCount; protected bool channel1ForceEnable, channel2ForceEnable, channel3ForceEnable, channel4ForceEnable; public (string Name, string Description)[] RuntimeOptions => new (string name, string description)[] { (channel1OptionName, "Channel 1 (Square)"), (channel2OptionName, "Channel 2 (Square)"), (channel3OptionName, "Channel 3 (Wave)"), (channel4OptionName, "Channel 4 (Noise)") }; public DMGAudio() { channelSampleBuffer = new List[numChannels]; for (int i = 0; i < numChannels; i++) channelSampleBuffer[i] = new List(); mixedSampleBuffer = new List(); channel1 = new Square(true); channel2 = new Square(false); channel3 = new Wave(); channel4 = new Noise(); samplesPerFrame = cyclesPerFrame = cyclesPerSample = -1; channel1ForceEnable = true; channel2ForceEnable = true; channel3ForceEnable = true; channel4ForceEnable = true; } public object GetRuntimeOption(string name) { switch (name) { case channel1OptionName: return channel1ForceEnable; case channel2OptionName: return channel2ForceEnable; case channel3OptionName: return channel3ForceEnable; case channel4OptionName: return channel4ForceEnable; default: return null; } } public void SetRuntimeOption(string name, object value) { switch (name) { case channel1OptionName: channel1ForceEnable = (bool)value; break; case channel2OptionName: channel2ForceEnable = (bool)value; break; case channel3OptionName: channel3ForceEnable = (bool)value; break; case channel4OptionName: channel4ForceEnable = (bool)value; break; } } public void SetSampleRate(int rate) { sampleRate = rate; ConfigureTimings(); } public void SetOutputChannels(int channels) { numOutputChannels = channels; ConfigureTimings(); } public void SetClockRate(double clock) { clockRate = clock; ConfigureTimings(); } public void SetRefreshRate(double refresh) { refreshRate = refresh; ConfigureTimings(); } private void ConfigureTimings() { samplesPerFrame = (int)(sampleRate / refreshRate); cyclesPerFrame = (int)Math.Round(clockRate / refreshRate); cyclesPerSample = (cyclesPerFrame / samplesPerFrame); volumeRightLeft = new byte[numOutputChannels]; vinEnableRightLeft = new bool[numOutputChannels]; channel1Enable = new bool[numOutputChannels]; channel2Enable = new bool[numOutputChannels]; channel3Enable = new bool[numOutputChannels]; channel4Enable = new bool[numOutputChannels]; FlushSamples(); } public virtual void Startup() { Reset(); if (samplesPerFrame == -1) throw new EmulationException("GB PSG: Timings not configured, invalid samples per frame"); if (cyclesPerFrame == -1) throw new EmulationException("GB PSG: Timings not configured, invalid cycles per frame"); if (cyclesPerSample == -1) throw new EmulationException("GB PSG: Timings not configured, invalid cycles per sample"); } public virtual void Shutdown() { // } public virtual void Reset() { FlushSamples(); channel1.Reset(); channel2.Reset(); channel3.Reset(); channel4.Reset(); for (var i = 0; i < numOutputChannels; i++) { volumeRightLeft[i] = 0; vinEnableRightLeft[i] = false; channel1Enable[i] = false; channel2Enable[i] = false; channel3Enable[i] = false; channel4Enable[i] = false; } frameSequencerReload = (int)(clockRate / 512); frameSequencerCounter = frameSequencerReload; frameSequencer = 0; sampleCycleCount = frameCycleCount = 0; } public void Step(int clockCyclesInStep) { if (!isSoundHwEnabled) return; sampleCycleCount += clockCyclesInStep; frameCycleCount += clockCyclesInStep; for (int i = 0; i < clockCyclesInStep; i++) { frameSequencerCounter--; if (frameSequencerCounter == 0) { frameSequencerCounter = frameSequencerReload; switch (frameSequencer) { case 0: channel1.LengthCounterClock(); channel2.LengthCounterClock(); channel3.LengthCounterClock(); channel4.LengthCounterClock(); break; case 1: break; case 2: channel1.SweepClock(); channel1.LengthCounterClock(); channel2.LengthCounterClock(); channel3.LengthCounterClock(); channel4.LengthCounterClock(); break; case 3: break; case 4: channel1.LengthCounterClock(); channel2.LengthCounterClock(); channel3.LengthCounterClock(); channel4.LengthCounterClock(); break; case 5: break; case 6: channel1.SweepClock(); channel1.LengthCounterClock(); channel2.LengthCounterClock(); channel3.LengthCounterClock(); channel4.LengthCounterClock(); break; case 7: channel1.VolumeEnvelopeClock(); channel2.VolumeEnvelopeClock(); channel4.VolumeEnvelopeClock(); break; } frameSequencer++; if (frameSequencer >= 8) frameSequencer = 0; } channel1.Step(); channel2.Step(); channel3.Step(); channel4.Step(); } if (sampleCycleCount >= cyclesPerSample) { GenerateSample(); sampleCycleCount -= cyclesPerSample; } if (mixedSampleBuffer.Count >= (samplesPerFrame * numOutputChannels)) { OnEnqueueSamples(new EnqueueSamplesEventArgs( numChannels, channelSampleBuffer.Select(x => x.ToArray()).ToArray(), new bool[] { !channel1ForceEnable, !channel2ForceEnable, !channel3ForceEnable, !channel4ForceEnable }, mixedSampleBuffer.ToArray())); FlushSamples(); } if (frameCycleCount >= cyclesPerFrame) { frameCycleCount -= cyclesPerFrame; sampleCycleCount = frameCycleCount; } } protected virtual void GenerateSample() { for (int i = 0; i < numOutputChannels; i++) { /* Generate samples */ var ch1 = (short)(((channel1Enable[i] ? channel1.OutputVolume : 0) * (volumeRightLeft[i] + 1)) << 8); var ch2 = (short)(((channel2Enable[i] ? channel2.OutputVolume : 0) * (volumeRightLeft[i] + 1)) << 8); var ch3 = (short)(((channel3Enable[i] ? channel3.OutputVolume : 0) * (volumeRightLeft[i] + 1)) << 8); var ch4 = (short)(((channel4Enable[i] ? channel4.OutputVolume : 0) * (volumeRightLeft[i] + 1)) << 8); channelSampleBuffer[0].Add(ch1); channelSampleBuffer[1].Add(ch2); channelSampleBuffer[2].Add(ch3); channelSampleBuffer[3].Add(ch4); /* Mix samples */ var mixed = 0; if (channel1ForceEnable) mixed += ch1; if (channel2ForceEnable) mixed += ch2; if (channel3ForceEnable) mixed += ch3; if (channel4ForceEnable) mixed += ch4; mixed /= numChannels; mixedSampleBuffer.Add((short)mixed); } } public void FlushSamples() { for (int i = 0; i < numChannels; i++) channelSampleBuffer[i].Clear(); mixedSampleBuffer.Clear(); } public virtual byte ReadPort(byte port) { // Channels if (port >= 0x10 && port <= 0x14) return channel1.ReadPort((byte)(port - 0x10)); else if (port >= 0x15 && port <= 0x19) return channel2.ReadPort((byte)(port - 0x15)); else if (port >= 0x1A && port <= 0x1E) return channel3.ReadPort((byte)(port - 0x1A)); else if (port >= 0x1F && port <= 0x23) return channel4.ReadPort((byte)(port - 0x1F)); // Channel 3 Wave RAM else if (port >= 0x30 && port <= 0x3F) return channel3.ReadWaveRam((byte)(port - 0x30)); // Control ports else switch (port) { case 0x24: return (byte)( (vinEnableRightLeft[1] ? (1 << 7) : 0) | (volumeRightLeft[1] << 4) | (vinEnableRightLeft[0] ? (1 << 3) : 0) | (volumeRightLeft[0] << 0)); case 0x25: return (byte)( (channel4Enable[1] ? (1 << 7) : 0) | (channel3Enable[1] ? (1 << 6) : 0) | (channel2Enable[1] ? (1 << 5) : 0) | (channel1Enable[1] ? (1 << 4) : 0) | (channel4Enable[0] ? (1 << 3) : 0) | (channel3Enable[0] ? (1 << 2) : 0) | (channel2Enable[0] ? (1 << 1) : 0) | (channel1Enable[0] ? (1 << 0) : 0)); case 0x26: return (byte)( 0x70 | (isSoundHwEnabled ? (1 << 7) : 0) | (channel4.IsActive ? (1 << 3) : 0) | (channel3.IsActive ? (1 << 2) : 0) | (channel2.IsActive ? (1 << 1) : 0) | (channel1.IsActive ? (1 << 0) : 0)); default: return 0xFF; } } public virtual void WritePort(byte port, byte value) { // Channels if (port >= 0x10 && port <= 0x14) channel1.WritePort((byte)(port - 0x10), value); else if (port >= 0x15 && port <= 0x19) channel2.WritePort((byte)(port - 0x15), value); else if (port >= 0x1A && port <= 0x1E) channel3.WritePort((byte)(port - 0x1A), value); else if (port >= 0x1F && port <= 0x23) channel4.WritePort((byte)(port - 0x1F), value); // Channel 3 Wave RAM else if (port >= 0x30 && port <= 0x3F) channel3.WriteWaveRam((byte)(port - 0x30), value); // Control ports else switch (port) { case 0x24: vinEnableRightLeft[1] = ((value >> 7) & 0b1) == 0b1; volumeRightLeft[1] = (byte)((value >> 4) & 0b111); vinEnableRightLeft[0] = ((value >> 3) & 0b1) == 0b1; volumeRightLeft[0] = (byte)((value >> 0) & 0b111); break; case 0x25: channel4Enable[1] = ((value >> 7) & 0b1) == 0b1; channel3Enable[1] = ((value >> 6) & 0b1) == 0b1; channel2Enable[1] = ((value >> 5) & 0b1) == 0b1; channel1Enable[1] = ((value >> 4) & 0b1) == 0b1; channel4Enable[0] = ((value >> 3) & 0b1) == 0b1; channel3Enable[0] = ((value >> 2) & 0b1) == 0b1; channel2Enable[0] = ((value >> 1) & 0b1) == 0b1; channel1Enable[0] = ((value >> 0) & 0b1) == 0b1; break; case 0x26: isSoundHwEnabled = ((value >> 7) & 0b1) == 0b1; break; } } } }