438 lines
15 KiB
C#
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
|
|
}
|
|
}
|