BizHawk/EMU7800/Core/PokeySound.cs

438 lines
15 KiB
C#

/*
* 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
}
}