/* * PokeySound.cs * * Emulation of the audio features of the Atari Pot Keyboard Integrated Circuit (POKEY, C012294). * * Implementation inspired by prior works of Greg Stanton (ProSystem Emulator) and Ron Fries. * * Copyright © 2012 Mike Murphy * */ using System; namespace EMU7800.Core { public sealed class PokeySound { #region Constants and Tables const int AUDF1 = 0x00, // write reg: channel 1 frequency AUDC1 = 0x01, // write reg: channel 1 generator AUDF2 = 0x02, // write reg: channel 2 frequency AUDC2 = 0x03, // write reg: channel 2 generator AUDF3 = 0x04, // write reg: channel 3 frequency AUDC3 = 0x05, // write reg: channel 3 generator AUDF4 = 0x06, // write reg: channel 4 frequency AUDC4 = 0x07, // write reg: channel 4 generator AUDCTL = 0x08, // write reg: control over audio channels SKCTL = 0x0f, // write reg: control over serial port RANDOM = 0x0a; // read reg: random number generator value const int AUDCTL_POLY9 = 0x80, // make 17-bit poly counter into a 9-bit poly counter AUDCTL_CH1_179 = 0x40, // clocks channel 1 with 1.79 MHz, instead of 64 kHz AUDCTL_CH3_179 = 0x20, // clocks channel 3 with 1.79 MHz, instead of 64 kHz AUDCTL_CH1_CH2 = 0x10, // clock channel 2 with channel 1, instead of 64 kHz (16-bit) AUDCTL_CH3_CH4 = 0x08, // clock channel 4 with channel 3, instead of 64 kHz (16-bit) AUDCTL_CH1_FILTER = 0x04, // inserts high-pass filter into channel 1, clocked by channel 3 AUDCTL_CH2_FILTER = 0x02, // inserts high-pass filter into channel 2, clocked by channel 4 AUDCTL_CLOCK_15 = 0x01; // change normal clock base from 64 kHz to 15 kHz const int AUDC_NOTPOLY5 = 0x80, AUDC_POLY4 = 0x40, AUDC_PURE = 0x20, AUDC_VOLUME_ONLY = 0x10, AUDC_VOLUME_MASK = 0x0f; const int DIV_64 = 28, DIV_15 = 114, POLY9_SIZE = 0x01ff, POLY17_SIZE = 0x0001ffff, POKEY_FREQ = 1787520, SKCTL_RESET = 3; const int CPU_TICKS_PER_AUDIO_SAMPLE = 57; readonly byte[] _poly04 = { 1, 1, 0, 1, 1, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0 }; readonly byte[] _poly05 = { 0, 0, 1, 1, 0, 0, 0, 1, 1, 1, 1, 0, 0, 1, 0, 1, 0, 1, 1, 0, 1, 1, 1, 0, 1, 0, 0, 0, 0, 0, 1 }; readonly byte[] _poly17 = new byte[POLY9_SIZE]; // should be POLY17_SIZE, but instead wrapping around to conserve storage readonly Random _random = new Random(); #endregion #region Object State readonly MachineBase M; readonly int _pokeyTicksPerSample; int _pokeyTicks; ulong _lastUpdateCpuClock; int _bufferIndex; readonly byte[] _audf = new byte[4]; readonly byte[] _audc = new byte[4]; byte _audctl, _skctl; int _baseMultiplier; int _poly04Counter; int _poly05Counter; int _poly17Counter, _poly17Size; readonly int[] _divideMax = new int[4]; readonly int[] _divideCount = new int[4]; readonly byte[] _output = new byte[4]; readonly byte[] _outvol = new byte[4]; #endregion #region Public Members public void Reset() { _poly04Counter = _poly05Counter = _poly17Counter = _audctl = _skctl = 0; _baseMultiplier = DIV_64; _poly17Size = POLY17_SIZE; _pokeyTicks = 0; for (var ch = 0; ch < 4; ch++) { _outvol[ch] = _output[ch] = _audc[ch] = _audf[ch] = 0; _divideCount[ch] = Int32.MaxValue; _divideMax[ch] = Int32.MaxValue; } } public void StartFrame() { _lastUpdateCpuClock = M.CPU.Clock; _bufferIndex = 0; } public void EndFrame() { RenderSamples(M.FrameBuffer.SoundBufferByteLength - _bufferIndex); } public byte Read(ushort addr) { addr &= 0xf; switch (addr) { // If the 2 least significant bits of SKCTL are 0, the random number generator is disabled (return all 1s.) // Ballblazer music relies on this. case RANDOM: return (_skctl & SKCTL_RESET) == 0 ? (byte)0xff : (byte)_random.Next(0xff); default: return 0; } } public void Update(ushort addr, byte data) { if (M.CPU.Clock > _lastUpdateCpuClock) { var updCpuClocks = (int)(M.CPU.Clock - _lastUpdateCpuClock); var samples = updCpuClocks / CPU_TICKS_PER_AUDIO_SAMPLE; RenderSamples(samples); _lastUpdateCpuClock += (ulong)(samples * CPU_TICKS_PER_AUDIO_SAMPLE); } addr &= 0xf; switch (addr) { case AUDF1: _audf[0] = data; ResetChannel1(); if ((_audctl & AUDCTL_CH1_CH2) != 0) ResetChannel2(); break; case AUDC1: _audc[0] = data; ResetChannel1(); break; case AUDF2: _audf[1] = data; ResetChannel2(); break; case AUDC2: _audc[1] = data; ResetChannel2(); break; case AUDF3: _audf[2] = data; ResetChannel3(); if ((_audctl & AUDCTL_CH3_CH4) != 0) ResetChannel4(); break; case AUDC3: _audc[2] = data; ResetChannel3(); break; case AUDF4: _audf[3] = data; ResetChannel4(); break; case AUDC4: _audc[3] = data; ResetChannel4(); break; case AUDCTL: _audctl = data; _poly17Size = ((_audctl & AUDCTL_POLY9) != 0) ? POLY9_SIZE : POLY17_SIZE; _baseMultiplier = ((_audctl & AUDCTL_CLOCK_15) != 0) ? DIV_15 : DIV_64; ResetChannel1(); ResetChannel2(); ResetChannel3(); ResetChannel4(); break; case SKCTL: _skctl = data; break; } } #endregion #region Constructors private PokeySound() { _random.NextBytes(_poly17); for (var i = 0; i < _poly17.Length; i++) _poly17[i] &= 0x01; Reset(); } public PokeySound(MachineBase m) : this() { if (m == null) throw new ArgumentNullException("m"); M = m; // Add 8-bits of fractional representation to reduce distortion on output _pokeyTicksPerSample = (POKEY_FREQ << 8) / M.SoundSampleFrequency; } #endregion #region Serialization Members public PokeySound(DeserializationContext input, MachineBase m) : this(m) { if (input == null) throw new ArgumentNullException("input"); input.CheckVersion(1); _lastUpdateCpuClock = input.ReadUInt64(); _bufferIndex = input.ReadInt32(); _audf = input.ReadBytes(); _audc = input.ReadBytes(); _audctl = input.ReadByte(); _skctl = input.ReadByte(); _output = input.ReadBytes(); _outvol = input.ReadBytes(); _divideMax = input.ReadIntegers(4); _divideCount = input.ReadIntegers(4); _pokeyTicks = input.ReadInt32(); _pokeyTicksPerSample = input.ReadInt32(); _baseMultiplier = input.ReadInt32(); _poly04Counter = input.ReadInt32(); _poly05Counter = input.ReadInt32(); _poly17Counter = input.ReadInt32(); _poly17Size = input.ReadInt32(); } public void GetObjectData(SerializationContext output) { if (output == null) throw new ArgumentNullException("output"); output.WriteVersion(1); output.Write(_lastUpdateCpuClock); output.Write(_bufferIndex); output.Write(_audf); output.Write(_audc); output.Write(_audctl); output.Write(_skctl); output.Write(_output); output.Write(_outvol); output.Write(_divideMax); output.Write(_divideCount); output.Write(_pokeyTicks); output.Write(_pokeyTicksPerSample); output.Write(_baseMultiplier); output.Write(_poly04Counter); output.Write(_poly05Counter); output.Write(_poly17Counter); output.Write(_poly17Size); } #endregion #region Helpers void RenderSamples(int count) { const int POKEY_SAMPLE = 4; var poly17Length = (_poly17Size > _poly17.Length ? _poly17.Length : _poly17Size); while (count > 0 && _bufferIndex < M.FrameBuffer.SoundBufferByteLength) { var nextEvent = POKEY_SAMPLE; var wholeTicksToConsume = (_pokeyTicks >> 8); for (var ch = 0; ch < 4; ch++) { if (_divideCount[ch] <= wholeTicksToConsume) { wholeTicksToConsume = _divideCount[ch]; nextEvent = ch; } } for (var ch = 0; ch < 4; ch++) _divideCount[ch] -= wholeTicksToConsume; _pokeyTicks -= (wholeTicksToConsume << 8); if (nextEvent == POKEY_SAMPLE) { _pokeyTicks += _pokeyTicksPerSample; byte sample = 0; for (var ch = 0; ch < 4; ch++) sample += _outvol[ch]; M.FrameBuffer.SoundBuffer[_bufferIndex++] += sample; count--; continue; } _divideCount[nextEvent] += _divideMax[nextEvent]; _poly04Counter += wholeTicksToConsume; _poly04Counter %= _poly04.Length; _poly05Counter += wholeTicksToConsume; _poly05Counter %= _poly05.Length; _poly17Counter += wholeTicksToConsume; _poly17Counter %= poly17Length; if ((_audc[nextEvent] & AUDC_NOTPOLY5) != 0 || _poly05[_poly05Counter] != 0) { if ((_audc[nextEvent] & AUDC_PURE) != 0) _output[nextEvent] ^= 1; else if ((_audc[nextEvent] & AUDC_POLY4) != 0) _output[nextEvent] = _poly04[_poly04Counter]; else _output[nextEvent] = _poly17[_poly17Counter]; } _outvol[nextEvent] = (_output[nextEvent] != 0) ? (byte)(_audc[nextEvent] & AUDC_VOLUME_MASK) : (byte)0; } } // As defined in the manual, the exact divider values are different depending on the frequency and resolution: // 64 kHz or 15 kHz AUDF + 1 // 1 MHz, 8-bit AUDF + 4 // 1 MHz, 16-bit AUDF[CHAN1] + 256 * AUDF[CHAN2] + 7 void ResetChannel1() { var val = ((_audctl & AUDCTL_CH1_179) != 0) ? (_audf[0] + 4) : ((_audf[0] + 1) * _baseMultiplier); if (val != _divideMax[0]) { _divideMax[0] = val; if (val < _divideCount[0]) _divideCount[0] = val; } UpdateVolumeSettingsForChannel(0); } void ResetChannel2() { int val; if ((_audctl & AUDCTL_CH1_CH2) != 0) { val = ((_audctl & AUDCTL_CH1_179) != 0) ? (_audf[1] * 256 + _audf[0] + 7) : ((_audf[1] * 256 + _audf[0] + 1) * _baseMultiplier); } else { val = ((_audf[1] + 1) * _baseMultiplier); } if (val != _divideMax[1]) { _divideMax[1] = val; if (val < _divideCount[1]) _divideCount[1] = val; } UpdateVolumeSettingsForChannel(1); } void ResetChannel3() { var val = ((_audctl & AUDCTL_CH3_179) != 0) ? (_audf[2] + 4) : ((_audf[2] + 1) * _baseMultiplier); if (val != _divideMax[2]) { _divideMax[2] = val; if (val < _divideCount[2]) _divideCount[2] = val; } UpdateVolumeSettingsForChannel(2); } void ResetChannel4() { int val; if ((_audctl & AUDCTL_CH3_CH4) != 0) { val = ((_audctl & AUDCTL_CH3_179) != 0) ? (_audf[3] * 256 + _audf[2] + 7) : ((_audf[3] * 256 + _audf[2] + 1) * _baseMultiplier); } else { val = ((_audf[3] + 1) * _baseMultiplier); } if (val != _divideMax[3]) { _divideMax[3] = val; if (val < _divideCount[3]) _divideCount[3] = val; } UpdateVolumeSettingsForChannel(3); } void UpdateVolumeSettingsForChannel(int ch) { if (((_audc[ch] & AUDC_VOLUME_ONLY) != 0) || ((_audc[ch] & AUDC_VOLUME_MASK) == 0) || (_divideMax[ch] < (_pokeyTicksPerSample >> 8))) { _outvol[ch] = (byte)(_audc[ch] & AUDC_VOLUME_MASK); _divideCount[ch] = Int32.MaxValue; _divideMax[ch] = Int32.MaxValue; } } [System.Diagnostics.Conditional("DEBUG")] void LogDebug(string format, params object[] args) { if (M == null || M.Logger == null) return; M.Logger.WriteLine(format, args); } #endregion } }