using Core.Interfaces; using System.IO; 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) // --- FILTER VARIABLES --- private float _previousSample = 0f; private float _previousFiltered = 0f; // 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 }; #pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable. public SmsApu() #pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable. { 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) { // 1. Reload the counter 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 // 2. NO FLIP FLOP! Shift the LFSR immediately! bool isWhiteNoise = (Registers[6] & 0x04) != 0; // Read the bits BEFORE shifting int bit0 = _lfsr & 1; int bit3 = (_lfsr >> 3) & 1; // The Master System physically XORs bit 0 and bit 3 for White Noise. // For Periodic Noise, it just feeds bit 0 straight back in. int feedback = isWhiteNoise ? (bit0 ^ bit3) : bit0; // Shift the register right and push the feedback into the highest bit (Bit 15) _lfsr = (ushort)((_lfsr >> 1) | (feedback << 15)); // The noise speaker is driven directly by the lowest bit _polarities[3] = (_lfsr & 1) == 1 ? 1 : -1; } } private void MixAndOutputSample() { if (AudioDevice == null) return; float sample = 0f; // Mix Tone Channels 0, 1, 2 for (int i = 0; i < 3; i++) { int tone = Registers[i * 2]; // THE FIX 1: Catch ALL ultrasonic frequencies (1 through 5) to stop Aliasing! if (tone >= 0 && tone <= 5) { // THE FIX 2: Scale it by 0.5f. A fast square wave spends 50% of its time // high, so it naturally averages out to half volume. This prevents clipping! sample += 0.5f * VolumeTable[Registers[(i * 2) + 1]]; } else { 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 sample /= 4.0f; // THE FIX 3: The DC Blocker (1-Pole High-Pass Filter) // PCM speech creates a massive DC offset (pushes the speaker cone forward and leaves it there). // This filter smoothly pulls the speaker cone back to center at 35Hz, removing the hum and distortion! float filteredSample = sample - _previousSample + 0.995f * _previousFiltered; _previousSample = sample; _previousFiltered = filteredSample; AudioDevice.AddSample(filteredSample); } 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; if (_latchedRegister == 6) _lfsr = 0x8000; } 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); if (_latchedRegister == 6) _lfsr = 0x8000; } } 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); } } } public void SaveState(BinaryWriter bw) { for (int i = 0; i < 8; i++) bw.Write(Registers[i]); bw.Write(_latchedRegister); bw.Write(_sampleCycleTracker); bw.Write(_psgCycleTracker); for (int i = 0; i < 4; i++) { bw.Write(_counters[i]); bw.Write(_polarities[i]); } bw.Write(_lfsr); bw.Write(_previousSample); bw.Write(_previousFiltered); } public void LoadState(BinaryReader br) { for (int i = 0; i < 8; i++) Registers[i] = br.ReadUInt16(); _latchedRegister = br.ReadInt32(); _sampleCycleTracker = br.ReadDouble(); _psgCycleTracker = br.ReadInt32(); for (int i = 0; i < 4; i++) { _counters[i] = br.ReadInt32(); _polarities[i] = br.ReadInt32(); } _lfsr = br.ReadUInt16(); _previousSample = br.ReadSingle(); _previousFiltered = br.ReadSingle(); } } }