using BizHawk.Common; using BizHawk.Common.NumberExtensions; using BizHawk.Emulation.Common; using System; using System.Collections.Generic; namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum { /// /// AY-3-8912 Emulated Device /// /// Based heavily on the YM-2149F / AY-3-8910 emulator used in Unreal Speccy /// (Originally created under Public Domain license by SMT jan.2006) /// /// https://github.com/mkoloberdin/unrealspeccy/blob/master/sndrender/sndchip.cpp /// https://github.com/mkoloberdin/unrealspeccy/blob/master/sndrender/sndchip.h /// public class AY38912 : IPSG { #region Device Fields /// /// The emulated machine (passed in via constructor) /// private SpectrumBase _machine; private int _tStatesPerFrame; private int _sampleRate; private int _samplesPerFrame; private int _tStatesPerSample; private short[] _audioBuffer; private int _audioBufferIndex; private int _lastStateRendered; #endregion #region Construction & Initialization /// /// Main constructor /// public AY38912(SpectrumBase machine) { _machine = machine; } /// /// Initialises the AY chip /// public void Init(int sampleRate, int tStatesPerFrame) { InitTiming(sampleRate, tStatesPerFrame); UpdateVolume(); Reset(); } #endregion #region IPortIODevice /// /// |11-- ---- ---- --0-| - IN - Read value of currently selected register /// /// public bool ReadPort(ushort port, ref int value) { if (!port.Bit(1)) { if ((port >> 14) == 3) { // port read is addressing this device value = PortRead(); return true; } } // port read is not addressing this device return false; } /// /// |11-- ---- ---- --0-| - OUT - Register Select /// |10-- ---- ---- --0-| - OUT - Write value to currently selected register /// /// public bool WritePort(ushort port, int value) { if (!port.Bit(1)) { if ((port >> 14) == 3) { // register select SelectedRegister = value & 0x0f; return true; } else if ((port >> 14) == 2) { // Update the audiobuffer based on the current CPU cycle // (this process the previous data BEFORE writing to the currently selected register) int d = (int)(_machine.CurrentFrameCycle); BufferUpdate(d); // write to register PortWrite(value); return true; } } return false; } #endregion #region AY Implementation #region Public Properties /// /// AY mixer panning configuration /// [Flags] public enum AYPanConfig { MONO = 0, ABC = 1, ACB = 2, BAC = 3, BCA = 4, CAB = 5, CBA = 6, } /// /// The AY panning configuration /// public AYPanConfig PanningConfiguration { get => _currentPanTab; set { if (value != _currentPanTab) { _currentPanTab = value; UpdateVolume(); } } } /// /// The AY chip output volume /// (0 - 100) /// public int Volume { get => _volume; set { //value = Math.Max(0, value); //value = Math.Max(100, value); if (_volume == value) { return; } _volume = value; UpdateVolume(); } } /// /// The currently selected register /// public int SelectedRegister { get => _activeRegister; set => _activeRegister = (byte)value; } /// /// Used for snapshot generation /// public int[] ExportRegisters() { return _registers; } #endregion #region Public Methods /// /// Resets the PSG /// public void Reset() { for (int i = 0; i < 16; i++) { if (i == 6) _registers[i] = 0xff; else _registers[i] = 0; } /* _noiseVal = 0x0FFFF; _outABC = 0; _outNoiseABC = 0; _counterNoise = 0; _counterA = 0; _counterB = 0; _counterC = 0; _EnvelopeCounterBend = 0; // clear all the registers for (int i = 0; i < 14; i++) { SelectedRegister = i; PortWrite(0); } randomSeed = 1; // number of frames to update var fr = (_audioBufferIndex * _tStatesPerFrame) / _audioBuffer.Length; // update the audio buffer BufferUpdate(fr); */ } /// /// Reads the value from the currently selected register /// public int PortRead() { return _registers[_activeRegister]; } /// /// Writes to the currently selected register /// public void PortWrite(int value) { if (_activeRegister >= 0x10) return; byte val = (byte)value; if (((1 << _activeRegister) & ((1 << 1) | (1 << 3) | (1 << 5) | (1 << 13))) != 0) val &= 0x0F; if (((1 << _activeRegister) & ((1 << 6) | (1 << 8) | (1 << 9) | (1 << 10))) != 0) val &= 0x1F; if (_activeRegister != 13 && _registers[_activeRegister] == val) return; _registers[_activeRegister] = val; switch (_activeRegister) { // Channel A (Combined Pitch) // (not written to directly) case 0: case 1: _dividerA = _registers[AY_A_FINE] | (_registers[AY_A_COARSE] << 8); break; // Channel B (Combined Pitch) // (not written to directly) case 2: case 3: _dividerB = _registers[AY_B_FINE] | (_registers[AY_B_COARSE] << 8); break; // Channel C (Combined Pitch) // (not written to directly) case 4: case 5: _dividerC = _registers[AY_C_FINE] | (_registers[AY_C_COARSE] << 8); break; // Noise Pitch case 6: _dividerN = val * 2; break; // Mixer case 7: _bit0 = 0 - ((val >> 0) & 1); _bit1 = 0 - ((val >> 1) & 1); _bit2 = 0 - ((val >> 2) & 1); _bit3 = 0 - ((val >> 3) & 1); _bit4 = 0 - ((val >> 4) & 1); _bit5 = 0 - ((val >> 5) & 1); break; // Channel Volumes case 8: _eMaskA = (val & 0x10) != 0 ? -1 : 0; _vA = ((val & 0x0F) * 2 + 1) & ~_eMaskA; break; case 9: _eMaskB = (val & 0x10) != 0 ? -1 : 0; _vB = ((val & 0x0F) * 2 + 1) & ~_eMaskB; break; case 10: _eMaskC = (val & 0x10) != 0 ? -1 : 0; _vC = ((val & 0x0F) * 2 + 1) & ~_eMaskC; break; // Envelope (Combined Duration) // (not written to directly) case 11: case 12: _dividerE = _registers[AY_E_FINE] | (_registers[AY_E_COARSE] << 8); break; // Envelope Shape case 13: // reset the envelope counter _countE = 0; if ((_registers[AY_E_SHAPE] & 4) != 0) { // attack _eState = 0; _eDirection = 1; } else { // decay _eState = 31; _eDirection = -1; } break; case 14: // IO Port - not implemented break; } } /// /// Start of frame /// public void StartFrame() { _audioBufferIndex = 0; BufferUpdate(0); } /// /// End of frame /// public void EndFrame() { BufferUpdate(_tStatesPerFrame); } /// /// Updates the audiobuffer based on the current frame t-state /// public void UpdateSound(int frameCycle) { BufferUpdate(frameCycle); } #endregion #region Private Fields /// /// Register indicies /// private const int AY_A_FINE = 0; private const int AY_A_COARSE = 1; private const int AY_B_FINE = 2; private const int AY_B_COARSE = 3; private const int AY_C_FINE = 4; private const int AY_C_COARSE = 5; private const int AY_NOISEPITCH = 6; private const int AY_MIXER = 7; private const int AY_A_VOL = 8; private const int AY_B_VOL = 9; private const int AY_C_VOL = 10; private const int AY_E_FINE = 11; private const int AY_E_COARSE = 12; private const int AY_E_SHAPE = 13; private const int AY_PORT_A = 14; private const int AY_PORT_B = 15; /// /// The register array /// /* The AY-3-8910/8912 contains 16 internal registers as follows: Register Function Range 0 Channel A fine pitch 8-bit (0-255) 1 Channel A course pitch 4-bit (0-15) 2 Channel B fine pitch 8-bit (0-255) 3 Channel B course pitch 4-bit (0-15) 4 Channel C fine pitch 8-bit (0-255) 5 Channel C course pitch 4-bit (0-15) 6 Noise pitch 5-bit (0-31) 7 Mixer 8-bit (see below) 8 Channel A volume 4-bit (0-15, see below) 9 Channel B volume 4-bit (0-15, see below) 10 Channel C volume 4-bit (0-15, see below) 11 Envelope fine duration 8-bit (0-255) 12 Envelope course duration 8-bit (0-255) 13 Envelope shape 4-bit (0-15) 14 I/O port A 8-bit (0-255) 15 I/O port B 8-bit (0-255) (Not present on the AY-3-8912) * The volume registers (8, 9 and 10) contain a 4-bit setting but if bit 5 is set then that channel uses the envelope defined by register 13 and ignores its volume setting. * The mixer (register 7) is made up of the following bits (low=enabled): Bit: 7 6 5 4 3 2 1 0 Register: I/O I/O Noise Noise Noise Tone Tone Tone Channel: B A C B A C B A The AY-3-8912 ignores bit 7 of this register. */ private int[] _registers = new int[16]; /// /// The currently selected register /// private byte _activeRegister; /// /// The frequency of the AY chip /// private static int _chipFrequency = 1773400; /// /// The rendering resolution of the chip /// private double _resolution = 50D * 8D / _chipFrequency; /// /// Channel generator state /// private int _bitA; private int _bitB; private int _bitC; /// /// Envelope state /// private int _eState; /// /// Envelope direction /// private int _eDirection; /// /// Noise seed /// private int _noiseSeed; /// /// Mixer state /// private int _bit0; private int _bit1; private int _bit2; private int _bit3; private int _bit4; private int _bit5; /// /// Noise generator state /// private int _bitN; /// /// Envelope masks /// private int _eMaskA; private int _eMaskB; private int _eMaskC; /// /// Amplitudes /// private int _vA; private int _vB; private int _vC; /// /// Channel gen counters /// private int _countA; private int _countB; private int _countC; /// /// Envelope gen counter /// private int _countE; /// /// Noise gen counter /// private int _countN; /// /// Channel gen dividers /// private int _dividerA; private int _dividerB; private int _dividerC; /// /// Envelope gen divider /// private int _dividerE; /// /// Noise gen divider /// private int _dividerN; /// /// Panning table list /// private static List PanTabs = new List { // MONO new uint[] { 50,50, 50,50, 50,50 }, // ABC new uint[] { 100,10, 66,66, 10,100 }, // ACB new uint[] { 100,10, 10,100, 66,66 }, // BAC new uint[] { 66,66, 100,10, 10,100 }, // BCA new uint[] { 10,100, 100,10, 66,66 }, // CAB new uint[] { 66,66, 10,100, 100,10 }, // CBA new uint[] { 10,100, 66,66, 100,10 } }; /// /// The currently selected panning configuration /// private AYPanConfig _currentPanTab = AYPanConfig.ABC; /// /// The current volume /// private int _volume = 75; /// /// Volume tables state /// private uint[][] _volumeTables; /// /// Volume table to be used /// private static uint[] AYVolumes = new uint[] { 0x0000,0x0000,0x0340,0x0340,0x04C0,0x04C0,0x06F2,0x06F2, 0x0A44,0x0A44,0x0F13,0x0F13,0x1510,0x1510,0x227E,0x227E, 0x289F,0x289F,0x414E,0x414E,0x5B21,0x5B21,0x7258,0x7258, 0x905E,0x905E,0xB550,0xB550,0xD7A0,0xD7A0,0xFFFF,0xFFFF, }; #endregion #region Private Methods /// /// Forces an update of the volume tables /// private void UpdateVolume() { int upperFloor = 40000; var inc = (0xFFFF - upperFloor) / 100; var vol = inc * _volume; // ((ulong)0xFFFF * (ulong)_volume / 100UL) - 20000 ; _volumeTables = new uint[6][]; // parent array for (int j = 0; j < _volumeTables.Length; j++) { _volumeTables[j] = new uint[32]; // child array for (int i = 0; i < _volumeTables[j].Length; i++) { _volumeTables[j][i] = (uint)( (PanTabs[(int)_currentPanTab][j] * AYVolumes[i] * vol) / (3 * 65535 * 100)); } } } private int mult_const; /// /// Initializes timing information for the frame /// private void InitTiming(int sampleRate, int frameTactCount) { _sampleRate = sampleRate; _tStatesPerFrame = frameTactCount; _samplesPerFrame = 882; _tStatesPerSample = 79; //(int)Math.Round(((double)_tStatesPerFrame * 50D) / //(16D * (double)_sampleRate), //MidpointRounding.AwayFromZero); //_samplesPerFrame = _tStatesPerFrame / _tStatesPerSample; _audioBuffer = new short[_samplesPerFrame * 2]; //[_sampleRate / 50]; _audioBufferIndex = 0; mult_const = ((_chipFrequency / 8) << 14) / _machine.ULADevice.ClockSpeed; var aytickspercputick = (double)_machine.ULADevice.ClockSpeed / (double)_chipFrequency; int ayCyclesPerSample = (int)((double)_tStatesPerSample * (double)aytickspercputick); } /// /// Updates the audiobuffer based on the current frame t-state /// private void BufferUpdate(int cycle) { if (cycle > _tStatesPerFrame) { // we are outside of the frame - just process the last value cycle = _tStatesPerFrame; } // get the current length of the audiobuffer int bufferLength = _samplesPerFrame; // _audioBuffer.Length; int toEnd = ((bufferLength * cycle) / _tStatesPerFrame); // loop through the number of samples we need to render while (_audioBufferIndex < toEnd) { // run the AY chip processing at the correct resolution for (int i = 0; i < _tStatesPerSample / 14; i++) { if (++_countA >= _dividerA) { _countA = 0; _bitA ^= -1; } if (++_countB >= _dividerB) { _countB = 0; _bitB ^= -1; } if (++_countC >= _dividerC) { _countC = 0; _bitC ^= -1; } if (++_countN >= _dividerN) { _countN = 0; _noiseSeed = (_noiseSeed * 2 + 1) ^ (((_noiseSeed >> 16) ^ (_noiseSeed >> 13)) & 1); _bitN = 0 - ((_noiseSeed >> 16) & 1); } if (++_countE >= _dividerE) { _countE = 0; _eState += +_eDirection; if ((_eState & ~31) != 0) { var mask = (1 << _registers[AY_E_SHAPE]); if ((mask & ((1 << 0) | (1 << 1) | (1 << 2) | (1 << 3) | (1 << 4) | (1 << 5) | (1 << 6) | (1 << 7) | (1 << 9) | (1 << 15))) != 0) { _eState = _eDirection = 0; } else if ((mask & ((1 << 8) | (1 << 12))) != 0) { _eState &= 31; } else if ((mask & ((1 << 10) | (1 << 14))) != 0) { _eDirection = -_eDirection; _eState += _eDirection; } else { // 11,13 _eState = 31; _eDirection = 0; } } } } // mix the sample var mixA = ((_eMaskA & _eState) | _vA) & ((_bitA | _bit0) & (_bitN | _bit3)); var mixB = ((_eMaskB & _eState) | _vB) & ((_bitB | _bit1) & (_bitN | _bit4)); var mixC = ((_eMaskC & _eState) | _vC) & ((_bitC | _bit2) & (_bitN | _bit5)); var l = _volumeTables[0][mixA]; var r = _volumeTables[1][mixA]; l += _volumeTables[2][mixB]; r += _volumeTables[3][mixB]; l += _volumeTables[4][mixC]; r += _volumeTables[5][mixC]; _audioBuffer[_audioBufferIndex * 2] = (short)l; _audioBuffer[(_audioBufferIndex * 2) + 1] = (short)r; _audioBufferIndex++; } _lastStateRendered = cycle; } #endregion #endregion #region ISoundProvider public bool CanProvideAsync => false; public SyncSoundMode SyncMode => SyncSoundMode.Sync; public void SetSyncMode(SyncSoundMode mode) { if (mode != SyncSoundMode.Sync) throw new InvalidOperationException("Only Sync mode is supported."); } public void GetSamplesAsync(short[] samples) { throw new NotSupportedException("Async is not available"); } public void DiscardSamples() { _audioBuffer = new short[_samplesPerFrame * 2]; } public void GetSamplesSync(out short[] samples, out int nsamp) { nsamp = _samplesPerFrame; samples = _audioBuffer; DiscardSamples(); } #endregion #region State Serialization public int nullDump = 0; /// /// State serialization /// public void SyncState(Serializer ser) { ser.BeginSection("PSG-AY"); ser.Sync(nameof(_tStatesPerFrame), ref _tStatesPerFrame); ser.Sync(nameof(_sampleRate), ref _sampleRate); ser.Sync(nameof(_samplesPerFrame), ref _samplesPerFrame); ser.Sync(nameof(_tStatesPerSample), ref _tStatesPerSample); ser.Sync(nameof(_audioBufferIndex), ref _audioBufferIndex); ser.Sync(nameof(_audioBuffer), ref _audioBuffer, false); ser.Sync(nameof(_registers), ref _registers, false); ser.Sync(nameof(_activeRegister), ref _activeRegister); ser.Sync(nameof(_bitA), ref _bitA); ser.Sync(nameof(_bitB), ref _bitB); ser.Sync(nameof(_bitC), ref _bitC); ser.Sync(nameof(_eState), ref _eState); ser.Sync(nameof(_eDirection), ref _eDirection); ser.Sync(nameof(_noiseSeed), ref _noiseSeed); ser.Sync(nameof(_bit0), ref _bit0); ser.Sync(nameof(_bit1), ref _bit1); ser.Sync(nameof(_bit2), ref _bit2); ser.Sync(nameof(_bit3), ref _bit3); ser.Sync(nameof(_bit4), ref _bit4); ser.Sync(nameof(_bit5), ref _bit5); ser.Sync(nameof(_bitN), ref _bitN); ser.Sync(nameof(_eMaskA), ref _eMaskA); ser.Sync(nameof(_eMaskB), ref _eMaskB); ser.Sync(nameof(_eMaskC), ref _eMaskC); ser.Sync(nameof(_vA), ref _vA); ser.Sync(nameof(_vB), ref _vB); ser.Sync(nameof(_vC), ref _vC); ser.Sync(nameof(_countA), ref _countA); ser.Sync(nameof(_countB), ref _countB); ser.Sync(nameof(_countC), ref _countC); ser.Sync(nameof(_countE), ref _countE); ser.Sync(nameof(_countN), ref _countN); ser.Sync(nameof(_dividerA), ref _dividerA); ser.Sync(nameof(_dividerB), ref _dividerB); ser.Sync(nameof(_dividerC), ref _dividerC); ser.Sync(nameof(_dividerE), ref _dividerE); ser.Sync(nameof(_dividerN), ref _dividerN); ser.SyncEnum(nameof(_currentPanTab), ref _currentPanTab); ser.Sync(nameof(_volume), ref nullDump); for (int i = 0; i < 6; i++) { ser.Sync("volTable" + i, ref _volumeTables[i], false); } if (ser.IsReader) _volume = _machine.Spectrum.Settings.AYVolume; ser.EndSection(); } #endregion } }