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