362 lines
14 KiB
C#
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
|
|
}
|
|
}
|