Added AY38912 support
This commit is contained in:
69
Core/Audio/Ay38912.cs
Normal file
69
Core/Audio/Ay38912.cs
Normal file
@@ -0,0 +1,69 @@
|
||||
using System;
|
||||
|
||||
namespace Core.Audio
|
||||
{
|
||||
public class Ay38912
|
||||
{
|
||||
private readonly byte[] _registers = new byte[16];
|
||||
private byte _selectedRegister = 0;
|
||||
|
||||
// The AY-3-8912 uses logarithmic volume, not linear!
|
||||
// This table converts the 0-15 register value into a precise float multiplier.
|
||||
private readonly float[] _volumeTable = {
|
||||
0.0000f, 0.0137f, 0.0205f, 0.0291f, 0.0423f, 0.0618f, 0.0847f, 0.1369f,
|
||||
0.1691f, 0.2647f, 0.3527f, 0.4499f, 0.5704f, 0.6873f, 0.8482f, 1.0000f
|
||||
};
|
||||
|
||||
// --- Hardware Port Intercepts ---
|
||||
public void SelectRegister(byte register)
|
||||
{
|
||||
_selectedRegister = (byte)(register & 0x0F); // Only 16 registers exist
|
||||
}
|
||||
|
||||
public void WriteRegister(byte value)
|
||||
{
|
||||
_registers[_selectedRegister] = value;
|
||||
}
|
||||
|
||||
public byte ReadRegister()
|
||||
{
|
||||
return _registers[_selectedRegister];
|
||||
}
|
||||
|
||||
// --- Audio Math Generators ---
|
||||
|
||||
// The AY clock on a Spectrum 128 is 1.7734 MHz.
|
||||
// The chip divides this by 16 internally, giving a base clock of ~110,837 Hz.
|
||||
private float GetFrequency(int registerLow, int registerHigh)
|
||||
{
|
||||
int period = _registers[registerLow] | ((_registers[registerHigh] & 0x0F) << 8);
|
||||
if (period == 0) return 0f; // Prevent divide-by-zero
|
||||
return 110837.5f / period;
|
||||
}
|
||||
|
||||
private float GetVolume(int volumeRegister)
|
||||
{
|
||||
byte vol = _registers[volumeRegister];
|
||||
|
||||
// Bit 4 indicates if the channel is using the hardware Envelope generator.
|
||||
// For Version 1 of our chip, if the envelope is active, we will just
|
||||
// output half volume so the game doesn't go silent!
|
||||
if ((vol & 0x10) != 0)
|
||||
{
|
||||
return 0.5f;
|
||||
}
|
||||
|
||||
return _volumeTable[vol & 0x0F];
|
||||
}
|
||||
|
||||
// --- Public Data for your Audio Engine ---
|
||||
public float FreqA => GetFrequency(0, 1);
|
||||
public float FreqB => GetFrequency(2, 3);
|
||||
public float FreqC => GetFrequency(4, 5);
|
||||
|
||||
// Register 7 is the Mixer. Bit 0, 1, and 2 turn the tone channels OFF when set to 1.
|
||||
public float VolA => (_registers[7] & 0x01) == 0 ? GetVolume(8) : 0f;
|
||||
public float VolB => (_registers[7] & 0x02) == 0 ? GetVolume(9) : 0f;
|
||||
public float VolC => (_registers[7] & 0x04) == 0 ? GetVolume(10) : 0f;
|
||||
}
|
||||
}
|
||||
@@ -547,7 +547,7 @@ namespace Core.Cpu
|
||||
return (ushort)((high << 8) | low);
|
||||
}
|
||||
|
||||
private int ExecuteOpcode(byte opcode)
|
||||
internal int ExecuteOpcode(byte opcode)
|
||||
{
|
||||
sbyte offset = 0;
|
||||
byte oldCarry = 0;
|
||||
@@ -1588,6 +1588,32 @@ namespace Core.Cpu
|
||||
AF.Low = flags;
|
||||
return 16;
|
||||
}
|
||||
case 0xAB: // OUTD
|
||||
{
|
||||
// 1. Read the byte from memory at the HL address (Applying Wait States!)
|
||||
byte outdVal = ReadMemory(HL.Word);
|
||||
|
||||
// 2. Decrement the B register
|
||||
BC.High--;
|
||||
|
||||
// 3. Output that byte to the hardware port stored in BC
|
||||
_simpleIoBus.WritePort(BC.Word, outdVal);
|
||||
|
||||
// 4. Decrement HL
|
||||
HL.Word--;
|
||||
|
||||
// 5. Update Flags
|
||||
// The N flag (Bit 1) is always set.
|
||||
AF.Low |= 0x02;
|
||||
|
||||
// The Z flag (Bit 6) is set if B reaches 0, otherwise cleared.
|
||||
if (BC.High == 0)
|
||||
AF.Low |= 0x40;
|
||||
else
|
||||
AF.Low &= 0xBF;
|
||||
|
||||
return 16; // Takes 16 T-States
|
||||
}
|
||||
case 0xB9: // CPDR
|
||||
{
|
||||
byte memVal = ReadMemory(HL.Word);
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
{
|
||||
public interface IAudioDevice
|
||||
{
|
||||
void AddSample(bool isHigh);
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Diagnostics;
|
||||
using Core.Audio;
|
||||
using Core.Interfaces;
|
||||
using Core.Memory;
|
||||
using System.Diagnostics;
|
||||
using static System.Runtime.InteropServices.JavaScript.JSType;
|
||||
|
||||
namespace Core.Io
|
||||
@@ -8,6 +9,7 @@ namespace Core.Io
|
||||
public class IO_Bus
|
||||
{
|
||||
public byte BorderColourIndex { get; set; } = 7;
|
||||
public Ay38912 AyChip { get; private set; } = new Ay38912();
|
||||
public byte KempstonState { get; set; } = 0x00;
|
||||
public bool BeeperState { get; private set; } = false;
|
||||
public byte[] KeyboardRows = new byte[8] { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF };
|
||||
@@ -49,9 +51,13 @@ namespace Core.Io
|
||||
{
|
||||
return KempstonState;
|
||||
}
|
||||
// AY-3-8912 Data Read (Port 0xFFFD)
|
||||
if ((portAddress & 0xC002) == 0xC000)
|
||||
{
|
||||
return AyChip.ReadRegister();
|
||||
}
|
||||
|
||||
//Return 0xFF for unhandled ports
|
||||
return 0x00;
|
||||
return 0xFF; // Default floating bus
|
||||
}
|
||||
|
||||
public void WritePort(ushort portAddress, byte portValue)
|
||||
@@ -79,6 +85,16 @@ namespace Core.Io
|
||||
{
|
||||
_memory.HandlePaging(0x1FFD, portValue);
|
||||
}
|
||||
// AY-3-8912 Register Select (Port 0xFFFD)
|
||||
if ((portAddress & 0xC002) == 0xC000)
|
||||
{
|
||||
AyChip.SelectRegister(portValue);
|
||||
}
|
||||
// AY-3-8912 Data Write (Port 0xBFFD)
|
||||
else if ((portAddress & 0xC002) == 0x8000)
|
||||
{
|
||||
AyChip.WriteRegister(portValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using Core.Interfaces;
|
||||
using Core.Interfaces;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Core.Memory
|
||||
{
|
||||
@@ -46,6 +47,7 @@ namespace Core.Memory
|
||||
// Called by the IO Bus when the CPU writes to a paging port
|
||||
public void HandlePaging(ushort port, byte value)
|
||||
{
|
||||
Debug.WriteLine($"[PAGING] 0x7FFD Triggered! Hex: 0x{value:X2} | Bank: {value & 0x07}");
|
||||
if (_pagingDisabled || _currentRomBank == 4) return; // Ignore if locked or in 48K mode
|
||||
|
||||
if (port == 0x7FFD)
|
||||
@@ -65,33 +67,62 @@ namespace Core.Memory
|
||||
_currentRomBank = (romHigh << 1) | romLow;
|
||||
}
|
||||
|
||||
public byte Read(ushort address)
|
||||
private int GetBankForSlot(int slot)
|
||||
{
|
||||
int slot = address >> 14; // Divide by 16384 to find Slot 0, 1, 2, or 3
|
||||
int offset = address & 0x3FFF; // Find the exact byte inside that 16KB slot
|
||||
// Amstrad +2A / +3 Special Paging Mode
|
||||
bool specialPaging = (_port1FFD & 0x01) != 0;
|
||||
|
||||
if (specialPaging)
|
||||
{
|
||||
// Bits 1 and 2 dictate the specific Special RAM layout
|
||||
int config = (_port1FFD >> 1) & 0x03;
|
||||
switch (config)
|
||||
{
|
||||
case 0: return slot == 0 ? 0 : slot == 1 ? 1 : slot == 2 ? 2 : 3;
|
||||
case 1: return slot == 0 ? 4 : slot == 1 ? 5 : slot == 2 ? 6 : 7;
|
||||
case 2: return slot == 0 ? 4 : slot == 1 ? 5 : slot == 2 ? 6 : 3;
|
||||
case 3: return slot == 0 ? 4 : slot == 1 ? 7 : slot == 2 ? 6 : 3;
|
||||
}
|
||||
}
|
||||
|
||||
// Standard 128K Paging
|
||||
switch (slot)
|
||||
{
|
||||
case 0: return _romBanks[_currentRomBank][offset]; // 0x0000 - 0x3FFF (ROM)
|
||||
case 1: return _ramBanks[5][offset]; // 0x4000 - 0x7FFF (Always RAM 5)
|
||||
case 2: return _ramBanks[2][offset]; // 0x8000 - 0xBFFF (Always RAM 2)
|
||||
case 3: return _ramBanks[_currentRamBankSlot3][offset]; // 0xC000 - 0xFFFF (Switchable RAM)
|
||||
default: return 0xFF;
|
||||
case 1: return 5;
|
||||
case 2: return 2;
|
||||
case 3: return _currentRamBankSlot3;
|
||||
default: return -1; // -1 means it is pointing to a ROM chip!
|
||||
}
|
||||
}
|
||||
public byte Read(ushort address)
|
||||
{
|
||||
int slot = address >> 14; // Find the 16KB Slot (0, 1, 2, or 3)
|
||||
int offset = address & 0x3FFF;
|
||||
|
||||
int bank = GetBankForSlot(slot);
|
||||
|
||||
if (bank == -1)
|
||||
{
|
||||
return _romBanks[_currentRomBank][offset];
|
||||
}
|
||||
else
|
||||
{
|
||||
return _ramBanks[bank][offset];
|
||||
}
|
||||
}
|
||||
|
||||
public void Write(ushort address, byte value)
|
||||
{
|
||||
if (address < 0x4000) return; // Cannot write to ROM
|
||||
|
||||
int slot = address >> 14;
|
||||
int offset = address & 0x3FFF;
|
||||
|
||||
switch (slot)
|
||||
int bank = GetBankForSlot(slot);
|
||||
|
||||
// If the bank is NOT a ROM chip, we are allowed to write to it!
|
||||
// (This allows the BIOS to write to 0x0000 during the self-test)
|
||||
if (bank != -1)
|
||||
{
|
||||
case 1: _ramBanks[5][offset] = value; break;
|
||||
case 2: _ramBanks[2][offset] = value; break;
|
||||
case 3: _ramBanks[_currentRamBankSlot3][offset] = value; break;
|
||||
_ramBanks[bank][offset] = value;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -170,8 +170,20 @@ namespace Core
|
||||
{
|
||||
while (Cpu.TotalTStates >= (long)(audioSampleCount * 79.365))
|
||||
{
|
||||
// Mix the speaker and the tape ear port
|
||||
bool finalAudioOutput = IoBus.BeeperState ^ TapeDeck.EarBit;
|
||||
_beeper.AddSample(finalAudioOutput);
|
||||
|
||||
// Blast the 1-bit Beeper AND the 3-Channel Synth into the audio buffer!
|
||||
_beeper.AddSample(
|
||||
finalAudioOutput,
|
||||
IoBus.AyChip.FreqA,
|
||||
IoBus.AyChip.VolA,
|
||||
IoBus.AyChip.FreqB,
|
||||
IoBus.AyChip.VolB,
|
||||
IoBus.AyChip.FreqC,
|
||||
IoBus.AyChip.VolC
|
||||
);
|
||||
|
||||
audioSampleCount++;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,28 @@
|
||||
using NAudio.Wave;
|
||||
using Core.Interfaces;
|
||||
using System;
|
||||
using System.Threading;
|
||||
|
||||
namespace Desktop
|
||||
{
|
||||
|
||||
public class BeeperDevice : IAudioDevice
|
||||
{
|
||||
private WaveOutEvent _waveOut;
|
||||
private BufferedWaveProvider _buffer;
|
||||
|
||||
// Audio Filtering
|
||||
private float _lastSample = 0.0f;
|
||||
private float _lastFiltered = 0.0f;
|
||||
|
||||
// AY-3-8912 Digital Oscillators (Phase Trackers)
|
||||
private float _phaseA = 0f;
|
||||
private float _phaseB = 0f;
|
||||
private float _phaseC = 0f;
|
||||
|
||||
public BeeperDevice()
|
||||
{
|
||||
_waveOut = new WaveOutEvent();
|
||||
_waveOut.DesiredLatency = 50; // 100ms latency to prevent buffer stutter
|
||||
_waveOut.DesiredLatency = 50;
|
||||
|
||||
// 44.1kHz, 1 channel (Mono), Float format
|
||||
_buffer = new BufferedWaveProvider(WaveFormat.CreateIeeeFloatWaveFormat(44100, 1));
|
||||
@@ -26,21 +33,53 @@ namespace Desktop
|
||||
_waveOut.Play();
|
||||
}
|
||||
|
||||
public void AddSample(bool isHigh)
|
||||
public void AddSample(bool isHigh, float freqA, float volA, float freqB, float volB, float freqC, float volC)
|
||||
{
|
||||
//Buffer overrun check and dump
|
||||
// Buffer overrun check
|
||||
while (_buffer.BufferedDuration.TotalMilliseconds > 80)
|
||||
{
|
||||
Thread.Sleep(1);
|
||||
}
|
||||
|
||||
// Convert the boolean into a physical sound wave (-0.2 or +0.2)
|
||||
float rawSample = isHigh ? 0.2f : -0.2f;
|
||||
// 1. The Beeper (Reduced base volume from 0.2 to 0.1 to make headroom for the AY)
|
||||
float beeper = isHigh ? 0.1f : -0.1f;
|
||||
|
||||
// 2. Channel A Oscillator
|
||||
float ayA = 0f;
|
||||
if (freqA > 0)
|
||||
{
|
||||
_phaseA += freqA / 44100f;
|
||||
if (_phaseA > 1f) _phaseA -= 1f; // Wrap around
|
||||
ayA = (_phaseA < 0.5f ? 0.1f : -0.1f) * volA;
|
||||
}
|
||||
|
||||
// 3. Channel B Oscillator
|
||||
float ayB = 0f;
|
||||
if (freqB > 0)
|
||||
{
|
||||
_phaseB += freqB / 44100f;
|
||||
if (_phaseB > 1f) _phaseB -= 1f;
|
||||
ayB = (_phaseB < 0.5f ? 0.1f : -0.1f) * volB;
|
||||
}
|
||||
|
||||
// 4. Channel C Oscillator
|
||||
float ayC = 0f;
|
||||
if (freqC > 0)
|
||||
{
|
||||
_phaseC += freqC / 44100f;
|
||||
if (_phaseC > 1f) _phaseC -= 1f;
|
||||
ayC = (_phaseC < 0.5f ? 0.1f : -0.1f) * volC;
|
||||
}
|
||||
|
||||
// Mix them all together!
|
||||
float rawSample = beeper + ayA + ayB + ayC;
|
||||
|
||||
// Pass the mixed sound wave through your existing High-Pass filter to stop popping
|
||||
float filteredSample = rawSample - _lastSample + 0.995f * _lastFiltered;
|
||||
_lastSample = rawSample;
|
||||
_lastFiltered = filteredSample;
|
||||
|
||||
// Convert the float to bytes and drop it in the pipe
|
||||
// Convert to bytes and drop it in the pipe
|
||||
byte[] bytes = BitConverter.GetBytes(filteredSample);
|
||||
_buffer.AddSamples(bytes, 0, 4);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user