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