BizHawk/EMU7800/Core/TIASound.cs

362 lines
14 KiB
C#

/*
* TIASound.cs
*
* Sound emulation for the 2600. Based upon TIASound © 1997 by Ron Fries.
*
* Copyright © 2003, 2004 Mike Murphy
*
*/
/*****************************************************************************/
/* */
/* License Information and Copyright Notice */
/* ======================================== */
/* */
/* TiaSound is Copyright(c) 1997 by Ron Fries */
/* */
/* This library is free software; you can redistribute it and/or modify it */
/* under the terms of version 2 of the GNU Library General Public License */
/* as published by the Free Software Foundation. */
/* */
/* This library is distributed in the hope that it will be useful, but */
/* WITHOUT ANY WARRANTY; without even the implied warranty of */
/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library */
/* General Public License for more details. */
/* To obtain a copy of the GNU Library General Public License, write to the */
/* Free Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. */
/* */
/* Any permitted reproduction of these routines, in whole or in part, must */
/* bear this legend. */
/* */
/*****************************************************************************/
using System;
namespace EMU7800.Core
{
public sealed class TIASound
{
#region Constants and Tables
// Clock Source Clock Modifier Source Pattern
const int
SET_TO_1 = 0x00, // 0 0 0 0 3.58 Mhz/114 none (pure) none
//POLY4 = 0x01, // 0 0 0 1 3.58 Mhz/114 none (pure) 4-bit poly
//DIV31_POLY4 = 0x02, // 0 0 1 0 3.58 Mhz/114 divide by 31 4-bit poly
//POLY5_POLY4 = 0x03, // 0 0 1 1 3.58 Mhz/114 5-bit poly 4-bit poly
//PURE = 0x04, // 0 1 0 0 3.58 Mhz/114 none (pure) pure (~Q)
//PURE2 = 0x05, // 0 1 0 1 3.58 Mhz/114 none (pure) pure (~Q)
//DIV31_PURE = 0x06, // 0 1 1 0 3.58 Mhz/114 divide by 31 pure (~Q)
//POLY5_2 = 0x07, // 0 1 1 1 3.58 Mhz/114 5-bit poly pure (~Q)
POLY9 = 0x08; // 1 0 0 0 3.58 Mhz/114 none (pure) 9-bit poly
//POLY5 = 0x09, // 1 0 0 1 3.58 Mhz/114 none (pure) 5-bit poly
//DIV31_POLY5 = 0x0a, // 1 0 1 0 3.58 Mhz/114 divide by 31 5-bit poly
//POLY5_POLY5 = 0x0b, // 1 0 1 1 3.58 Mhz/114 5-bit poly 5-bit poly
//DIV3_PURE = 0x0c, // 1 1 0 0 1.19 Mhz/114 none (pure) pure (~Q)
//DIV3_PURE2 = 0x0d, // 1 1 0 1 1.19 Mhz/114 none (pure) pure (~Q)
//DIV93_PURE = 0x0e, // 1 1 1 0 1.19 Mhz/114 divide by 31 pure (~Q)
//DIV3_POLY5 = 0x0f; // 1 1 1 1 1.19 Mhz/114 5-bit poly pure (~Q)
const int
AUDC0 = 0x15, // audio control 0 (D3-0)
AUDC1 = 0x16, // audio control 1 (D4-0)
AUDF0 = 0x17, // audio frequency 0 (D4-0)
AUDF1 = 0x18, // audio frequency 1 (D3-0)
AUDV0 = 0x19, // audio volume 0 (D3-0)
AUDV1 = 0x1a; // audio volume 1 (D3-0)
// The 4bit and 5bit patterns are the identical ones used in the tia chip.
readonly byte[] Bit4 = new byte[] { 1, 1, 0, 1, 1, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0 }; // 2^4 - 1 = 15
readonly byte[] Bit5 = new byte[] { 0, 0, 1, 0, 1, 1, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 0, 1, 1, 1, 0, 1, 0, 1, 0, 0, 0, 0, 1 }; // 2^5 - 1 = 31
// [Ron] treated the 'Div by 31' counter as another polynomial because of
// the way it operates. It does not have a 50% duty cycle, but instead
// has a 13:18 ratio (of course, 13+18 = 31). This could also be
// implemented by using counters.
readonly byte[] Div31 = new byte[] { 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };
// Rather than have a table with 511 entries, I use a random number
readonly byte[] Bit9 = new byte[511]; // 2^9 - 1 = 511
readonly int[] P4 = new int[2]; // Position counter for the 4-bit POLY array
readonly int[] P5 = new int[2]; // Position counter for the 5-bit POLY array
readonly int[] P9 = new int[2]; // Position counter for the 9-bit POLY array
readonly int[] DivByNCounter = new int[2]; // Divide by n counter, one for each channel
readonly int[] DivByNMaximum = new int[2]; // Divide by n maximum, one for each channel
readonly int _cpuClocksPerSample;
#endregion
#region Object State
readonly MachineBase M;
// The TIA Sound registers
readonly byte[] AUDC = new byte[2];
readonly byte[] AUDF = new byte[2];
readonly byte[] AUDV = new byte[2];
// The last output volume for each channel
readonly byte[] OutputVol = new byte[2];
// Used to determine how much sound to render
ulong LastUpdateCPUClock;
int BufferIndex;
#endregion
#region Public Members
public void Reset()
{
for (var chan = 0; chan < 2; chan++)
{
OutputVol[chan] = 0;
DivByNCounter[chan] = 0;
DivByNMaximum[chan] = 0;
AUDC[chan] = 0;
AUDF[chan] = 0;
AUDV[chan] = 0;
P4[chan] = 0;
P5[chan] = 0;
P9[chan] = 0;
}
}
public void StartFrame()
{
LastUpdateCPUClock = M.CPU.Clock;
BufferIndex = 0;
}
public void EndFrame()
{
RenderSamples(M.FrameBuffer.SoundBufferByteLength - BufferIndex);
}
public void Update(ushort addr, byte data)
{
if (M.CPU.Clock > LastUpdateCPUClock)
{
var updCPUClocks = (int)(M.CPU.Clock - LastUpdateCPUClock);
var samples = updCPUClocks / _cpuClocksPerSample;
RenderSamples(samples);
LastUpdateCPUClock += (ulong)(samples * _cpuClocksPerSample);
}
byte chan;
switch (addr)
{
case AUDC0:
AUDC[0] = (byte)(data & 0x0f);
chan = 0;
break;
case AUDC1:
AUDC[1] = (byte)(data & 0x0f);
chan = 1;
break;
case AUDF0:
AUDF[0] = (byte)(data & 0x1f);
chan = 0;
break;
case AUDF1:
AUDF[1] = (byte)(data & 0x1f);
chan = 1;
break;
case AUDV0:
AUDV[0] = (byte)(data & 0x0f);
chan = 0;
break;
case AUDV1:
AUDV[1] = (byte)(data & 0x0f);
chan = 1;
break;
default:
return;
}
byte new_divn_max;
if (AUDC[chan] == SET_TO_1)
{
// indicate the clock is zero so no process will occur
new_divn_max = 0;
// and set the output to the selected volume
OutputVol[chan] = AUDV[chan];
}
else
{
// otherwise calculate the 'divide by N' value
new_divn_max = (byte)(AUDF[chan] + 1);
// if bits D2 & D3 are set, then multiply the 'div by n' count by 3
if ((AUDC[chan] & 0x0c) == 0x0c)
{
new_divn_max *= 3;
}
}
// only reset those channels that have changed
if (new_divn_max != DivByNMaximum[chan])
{
DivByNMaximum[chan] = new_divn_max;
// if the channel is now volume only or was volume only...
if (DivByNCounter[chan] == 0 || new_divn_max == 0)
{
// reset the counter (otherwise let it complete the previous)
DivByNCounter[chan] = new_divn_max;
}
}
}
#endregion
#region Constructors
private TIASound()
{
var r = new Random();
r.NextBytes(Bit9);
for (var i = 0; i < Bit9.Length; i++)
{
Bit9[i] &= 0x01;
}
}
public TIASound(MachineBase m, int cpuClocksPerSample) : this()
{
if (m == null)
throw new ArgumentNullException("m");
if (cpuClocksPerSample <= 0)
throw new ArgumentException("cpuClocksPerSample must be positive.");
M = m;
_cpuClocksPerSample = cpuClocksPerSample;
}
#endregion
#region Serialization Members
public TIASound(DeserializationContext input, MachineBase m, int cpuClocksPerSample) : this(m, cpuClocksPerSample)
{
if (input == null)
throw new ArgumentNullException("input");
input.CheckVersion(1);
Bit9 = input.ReadExpectedBytes(511);
P4 = input.ReadIntegers(2);
P5 = input.ReadIntegers(2);
P9 = input.ReadIntegers(2);
DivByNCounter = input.ReadIntegers(2);
DivByNMaximum = input.ReadIntegers(2);
AUDC = input.ReadExpectedBytes(2);
AUDF = input.ReadExpectedBytes(2);
AUDV = input.ReadExpectedBytes(2);
OutputVol = input.ReadExpectedBytes(2);
LastUpdateCPUClock = input.ReadUInt64();
BufferIndex = input.ReadInt32();
}
public void GetObjectData(SerializationContext output)
{
if (output == null)
throw new ArgumentNullException("output");
output.WriteVersion(1);
output.Write(Bit9);
output.Write(P4);
output.Write(P5);
output.Write(P9);
output.Write(DivByNCounter);
output.Write(DivByNMaximum);
output.Write(AUDC);
output.Write(AUDF);
output.Write(AUDV);
output.Write(OutputVol);
output.Write(LastUpdateCPUClock);
output.Write(BufferIndex);
}
#endregion
#region Helpers
void RenderSamples(int count)
{
for (; BufferIndex < M.FrameBuffer.SoundBufferByteLength && count-- > 0; BufferIndex++)
{
if (DivByNCounter[0] > 1)
{
DivByNCounter[0]--;
}
else if (DivByNCounter[0] == 1)
{
DivByNCounter[0] = DivByNMaximum[0];
ProcessChannel(0);
}
if (DivByNCounter[1] > 1)
{
DivByNCounter[1]--;
}
else if (DivByNCounter[1] == 1)
{
DivByNCounter[1] = DivByNMaximum[1];
ProcessChannel(1);
}
M.FrameBuffer.SoundBuffer[BufferIndex] += (byte)(OutputVol[0] + OutputVol[1]);
}
}
void ProcessChannel(int chan)
{
// the P5 counter has multiple uses, so we inc it here
if (++P5[chan] >= 31)
{ // POLY5 size: 2^5 - 1 = 31
P5[chan] = 0;
}
// check clock modifier for clock tick
if ((AUDC[chan] & 0x02) == 0 ||
((AUDC[chan] & 0x01) == 0 && Div31[P5[chan]] == 1) ||
((AUDC[chan] & 0x01) == 1 && Bit5[P5[chan]] == 1))
{
if ((AUDC[chan] & 0x04) != 0)
{ // pure modified clock selected
OutputVol[chan] = (OutputVol[chan] != 0) ? (byte)0 : AUDV[chan];
}
else if ((AUDC[chan] & 0x08) != 0)
{ // check for poly5/poly9
if (AUDC[chan] == POLY9)
{ // check for poly9
if (++P9[chan] >= 511)
{ // poly9 size: 2^9 - 1 = 511
P9[chan] = 0;
}
OutputVol[chan] = (Bit9[P9[chan]] == 1) ? AUDV[chan] : (byte)0;
}
else
{ // must be poly5
OutputVol[chan] = (Bit5[P5[chan]] == 1) ? AUDV[chan] : (byte)0;
}
}
else
{ // poly4 is the only remaining possibility
if (++P4[chan] >= 15)
{ // POLY4 size: 2^4 - 1 = 15
P4[chan] = 0;
}
OutputVol[chan] = (Bit4[P4[chan]] == 1) ? AUDV[chan] : (byte)0;
}
}
}
#endregion
}
}