245 lines
8.7 KiB
C#
245 lines
8.7 KiB
C#
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)
|
|
// --- 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
|
|
};
|
|
|
|
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)
|
|
{
|
|
// 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);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
//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;
|
|
// }
|
|
|
|
|
|
// }
|
|
//} |