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