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; using static Essgee.Emulation.Utilities; namespace Essgee.Emulation.Audio { public class SN76489 : IAudio { /* http://www.smspower.org/Development/SN76489 */ /* Differences in various system's PSGs: http://forums.nesdev.com/viewtopic.php?p=190216#p190216 */ protected const int numChannels = 4, numToneChannels = 3, noiseChannelIndex = 3; protected const string channel1OptionName = "AudioEnableCh1Square"; protected const string channel2OptionName = "AudioEnableCh2Square"; protected const string channel3OptionName = "AudioEnableCh3Square"; protected const string channel4OptionName = "AudioEnableCh4Noise"; /* Noise generation constants */ protected virtual ushort noiseLfsrMask => 0x7FFF; protected virtual ushort noiseTappedBits => 0x0003; /* Bits 0 and 1 */ protected virtual int noiseBitShift => 14; /* Sample generation & event handling */ protected List[] channelSampleBuffer; protected List mixedSampleBuffer; public virtual event EventHandler EnqueueSamples; public virtual void OnEnqueueSamples(EnqueueSamplesEventArgs e) { EnqueueSamples?.Invoke(this, e); } /* Audio output variables */ protected int sampleRate, numOutputChannels; /* Channel registers */ [StateRequired] protected ushort[] volumeRegisters; /* Channels 0-3: 4 bits */ [StateRequired] protected ushort[] toneRegisters; /* Channels 0-2 (tone): 10 bits; channel 3 (noise): 3 bits */ /* Channel counters */ [StateRequired] protected ushort[] channelCounters; /* 10-bit counters */ [StateRequired] protected bool[] channelOutput; /* Volume attenuation table */ protected short[] volumeTable; /* 2dB change per volume register step */ /* Latched channel/type */ [StateRequired] byte latchedChannel, latchedType; /* Linear-feedback shift register, for noise generation */ [StateRequired] protected ushort noiseLfsr; /* 15-bit */ /* Timing variables */ double clockRate, refreshRate; int samplesPerFrame, cyclesPerFrame, cyclesPerSample; [StateRequired] int sampleCycleCount, frameCycleCount, dividerCount; /* User-facing channel toggles */ 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 (Square)"), (channel4OptionName, "Channel 4 (Noise)") }; public SN76489() { channelSampleBuffer = new List[numChannels]; for (int i = 0; i < numChannels; i++) channelSampleBuffer[i] = new List(); mixedSampleBuffer = new List(); volumeRegisters = new ushort[numChannels]; toneRegisters = new ushort[numChannels]; channelCounters = new ushort[numChannels]; channelOutput = new bool[numChannels]; volumeTable = new short[16]; for (int i = 0; i < volumeTable.Length; i++) volumeTable[i] = (short)(short.MaxValue * Math.Pow(2.0, i * -2.0 / 6.0)); volumeTable[15] = 0; 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)(clockRate / refreshRate); cyclesPerSample = (cyclesPerFrame / samplesPerFrame); FlushSamples(); } public virtual void Startup() { Reset(); if (samplesPerFrame == -1) throw new EmulationException("SN76489: Timings not configured, invalid samples per frame"); if (cyclesPerFrame == -1) throw new EmulationException("SN76489: Timings not configured, invalid cycles per frame"); if (cyclesPerSample == -1) throw new EmulationException("SN76489: Timings not configured, invalid cycles per sample"); } public virtual void Shutdown() { // } public virtual void Reset() { FlushSamples(); latchedChannel = latchedType = 0x00; noiseLfsr = 0x4000; for (int i = 0; i < numChannels; i++) { volumeRegisters[i] = 0x000F; toneRegisters[i] = 0x0000; } sampleCycleCount = frameCycleCount = dividerCount = 0; } public void Step(int clockCyclesInStep) { sampleCycleCount += clockCyclesInStep; frameCycleCount += clockCyclesInStep; for (int i = 0; i < clockCyclesInStep; i++) { dividerCount++; if (dividerCount == 16) { for (int ch = 0; ch < numToneChannels; ch++) StepToneChannel(ch); StepNoiseChannel(); dividerCount = 0; } } 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; } } private void StepToneChannel(int ch) { /* Check for counter underflow */ if ((channelCounters[ch] & 0x03FF) > 0) channelCounters[ch]--; /* Counter underflowed, reload and flip output bit, then generate sample */ if ((channelCounters[ch] & 0x03FF) == 0) { channelCounters[ch] = (ushort)(toneRegisters[ch] & 0x03FF); channelOutput[ch] = !channelOutput[ch]; } } private void StepNoiseChannel() { int chN = noiseChannelIndex; { /* Check for counter underflow */ if ((channelCounters[chN] & 0x03FF) > 0) channelCounters[chN]--; /* Counter underflowed, reload and flip output bit */ if ((channelCounters[chN] & 0x03FF) == 0) { switch (toneRegisters[chN] & 0x3) { case 0x0: channelCounters[chN] = 0x10; break; case 0x1: channelCounters[chN] = 0x20; break; case 0x2: channelCounters[chN] = 0x40; break; case 0x3: channelCounters[chN] = (ushort)(toneRegisters[2] & 0x03FF); break; } channelOutput[chN] = !channelOutput[chN]; if (channelOutput[chN]) { /* Check noise type, then generate sample */ bool isWhiteNoise = (((toneRegisters[chN] >> 2) & 0x1) == 0x1); ushort newLfsrBit = (ushort)((isWhiteNoise ? CheckParity((ushort)(noiseLfsr & noiseTappedBits)) : (noiseLfsr & 0x01)) << noiseBitShift); noiseLfsr = (ushort)((newLfsrBit | (noiseLfsr >> 1)) & noiseLfsrMask); } } } } protected virtual void GenerateSample() { for (int i = 0; i < numOutputChannels; i++) { /* Generate samples */ var ch1 = (short)(volumeTable[volumeRegisters[0]] * ((toneRegisters[0] < 2 ? true : channelOutput[0]) ? 1.0 : 0.0)); var ch2 = (short)(volumeTable[volumeRegisters[1]] * ((toneRegisters[1] < 2 ? true : channelOutput[1]) ? 1.0 : 0.0)); var ch3 = (short)(volumeTable[volumeRegisters[2]] * ((toneRegisters[2] < 2 ? true : channelOutput[2]) ? 1.0 : 0.0)); var ch4 = (short)(volumeTable[volumeRegisters[3]] * (noiseLfsr & 0x1)); 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(); } private ushort CheckParity(ushort val) { val ^= (ushort)(val >> 8); val ^= (ushort)(val >> 4); val ^= (ushort)(val >> 2); val ^= (ushort)(val >> 1); return (ushort)(val & 0x1); } public virtual byte ReadPort(byte port) { throw new EmulationException("SN76489: Cannot read ports"); } public virtual void WritePort(byte port, byte data) { if (IsBitSet(data, 7)) { /* LATCH/DATA byte; get channel (0-3) and type (0 is tone/noise, 1 is volume) */ latchedChannel = (byte)((data >> 5) & 0x03); latchedType = (byte)((data >> 4) & 0x01); /* Mask off non-data bits */ data &= 0x0F; /* If target is channel 3 noise (3 bits), mask off highest bit */ if (latchedChannel == 3 && latchedType == 0) data &= 0x07; /* Write to register */ if (latchedType == 0) { /* Data is tone/noise */ toneRegisters[latchedChannel] = (ushort)((toneRegisters[latchedChannel] & 0x03F0) | data); } else { /* Data is volume */ volumeRegisters[latchedChannel] = data; } } else { /* DATA byte; mask off non-data bits */ data &= 0x3F; /* Write to register */ if (latchedType == 0) { /* Data is tone/noise */ if (latchedChannel == 3) { /* Target is channel 3 noise, mask off excess bits and write to low bits of register */ toneRegisters[latchedChannel] = (ushort)(data & 0x07); } else { /* Target is not channel 3 noise, write to high bits of register */ toneRegisters[latchedChannel] = (ushort)((toneRegisters[latchedChannel] & 0x000F) | (data << 4)); } } else { /* Data is volume; mask off excess bits and write to low bits of register */ volumeRegisters[latchedChannel] = (ushort)(data & 0x0F); } } } } }