using System; using System.Collections.Generic; using StoicGoose.Common.Attributes; using StoicGoose.Core.Interfaces; using StoicGoose.Core.Machines; using static StoicGoose.Common.Utilities.BitHandling; namespace StoicGoose.Core.Sound { public delegate byte WaveTableReadDelegate(ushort address); public abstract partial class SoundControllerCommon : IPortAccessComponent { /* http://daifukkat.su/docs/wsman/#hw_sound */ public abstract byte MaxMasterVolume { get; } public abstract int NumChannels { get; } readonly int sampleRate, numOutputChannels; protected readonly SoundChannel1 channel1 = default; protected readonly SoundChannel2 channel2 = default; protected readonly SoundChannel3 channel3 = default; protected readonly SoundChannel4 channel4 = default; protected readonly List[] channelSampleBuffers = default; protected readonly List mixedSampleBuffer = new(); public short[][] LastEnqueuedChannelSamples { get; private set; } = default; public short[] LastEnqueuedMixedSamples { get; private set; } = Array.Empty(); public Action SendSamples { get; set; } = default; readonly double clockRate, refreshRate; readonly int samplesPerFrame, cyclesPerFrame, cyclesPerSample; int cycleCount; protected readonly IMachine machine = default; /* REG_SND_WAVE_BASE */ protected byte waveTableBase; /* REG_SND_OUTPUT */ protected bool speakerEnable, headphoneEnable, headphonesConnected; protected byte speakerVolumeShift; /* REG_SND_VOLUME */ protected byte masterVolume; public SoundControllerCommon(IMachine machine, int rate, int outChannels) { this.machine = machine; sampleRate = rate; numOutputChannels = outChannels; channel1 = new SoundChannel1((a) => this.machine.ReadMemory((uint)((waveTableBase << 6) + (0 << 4) + a))); channel2 = new SoundChannel2((a) => this.machine.ReadMemory((uint)((waveTableBase << 6) + (1 << 4) + a))); channel3 = new SoundChannel3((a) => this.machine.ReadMemory((uint)((waveTableBase << 6) + (2 << 4) + a))); channel4 = new SoundChannel4((a) => this.machine.ReadMemory((uint)((waveTableBase << 6) + (3 << 4) + a))); channelSampleBuffers = new List[NumChannels]; for (var i = 0; i < channelSampleBuffers.Length; i++) channelSampleBuffers[i] = new(); LastEnqueuedChannelSamples = new short[NumChannels][]; clockRate = MachineCommon.CpuClock; refreshRate = Display.DisplayControllerCommon.VerticalClock; samplesPerFrame = (int)(sampleRate / refreshRate); cyclesPerFrame = (int)(clockRate / refreshRate); cyclesPerSample = cyclesPerFrame / samplesPerFrame; } public virtual void Reset() { cycleCount = 0; channel1.Reset(); channel2.Reset(); channel3.Reset(); channel4.Reset(); FlushSamples(); ResetRegisters(); } public virtual void ResetRegisters() { waveTableBase = 0; speakerEnable = headphoneEnable = false; headphonesConnected = true; /* NOTE: always set for stereo sound */ speakerVolumeShift = 0; masterVolume = MaxMasterVolume; } public void Shutdown() { /* Nothing to do */ } public void ChangeMasterVolume() { var newMasterVolume = MasterVolume - 1; if (newMasterVolume < 0) newMasterVolume = MaxMasterVolume; else if (newMasterVolume > MaxMasterVolume) newMasterVolume = 0; masterVolume = (byte)newMasterVolume; } public void Step(int clockCyclesInStep) { cycleCount += clockCyclesInStep; for (int i = 0; i < clockCyclesInStep; i++) StepChannels(); if (cycleCount >= cyclesPerSample) { ProcessSample(GenerateSample()); cycleCount -= cyclesPerSample; } if (mixedSampleBuffer.Count >= (samplesPerFrame * numOutputChannels)) { var sampleArray = mixedSampleBuffer.ToArray(); LastEnqueuedMixedSamples = sampleArray; for (var i = 0; i < NumChannels; i++) LastEnqueuedChannelSamples[i] = channelSampleBuffers[i].ToArray(); SendSamples?.Invoke(sampleArray.Clone() as short[]); FlushSamples(); } } public virtual void StepChannels() { channel1.Step(); channel2.Step(); channel3.Step(); channel4.Step(); } public abstract int[] GenerateSample(); public virtual void ProcessSample(int[] lrSamples) { if (headphonesConnected && !headphoneEnable && !speakerEnable) /* Headphones connected but neither headphones nor speaker enabled? Don't output sound */ lrSamples[0] = lrSamples[1] = 0; else if (!headphonesConnected) /* Otherwise, no headphones connected? Mix down to mono, perform volume shift */ lrSamples[0] = lrSamples[1] = ((lrSamples[0] + lrSamples[1]) / 2) >> speakerVolumeShift; mixedSampleBuffer.Add((short)(lrSamples[0] * (masterVolume / (double)MaxMasterVolume))); /* Left */ mixedSampleBuffer.Add((short)(lrSamples[1] * (masterVolume / (double)MaxMasterVolume))); /* Right */ } public void FlushSamples() { foreach (var buffer in channelSampleBuffers) buffer.Clear(); mixedSampleBuffer.Clear(); } public virtual byte ReadPort(ushort port) { var retVal = (byte)0; switch (port) { case 0x80: case 0x81: /* REG_SND_CH1_PITCH */ retVal |= (byte)(channel1.Pitch >> ((port & 0b1) * 8)); break; case 0x82: case 0x83: /* REG_SND_CH2_PITCH */ retVal |= (byte)(channel2.Pitch >> ((port & 0b1) * 8)); break; case 0x84: case 0x85: /* REG_SND_CH3_PITCH */ retVal |= (byte)(channel3.Pitch >> ((port & 0b1) * 8)); break; case 0x86: case 0x87: /* REG_SND_CH4_PITCH */ retVal |= (byte)(channel4.Pitch >> ((port & 0b1) * 8)); break; case 0x88: /* REG_SND_CH1_VOL */ retVal |= (byte)(channel1.VolumeLeft << 4 | channel1.VolumeRight); break; case 0x89: /* REG_SND_CH2_VOL */ retVal |= (byte)(channel2.VolumeLeft << 4 | channel2.VolumeRight); break; case 0x8A: /* REG_SND_CH3_VOL */ retVal |= (byte)(channel3.VolumeLeft << 4 | channel3.VolumeRight); break; case 0x8B: /* REG_SND_CH4_VOL */ retVal |= (byte)(channel4.VolumeLeft << 4 | channel4.VolumeRight); break; case 0x8C: /* REG_SND_SWEEP_VALUE */ retVal |= (byte)channel3.SweepValue; break; case 0x8D: /* REG_SND_SWEEP_TIME */ retVal |= (byte)(channel3.SweepTime & 0b11111); break; case 0x8E: /* REG_SND_NOISE */ retVal |= (byte)(channel4.NoiseMode & 0b111); /* Noise reset (bit 3) always reads 0 */ ChangeBit(ref retVal, 4, channel4.NoiseEnable); break; case 0x8F: /* REG_SND_WAVE_BASE */ retVal |= waveTableBase; break; case 0x90: /* REG_SND_CTRL */ ChangeBit(ref retVal, 0, channel1.IsEnabled); ChangeBit(ref retVal, 1, channel2.IsEnabled); ChangeBit(ref retVal, 2, channel3.IsEnabled); ChangeBit(ref retVal, 3, channel4.IsEnabled); ChangeBit(ref retVal, 5, channel2.IsVoiceEnabled); ChangeBit(ref retVal, 6, channel3.IsSweepEnabled); ChangeBit(ref retVal, 7, channel4.IsNoiseEnabled); break; case 0x91: /* REG_SND_OUTPUT */ ChangeBit(ref retVal, 0, speakerEnable); ChangeBit(ref retVal, 3, headphoneEnable); ChangeBit(ref retVal, 7, headphonesConnected); retVal |= (byte)((speakerVolumeShift & 0b11) << 1); break; case 0x92: case 0x93: /* REG_SND_RANDOM */ retVal |= (byte)((channel4.NoiseLfsr >> ((port & 0b1) * 8)) & 0xFF); break; case 0x94: /* REG_SND_VOICE_CTRL */ ChangeBit(ref retVal, 0, channel2.PcmRightFull); ChangeBit(ref retVal, 1, channel2.PcmRightHalf); ChangeBit(ref retVal, 2, channel2.PcmLeftFull); ChangeBit(ref retVal, 3, channel2.PcmLeftHalf); break; case 0x9E: /* REG_SND_VOLUME */ retVal |= (byte)(masterVolume & 0b11); break; default: throw new NotImplementedException($"Unimplemented sound register read {port:X2}"); } return retVal; } public virtual void WritePort(ushort port, byte value) { switch (port) { case 0x80: case 0x81: /* REG_SND_CH1_PITCH */ channel1.Pitch &= (ushort)((port & 0b1) != 0b1 ? 0x0700 : 0x00FF); channel1.Pitch |= (ushort)(value << ((port & 0b1) * 8)); break; case 0x82: case 0x83: /* REG_SND_CH2_PITCH */ channel2.Pitch &= (ushort)((port & 0b1) != 0b1 ? 0x0700 : 0x00FF); channel2.Pitch |= (ushort)(value << ((port & 0b1) * 8)); break; case 0x84: case 0x85: /* REG_SND_CH3_PITCH */ channel3.Pitch &= (ushort)((port & 0b1) != 0b1 ? 0x0700 : 0x00FF); channel3.Pitch |= (ushort)(value << ((port & 0b1) * 8)); break; case 0x86: case 0x87: /* REG_SND_CH4_PITCH */ channel4.Pitch &= (ushort)((port & 0b1) != 0b1 ? 0x0700 : 0x00FF); channel4.Pitch |= (ushort)(value << ((port & 0b1) * 8)); break; case 0x88: /* REG_SND_CH1_VOL */ channel1.VolumeLeft = (byte)((value >> 4) & 0b1111); channel1.VolumeRight = (byte)((value >> 0) & 0b1111); break; case 0x89: /* REG_SND_CH2_VOL */ channel2.VolumeLeft = (byte)((value >> 4) & 0b1111); channel2.VolumeRight = (byte)((value >> 0) & 0b1111); break; case 0x8A: /* REG_SND_CH3_VOL */ channel3.VolumeLeft = (byte)((value >> 4) & 0b1111); channel3.VolumeRight = (byte)((value >> 0) & 0b1111); break; case 0x8B: /* REG_SND_CH4_VOL */ channel4.VolumeLeft = (byte)((value >> 4) & 0b1111); channel4.VolumeRight = (byte)((value >> 0) & 0b1111); break; case 0x8C: /* REG_SND_SWEEP_VALUE */ channel3.SweepValue = (sbyte)value; break; case 0x8D: /* REG_SND_SWEEP_TIME */ channel3.SweepTime = (byte)(value & 0b11111); break; case 0x8E: /* REG_SND_NOISE */ channel4.NoiseMode = (byte)(value & 0b111); channel4.NoiseReset = IsBitSet(value, 3); channel4.NoiseEnable = IsBitSet(value, 4); break; case 0x8F: /* REG_SND_WAVE_BASE */ waveTableBase = value; break; case 0x90: /* REG_SND_CTRL */ channel1.IsEnabled = IsBitSet(value, 0); channel2.IsEnabled = IsBitSet(value, 1); channel3.IsEnabled = IsBitSet(value, 2); channel4.IsEnabled = IsBitSet(value, 3); channel2.IsVoiceEnabled = IsBitSet(value, 5); channel3.IsSweepEnabled = IsBitSet(value, 6); channel4.IsNoiseEnabled = IsBitSet(value, 7); break; case 0x91: /* REG_SND_OUTPUT */ speakerEnable = IsBitSet(value, 0); speakerVolumeShift = (byte)((value >> 1) & 0b11); headphoneEnable = IsBitSet(value, 3); /* Headphones connected (bit 7) is read-only */ break; case 0x92: case 0x93: /* REG_SND_RANDOM */ break; case 0x94: /* REG_SND_VOICE_CTRL */ channel2.PcmRightFull = IsBitSet(value, 0); channel2.PcmRightHalf = IsBitSet(value, 1); channel2.PcmLeftFull = IsBitSet(value, 2); channel2.PcmLeftHalf = IsBitSet(value, 3); break; case 0x9E: /* REG_SND_VOLUME */ masterVolume = (byte)(value & 0b11); break; default: throw new NotImplementedException($"Unimplemented sound register write {port:X2}"); } } [Port("REG_SND_CH1_PITCH", 0x080, 0x081)] [BitDescription("Channel 1 pitch (frequency reload)", 0, 10)] public ushort Channel1Pitch => channel1.Pitch; [Port("REG_SND_CH2_PITCH", 0x082, 0x083)] [BitDescription("Channel 2 pitch (frequency reload)", 0, 10)] public ushort Channel2Pitch => channel2.Pitch; [Port("REG_SND_CH3_PITCH", 0x084, 0x085)] [BitDescription("Channel 3 pitch (frequency reload)", 0, 10)] public ushort Channel3Pitch => channel3.Pitch; [Port("REG_SND_CH4_PITCH", 0x086, 0x087)] [BitDescription("Channel 4 pitch (frequency reload)", 0, 10)] public ushort Channel4Pitch => channel4.Pitch; [Port("REG_SND_CH1_VOL", 0x088)] [BitDescription("Channel 1 volume right", 0, 3)] public byte Channel1VolumeRight => channel1.VolumeRight; [Port("REG_SND_CH1_VOL", 0x088)] [BitDescription("Channel 1 volume left", 4, 7)] public byte Channel1VolumeLeft => channel1.VolumeLeft; [Port("REG_SND_CH2_VOL", 0x089)] [BitDescription("Channel 2 volume right", 0, 3)] public byte Channel2VolumeRight => channel2.VolumeRight; [Port("REG_SND_CH2_VOL", 0x089)] [BitDescription("Channel 2 volume left", 4, 7)] public byte Channel2VolumeLeft => channel2.VolumeLeft; [Port("REG_SND_CH3_VOL", 0x08A)] [BitDescription("Channel 3 volume right", 0, 3)] public byte Channel3VolumeRight => channel3.VolumeRight; [Port("REG_SND_CH3_VOL", 0x08A)] [BitDescription("Channel 3 volume left", 4, 7)] public byte Channel3VolumeLeft => channel3.VolumeLeft; [Port("REG_SND_CH4_VOL", 0x08B)] [BitDescription("Channel 4 volume right", 0, 3)] public byte Channel4VolumeRight => channel4.VolumeRight; [Port("REG_SND_CH4_VOL", 0x08B)] [BitDescription("Channel 4 volume left", 4, 7)] public byte Channel4VolumeLeft => channel4.VolumeLeft; [Port("REG_SND_SWEEP_VALUE", 0x08C)] [BitDescription("Channel 3 sweep value")] public sbyte Channel3SweepValue => channel3.SweepValue; [Port("REG_SND_SWEEP_TIME", 0x08D)] [BitDescription("Channel 3 sweep time", 0, 4)] public byte Channel3SweepTime => channel3.SweepTime; [Port("REG_SND_NOISE", 0x08E)] [BitDescription("Channel 4 noise mode", 0, 2)] public byte Channel4NoiseMode => channel4.NoiseMode; [Port("REG_SND_NOISE", 0x08E)] [BitDescription("Is channel 4 noise enabled?", 4)] public bool Channel4NoiseEnable => channel4.NoiseEnable; [Port("REG_SND_WAVE_BASE", 0x08F)] [BitDescription("Wavetable base address")] [Format("X4", 6)] public byte WaveTableBase => waveTableBase; [Port("REG_SND_CTRL", 0x090)] [BitDescription("Is channel 1 enabled?", 0)] public bool Channel1IsEnabled => channel1.IsEnabled; [Port("REG_SND_CTRL", 0x090)] [BitDescription("Is channel 2 enabled?", 1)] public bool Channel2IsEnabled => channel2.IsEnabled; [Port("REG_SND_CTRL", 0x090)] [BitDescription("Is channel 3 enabled?", 2)] public bool Channel3IsEnabled => channel3.IsEnabled; [Port("REG_SND_CTRL", 0x090)] [BitDescription("Is channel 4 enabled?", 3)] public bool Channel4IsEnabled => channel4.IsEnabled; [Port("REG_SND_CTRL", 0x090)] [BitDescription("Is channel 2 in voice mode?", 5)] public bool Channel2IsVoiceEnabled => channel2.IsVoiceEnabled; [Port("REG_SND_CTRL", 0x090)] [BitDescription("Is channel 3 in sweep mode?", 6)] public bool Channel3IsSweepEnabled => channel3.IsSweepEnabled; [Port("REG_SND_CTRL", 0x090)] [BitDescription("Is channel 4 in noise mode?", 7)] public bool Channel4IsNoiseEnabled => channel4.IsNoiseEnabled; [Port("REG_SND_OUTPUT", 0x091)] [BitDescription("Is speaker enabled?", 0)] public bool SpeakerEnable => speakerEnable; [Port("REG_SND_OUTPUT", 0x091)] [BitDescription("Speaker PWM volume bitshift", 1, 2)] public byte SpeakerVolumeShift => speakerVolumeShift; [Port("REG_SND_OUTPUT", 0x091)] [BitDescription("Are headphones enabled?", 3)] public bool HeadphoneEnable => headphoneEnable; [Port("REG_SND_OUTPUT", 0x091)] [BitDescription("Are headphones connected?", 7)] public bool HeadphonesConnected => headphonesConnected; [Port("REG_SND_RANDOM", 0x092, 0x093)] [BitDescription("Current noise LFSR value", 0, 14)] [Format("X4")] public ushort Channel4NoiseLfsr => channel4.NoiseLfsr; [Port("REG_SND_VOLUME", 0x09E)] [BitDescription("Master volume level", 0, 1)] public byte MasterVolume => masterVolume; } }