Added AY38912 support

This commit is contained in:
2026-04-30 14:35:12 +01:00
parent 3d13425d51
commit d90537de59
7 changed files with 223 additions and 29 deletions

69
Core/Audio/Ay38912.cs Normal file
View 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;
}
}

View File

@@ -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);

View File

@@ -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);
}
}

View File

@@ -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);
}
}
}
}

View File

@@ -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;
}
}

View File

@@ -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++;
}
}