diff --git a/Core/Audio/SmsApu.cs b/Core/Audio/SmsApu.cs new file mode 100644 index 0000000..338d34c --- /dev/null +++ b/Core/Audio/SmsApu.cs @@ -0,0 +1,224 @@ +using Core.Interfaces; + +namespace Core.Audio +{ + public class SmsApu + { + // Your existing variables + public ushort[] Registers { get; private set; } = new ushort[8]; + private int _latchedRegister = 0; + + // THE NEW CONNECTION: Where we send the audio! + public IAudioDevice AudioDevice { get; set; } + + // --- TIMING VARIABLES --- + private const double ClockRate = 3579545.0; // NTSC Master System speed + private const int SampleRate = 44100; // CD-Quality Audio + private double _cyclesPerSample = ClockRate / SampleRate; + private double _sampleCycleTracker = 0; + private int _psgCycleTracker = 0; + + // --- SYNTHESIZER STATE --- + private int[] _counters = new int[4]; + private int[] _polarities = new int[4] { 1, 1, 1, 1 }; // 1 = High, -1 = Low + private ushort _lfsr = 0x8000; // Linear Feedback Shift Register (For Noise) + + // The SN76489 Volume Table reduces amplitude by exactly 2 decibels per step. + private static readonly float[] VolumeTable = { + 1.0f, 0.7943f, 0.6309f, 0.5011f, 0.3981f, 0.3162f, 0.2511f, 0.1995f, + 0.1584f, 0.1258f, 0.1000f, 0.0794f, 0.0630f, 0.0501f, 0.0398f, 0.0f + }; + + public SmsApu() + { + Registers[1] = 0x0F; + Registers[3] = 0x0F; + Registers[5] = 0x0F; + Registers[7] = 0x0F; + } + + // [KEEP YOUR EXISTING WritePort7F METHOD HERE] + + public void Update(int tStates) + { + for (int i = 0; i < tStates; i++) + { + // 1. The hardware chip only updates its wave counters every 16 CPU cycles + _psgCycleTracker++; + if (_psgCycleTracker >= 16) + { + _psgCycleTracker = 0; + TickChannels(); + } + + // 2. We only want to generate 44,100 samples per second, not 3.58 million! + _sampleCycleTracker++; + if (_sampleCycleTracker >= _cyclesPerSample) + { + _sampleCycleTracker -= _cyclesPerSample; + MixAndOutputSample(); + } + } + } + + private void TickChannels() + { + // --- TONE CHANNELS (0, 1, and 2) --- + for (int i = 0; i < 3; i++) + { + _counters[i]--; + if (_counters[i] <= 0) + { + // Reload the counter from the Tone Register. + // HARDWARE QUIRK: A tone register of 0 acts as 1024! + int tone = Registers[i * 2]; + _counters[i] = (tone == 0) ? 1024 : tone; + + // Flip the wave polarity! (This creates the vibration of the sound) + _polarities[i] *= -1; + } + } + + // --- NOISE CHANNEL (3) --- + _counters[3]--; + if (_counters[3] <= 0) + { + // Noise rate depends on Bits 0-1 of Register 6 + int shiftRate = Registers[6] & 0x03; + if (shiftRate == 0) _counters[3] = 0x10; // Fast + else if (shiftRate == 1) _counters[3] = 0x20; // Medium + else if (shiftRate == 2) _counters[3] = 0x40; // Slow + else _counters[3] = (Registers[4] == 0) ? 1024 : Registers[4]; // Linked to Tone 2! + + // Shift the Noise LFSR + int tappedBit = _lfsr & 1; + _lfsr >>= 1; + + if (tappedBit == 1) + { + bool isWhiteNoise = (Registers[6] & 0x04) != 0; + // The Sega Master System physically tapped bits 0 and 3 for its white noise + if (isWhiteNoise) _lfsr ^= 0x0009; + + _lfsr |= 0x8000; // Inject the high bit + } + + _polarities[3] = (tappedBit == 1) ? 1 : -1; + } + } + + private void MixAndOutputSample() + { + // If the UI hasn't hooked up the speakers yet, just throw the audio away! + if (AudioDevice == null) return; + + float sample = 0f; + + // Mix Tone Channels 0, 1, 2 + for (int i = 0; i < 3; i++) + { + // HARDWARE QUIRK: If the tone frequency is 1 or 0, the channel outputs a + // constant DC voltage instead of vibrating, meaning it is effectively silent. + if (Registers[i * 2] > 1) + { + sample += _polarities[i] * VolumeTable[Registers[(i * 2) + 1]]; + } + } + + // Mix Noise Channel 3 + sample += _polarities[3] * VolumeTable[Registers[7]]; + + // Divide by 4 so all 4 channels together never exceed 1.0f (which would cause horrible distortion!) + sample /= 4.0f; + + AudioDevice.AddSample(sample); + } + public void WritePort7F(byte value) + { + if ((value & 0x80) != 0) + { + // --- LATCH BYTE --- (Bit 7 is 1) + // Bits 4-6 contain the Register Index (0 to 7) + _latchedRegister = (value >> 4) & 0x07; + + // Bits 0-3 contain the lower 4 bits of data + int data = value & 0x0F; + + if (_latchedRegister % 2 != 0 || _latchedRegister == 6) + { + // Volume registers (1, 3, 5, 7) and Noise Control (6) only hold 4 bits total. + // We completely overwrite them. + Registers[_latchedRegister] = (ushort)data; + } + else + { + // Tone registers (0, 2, 4) hold 10 bits. + // A Latch byte ONLY overwrites the bottom 4 bits and leaves the top 6 alone! + Registers[_latchedRegister] = (ushort)((Registers[_latchedRegister] & 0x03F0) | data); + } + } + else + { + // --- DATA BYTE --- (Bit 7 is 0) + // Bits 0-5 contain the upper 6 bits of data for the currently latched Tone register + int data = value & 0x3F; + + if (_latchedRegister % 2 == 0 && _latchedRegister != 6) + { + // Update the top 6 bits of the 10-bit Tone register + Registers[_latchedRegister] = (ushort)((Registers[_latchedRegister] & 0x000F) | (data << 4)); + } + else + { + // If a Data Byte is sent to a Volume or Noise register, it just overwrites the lower 4 bits again + Registers[_latchedRegister] = (ushort)(data & 0x0F); + } + } + } + } +} + + + + + + + + + + + + + +//using System; + +//namespace Core.Audio +//{ +// public class SmsApu +// { +// // The 8 internal registers of the PSG +// // 0: Tone 0 Frequency (10 bits) +// // 1: Tone 0 Volume (4 bits) +// // 2: Tone 1 Frequency (10 bits) +// // 3: Tone 1 Volume (4 bits) +// // 4: Tone 2 Frequency (10 bits) +// // 5: Tone 2 Volume (4 bits) +// // 6: Noise Control (3 bits) +// // 7: Noise Volume (4 bits) +// public ushort[] Registers { get; private set; } = new ushort[8]; + +// // Remembers which register the CPU is currently talking to +// private int _latchedRegister = 0; + +// public SmsApu() +// { +// // Volumes default to 0x0F (Silence! 0 = max volume, 15 = off) +// Registers[1] = 0x0F; +// Registers[3] = 0x0F; +// Registers[5] = 0x0F; +// Registers[7] = 0x0F; +// } + + +// } +//} \ No newline at end of file diff --git a/Core/Core.csproj b/Core/Core.csproj index 0f32268..5e6b940 100644 --- a/Core/Core.csproj +++ b/Core/Core.csproj @@ -6,12 +6,6 @@ enable - - - - - - diff --git a/Core/Interfaces/IAudioDevice.cs b/Core/Interfaces/IAudioDevice.cs index 98d2426..6a7e435 100644 --- a/Core/Interfaces/IAudioDevice.cs +++ b/Core/Interfaces/IAudioDevice.cs @@ -2,7 +2,7 @@ { public interface IAudioDevice { - // Now accepts the Beeper state + 3 AY frequencies + 3 AY volumes - void AddSample(bool isHigh, float freqA, float volA, float freqB, float volB, float freqC, float volC); + // Accepts a single, fully-mixed audio sample (usually between -1.0f and 1.0f) + void AddSample(float sample); } } \ No newline at end of file diff --git a/Core/Io/SmsIoBus.cs b/Core/Io/SmsIoBus.cs index 9255b07..045ed5b 100644 --- a/Core/Io/SmsIoBus.cs +++ b/Core/Io/SmsIoBus.cs @@ -1,4 +1,5 @@ -using Core.Interfaces; +using Core.Audio; +using Core.Interfaces; using Core.Video; namespace Core.Io @@ -6,7 +7,7 @@ namespace Core.Io public class SmsIoBus : IIoBus { public SmsVdp VideoProcessor { get; set; } - // public Psg AudioProcessor { get; set; } + public SmsApu AudioProcessor { get; set; } // Joypad State (0xFF means no buttons pressed - the SMS uses Active-Low logic!) public byte Joypad1Keyboard = 0xFF; @@ -42,15 +43,19 @@ namespace Core.Io { byte lowerPort = (byte)(port & 0xFF); - if (lowerPort >= 0x40 && lowerPort <= 0x7F) + // Audio Ports + if (lowerPort == 0x7E || lowerPort == 0x7F) { - // PSG Audio Write (Usually written exactly to 0x7F) - // AudioProcessor.WriteData(value); + AudioProcessor.WritePort7F(value); } - else if (lowerPort >= 0x80 && lowerPort <= 0xBF) + // Video Ports + else if (lowerPort == 0xBE) { - if ((lowerPort & 0x01) == 0) VideoProcessor.WriteDataPort(value); - else VideoProcessor.WriteControlPort(value); + VideoProcessor.WriteDataPort(value); + } + else if (lowerPort == 0xBF) + { + VideoProcessor.WriteControlPort(value); } else if (lowerPort <= 0x3F) { diff --git a/Core/SmsMachine.cs b/Core/SmsMachine.cs index 98a16f9..ce410cb 100644 --- a/Core/SmsMachine.cs +++ b/Core/SmsMachine.cs @@ -1,6 +1,8 @@ using Core.Cpu; using Core.Io; using Core.Memory; +using Core.Video; +using Core.Audio; using System; using System.IO; using System.Collections.Generic; @@ -12,7 +14,8 @@ namespace Core public Z80 Cpu { get; private set; } public SmsMemoryBus MemoryBus { get; private set; } public SmsIoBus IoBus { get; private set; } - public Core.Video.SmsVdp VideoProcessor { get; private set; } + public SmsVdp VideoProcessor { get; private set; } + public SmsApu AudioProcessor { get; private set; } public ushort? Breakpoint { get; set; } = null; // NTSC SMS T-States per frame @@ -21,8 +24,9 @@ namespace Core public SmsMachine() { MemoryBus = new SmsMemoryBus(); - VideoProcessor = new Core.Video.SmsVdp(); - IoBus = new SmsIoBus { VideoProcessor = this.VideoProcessor }; + VideoProcessor = new SmsVdp(); + AudioProcessor = new SmsApu(); + IoBus = new SmsIoBus { VideoProcessor = this.VideoProcessor, AudioProcessor = this.AudioProcessor }; Cpu = new Z80(MemoryBus, IoBus); } @@ -50,13 +54,15 @@ namespace Core // 2. Tell the VDP to catch up VideoProcessor.Update(cycles); + AudioProcessor.Update(cycles); // 3. Check if the VDP is begging for attention! if (VideoProcessor.InterruptPending && Cpu.IFF1) { int intCycles = Cpu.RequestInterrupt(); tStatesThisFrame += intCycles; - VideoProcessor.Update(intCycles); // Keep VDP perfectly in sync + VideoProcessor.Update(intCycles); + AudioProcessor.Update(intCycles); } // 4. THE RESTORED BREAKPOINT TRAP diff --git a/Desktop/Desktop.csproj b/Desktop/Desktop.csproj index ea2517e..999caf6 100644 --- a/Desktop/Desktop.csproj +++ b/Desktop/Desktop.csproj @@ -9,31 +9,331 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + Always + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + Always + + Always + + + Always + + + Always + Always Always + + Always + Always + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + Always