diff --git a/Core/Audio/Ay38912.cs b/Core/Audio/Ay38912.cs new file mode 100644 index 0000000..f4fc1f9 --- /dev/null +++ b/Core/Audio/Ay38912.cs @@ -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; + } +} \ No newline at end of file diff --git a/Core/Cpu/Z80.cs b/Core/Cpu/Z80.cs index 7a5684c..3a26abd 100644 --- a/Core/Cpu/Z80.cs +++ b/Core/Cpu/Z80.cs @@ -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); diff --git a/Core/Interfaces/IAudioDevice.cs b/Core/Interfaces/IAudioDevice.cs index 8604841..98d2426 100644 --- a/Core/Interfaces/IAudioDevice.cs +++ b/Core/Interfaces/IAudioDevice.cs @@ -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); } } \ No newline at end of file diff --git a/Core/Io/IO_Bus.cs b/Core/Io/IO_Bus.cs index df8a97a..d547c0d 100644 --- a/Core/Io/IO_Bus.cs +++ b/Core/Io/IO_Bus.cs @@ -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); + } } } } \ No newline at end of file diff --git a/Core/Memory/MemoryBus.cs b/Core/Memory/MemoryBus.cs index 03df737..fb2aa14 100644 --- a/Core/Memory/MemoryBus.cs +++ b/Core/Memory/MemoryBus.cs @@ -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; } } diff --git a/Core/SpectrumMachine.cs b/Core/SpectrumMachine.cs index b26b46f..44d281b 100644 --- a/Core/SpectrumMachine.cs +++ b/Core/SpectrumMachine.cs @@ -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++; } } diff --git a/Desktop/BeeperDevice.cs b/Desktop/BeeperDevice.cs index 3e431a5..aa9c175 100644 --- a/Desktop/BeeperDevice.cs +++ b/Desktop/BeeperDevice.cs @@ -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); }