diff --git a/BizHawk.Emulation.Cores/Consoles/Atari/A7800Hawk/TIA_Sound/TIA.cs b/BizHawk.Emulation.Cores/Consoles/Atari/A7800Hawk/TIA_Sound/TIA.cs
new file mode 100644
index 0000000000..4c6ef5f20d
--- /dev/null
+++ b/BizHawk.Emulation.Cores/Consoles/Atari/A7800Hawk/TIA_Sound/TIA.cs
@@ -0,0 +1,334 @@
+using System;
+using System.Numerics;
+
+using BizHawk.Emulation.Common;
+
+namespace BizHawk.Emulation.Cores.Atari.A7800Hawk
+{
+ // Emulates the TIA
+ public partial class TIA : ISoundProvider
+ {
+ public byte BusState;
+
+ private bool _doTicks;
+
+ private byte _hsyncCnt;
+ private int _capChargeStart;
+ private bool _capCharging;
+ public int AudioClocks; // not savestated
+
+ private readonly Audio[] AUD = { new Audio(), new Audio() };
+
+ // current audio register state used to sample correct positions in the scanline (clrclk 0 and 114)
+ ////public byte[] current_audio_register = new byte[6];
+ public readonly short[] LocalAudioCycles = new short[2000];
+
+ public void Reset()
+ {
+ _hsyncCnt = 0;
+ _capChargeStart = 0;
+ _capCharging = false;
+ AudioClocks = 0;
+
+ _doTicks = false;
+ }
+
+ // Execute TIA cycles
+ public void Execute(int cycles)
+ {
+ // do the audio sampling
+ if (_hsyncCnt == 36 || _hsyncCnt == 148)
+ {
+ LocalAudioCycles[AudioClocks] += (short)(AUD[0].Cycle() / 2);
+ LocalAudioCycles[AudioClocks] += (short)(AUD[1].Cycle() / 2);
+ AudioClocks++;
+ }
+
+ }
+
+ public byte ReadMemory(ushort addr, bool peek)
+ {
+ var maskedAddr = (ushort)(addr & 0x000F);
+ byte coll = 0;
+ int mask = 0;
+
+ if (maskedAddr == 0x00) // CXM0P
+ {
+
+ }
+
+ if (maskedAddr == 0x01) // CXM1P
+ {
+
+ }
+
+ if (maskedAddr == 0x02) // CXP0FB
+ {
+
+ }
+
+ if (maskedAddr == 0x03) // CXP1FB
+ {
+
+ }
+
+ if (maskedAddr == 0x04) // CXM0FB
+ {
+
+ }
+
+ if (maskedAddr == 0x05) // CXM1FB
+ {
+
+ }
+
+ if (maskedAddr == 0x06) // CXBLPF
+ {
+
+ }
+
+ if (maskedAddr == 0x07) // CXPPMM
+ {
+
+ }
+
+ // inputs 0-3 are measured by a charging capacitor, these inputs are used with the paddles and the keyboard
+ // Changing the hard coded value will change the paddle position. The range seems to be roughly 0-56000 according to values from stella
+ // 6105 roughly centers the paddle in Breakout
+ if (maskedAddr == 0x08) // INPT0
+ {
+
+ }
+
+ if (maskedAddr == 0x09) // INPT1
+ {
+
+ }
+
+ if (maskedAddr == 0x0A) // INPT2
+ {
+
+ }
+
+ if (maskedAddr == 0x0B) // INPT3
+ {
+
+ }
+
+ if (maskedAddr == 0x0C) // INPT4
+ {
+
+ }
+
+ if (maskedAddr == 0x0D) // INPT5
+ {
+
+ }
+
+ // some bits of the databus will be undriven when a read call is made. Our goal here is to sort out what
+ // happens to the undriven pins. Most of the time, they will be in whatever state they were when previously
+ // assigned in some other bus access, so let's go with that.
+ coll += (byte)(mask & BusState);
+
+ if (!peek)
+ {
+ BusState = coll;
+ }
+
+ return coll;
+ }
+
+ public void WriteMemory(ushort addr, byte value, bool poke)
+ {
+ var maskedAddr = (ushort)(addr & 0x3f);
+ if (!poke)
+ {
+ BusState = value;
+ }
+
+ if (maskedAddr == 0x00) // VSYNC
+ {
+
+ }
+ else if (maskedAddr == 0x01) // VBLANK
+ {
+
+ }
+ else if (maskedAddr == 0x02) // WSYNC
+ {
+
+ }
+ else if (maskedAddr == 0x04) // NUSIZ0
+ {
+
+ }
+ else if (maskedAddr == 0x05) // NUSIZ1
+ {
+
+ }
+ else if (maskedAddr == 0x06) // COLUP0
+ {
+
+ }
+ else if (maskedAddr == 0x07) // COLUP1
+ {
+
+ }
+ else if (maskedAddr == 0x08) // COLUPF
+ {
+
+ }
+ else if (maskedAddr == 0x09) // COLUBK
+ {
+
+ }
+ else if (maskedAddr == 0x0A) // CTRLPF
+ {
+
+ }
+ else if (maskedAddr == 0x0B) // REFP0
+ {
+
+ }
+ else if (maskedAddr == 0x0C) // REFP1
+ {
+
+ }
+ else if (maskedAddr == 0x0D) // PF0
+ {
+
+ }
+ else if (maskedAddr == 0x0E) // PF1
+ {
+
+ }
+ else if (maskedAddr == 0x0F) // PF2
+ {
+
+ }
+ else if (maskedAddr == 0x10) // RESP0
+ {
+
+ }
+ else if (maskedAddr == 0x11) // RESP1
+ {
+
+ }
+ else if (maskedAddr == 0x12) // RESM0
+ {
+
+ }
+ else if (maskedAddr == 0x13) // RESM1
+ {
+
+ }
+ else if (maskedAddr == 0x14) // RESBL
+ {
+
+ }
+ else if (maskedAddr == 0x15) // AUDC0
+ {
+ AUD[0].AUDC = (byte)(value & 15);
+ }
+ else if (maskedAddr == 0x16) // AUDC1
+ {
+ AUD[1].AUDC = (byte)(value & 15);
+ }
+ else if (maskedAddr == 0x17) // AUDF0
+ {
+ AUD[0].AUDF = (byte)((value & 31) + 1);
+ }
+ else if (maskedAddr == 0x18) // AUDF1
+ {
+ AUD[1].AUDF = (byte)((value & 31) + 1);
+ }
+ else if (maskedAddr == 0x19) // AUDV0
+ {
+ AUD[0].AUDV = (byte)(value & 15);
+ }
+ else if (maskedAddr == 0x1A) // AUDV1
+ {
+ AUD[1].AUDV = (byte)(value & 15);
+ }
+ else if (maskedAddr == 0x1B) // GRP0
+ {
+
+ }
+ else if (maskedAddr == 0x1C) // GRP1
+ {
+
+ }
+ else if (maskedAddr == 0x1D) // ENAM0
+ {
+
+ }
+ else if (maskedAddr == 0x1E) // ENAM1
+ {
+
+ }
+ else if (maskedAddr == 0x1F) // ENABL
+ {
+
+ }
+ else if (maskedAddr == 0x20) // HMP0
+ {
+
+ }
+ else if (maskedAddr == 0x21) // HMP1
+ {
+
+ }
+ else if (maskedAddr == 0x22) // HMM0
+ {
+
+ }
+ else if (maskedAddr == 0x23) // HMM1
+ {
+
+ }
+ else if (maskedAddr == 0x24) // HMBL
+ {
+
+ }
+ else if (maskedAddr == 0x25) // VDELP0
+ {
+
+ }
+ else if (maskedAddr == 0x26) // VDELP1
+ {
+
+ }
+ else if (maskedAddr == 0x27) // VDELBL
+ {
+
+ }
+ else if (maskedAddr == 0x28) // RESMP0
+ {
+
+ }
+ else if (maskedAddr == 0x29) // RESMP1
+ {
+
+ }
+ else if (maskedAddr == 0x2A) // HMOVE
+ {
+
+ }
+ else if (maskedAddr == 0x2B) // HMCLR
+ {
+
+ }
+ else if (maskedAddr == 0x2C) // CXCLR
+ {
+
+ }
+ }
+
+ private enum AudioRegister : byte
+ {
+ AUDC, AUDF, AUDV
+ }
+
+ private int _frameStartCycles, _frameEndCycles;
+ }
+}
\ No newline at end of file
diff --git a/BizHawk.Emulation.Cores/Consoles/Atari/A7800Hawk/TIA_Sound/Tia.Audio.cs b/BizHawk.Emulation.Cores/Consoles/Atari/A7800Hawk/TIA_Sound/Tia.Audio.cs
new file mode 100644
index 0000000000..cf5aa3e0c7
--- /dev/null
+++ b/BizHawk.Emulation.Cores/Consoles/Atari/A7800Hawk/TIA_Sound/Tia.Audio.cs
@@ -0,0 +1,212 @@
+using System;
+using BizHawk.Common;
+
+namespace BizHawk.Emulation.Cores.Atari.A7800Hawk
+{
+ public partial class TIA
+ {
+ public class Audio
+ {
+ // noise/division control
+ public byte AUDC = 0;
+
+ // frequency divider
+ public byte AUDF = 1;
+
+ // volume
+ public byte AUDV = 0;
+
+ // 2 state counter
+ private bool sr1 = true;
+
+ // 4 bit shift register
+ private int sr4 = 0x0f;
+
+ // 5 bit shift register
+ private int sr5 = 0x1f;
+
+ // 9 bit shift register
+ private int sr9 = 0x1ff;
+
+ // 3 state counter
+ private int sr3 = 2;
+
+ // counter based off AUDF
+ private byte freqcnt;
+
+ // latched audio value
+ private bool on = true;
+
+ private bool Run3()
+ {
+ sr3++;
+ if (sr3 == 3)
+ {
+ sr3 = 0;
+ return true;
+ }
+
+ return false;
+ }
+
+ private bool Run4()
+ {
+ bool ret = (sr4 & 1) != 0;
+ bool c = ((sr4 & 1) != 0) ^ ((sr4 & 2) != 0);
+ sr4 = (sr4 >> 1) | (c ? 8 : 0);
+ return ret;
+ }
+
+ private bool Run5()
+ {
+ bool ret = (sr5 & 1) != 0;
+ bool c = ((sr5 & 1) != 0) ^ ((sr5 & 4) != 0);
+ sr5 = (sr5 >> 1) | (c ? 16 : 0);
+ return ret;
+ }
+
+ private bool One4()
+ {
+ bool ret = (sr4 & 1) != 0;
+ sr4 = (sr4 >> 1) | 8;
+ return ret;
+ }
+
+ private bool One5()
+ {
+ bool ret = (sr5 & 1) != 0;
+ sr5 = (sr5 >> 1) | 16;
+ return ret;
+ }
+
+ private bool Run1()
+ {
+ sr1 = !sr1;
+ return !sr1;
+ }
+
+ private bool Run9()
+ {
+ bool ret = (sr9 & 1) != 0;
+ bool c = ((sr9 & 1) != 0) ^ ((sr9 & 16) != 0);
+ sr9 = (sr9 >> 1) | (c ? 256 : 0);
+ return ret;
+ }
+
+ ///
+ /// call me approx 31k times a second
+ ///
+ /// 16 bit audio sample
+ public short Cycle()
+ {
+ if (++freqcnt >= AUDF)
+ {
+ freqcnt = 0;
+ switch (AUDC)
+ {
+ case 0x00:
+ case 0x0b:
+ // Both have a 1 s
+ One5();
+ on = One4();
+ break;
+
+ case 0x01:
+ // Both run, but the 5 bit is ignored
+ on = Run4();
+ //Run5();
+ break;
+ case 0x02:
+ if ((sr5 & 0x0f) == 0 || (sr5 & 0x0f) == 0x0f)
+ {
+ on = Run4();
+ }
+
+ Run5();
+ break;
+ case 0x03:
+ if (Run5())
+ {
+ on = Run4();
+ }
+
+ break;
+
+ case 0x04:
+ Run5();
+ One4();
+ on = Run1();
+ break;
+
+ case 0x05:
+ One5();
+ Run4();
+ on = Run1();
+ break;
+
+ case 0x06:
+ case 0x0a:
+ Run5();
+ if ((sr5 & 0x0f) == 0)
+ {
+ on = false;
+ }
+ else if ((sr5 & 0x0f) == 0x0f)
+ {
+ on = true;
+ }
+
+ break;
+
+ case 0x07:
+ case 0x09:
+ on = Run5();
+ break;
+
+ case 0x08:
+ on = Run9();
+ break;
+ case 0x0c:
+ case 0x0d:
+ if (Run3())
+ {
+ on = Run1();
+ }
+
+ break;
+ case 0x0e:
+ if (Run3())
+ {
+ goto case 0x06;
+ }
+
+ break;
+ case 0x0f:
+ if (Run3())
+ {
+ goto case 0x07;
+ }
+
+ break;
+ }
+ }
+
+ return (short)(on ? AUDV * 1092 : 0);
+ }
+
+ public void SyncState(Serializer ser)
+ {
+ ser.Sync("AUDC", ref AUDC);
+ ser.Sync("AUDF", ref AUDF);
+ ser.Sync("AUDV", ref AUDV);
+ ser.Sync("sr1", ref sr1);
+ ser.Sync("sr3", ref sr3);
+ ser.Sync("sr4", ref sr4);
+ ser.Sync("sr5", ref sr5);
+ ser.Sync("sr9", ref sr9);
+ ser.Sync("freqcnt", ref freqcnt);
+ ser.Sync("on", ref on);
+ }
+ }
+ }
+}
diff --git a/BizHawk.Emulation.Cores/Consoles/Atari/A7800Hawk/TIA_Sound/Tia.ISoundProvider.cs b/BizHawk.Emulation.Cores/Consoles/Atari/A7800Hawk/TIA_Sound/Tia.ISoundProvider.cs
new file mode 100644
index 0000000000..3a85b7bbd0
--- /dev/null
+++ b/BizHawk.Emulation.Cores/Consoles/Atari/A7800Hawk/TIA_Sound/Tia.ISoundProvider.cs
@@ -0,0 +1,65 @@
+using System;
+using BizHawk.Emulation.Common;
+
+namespace BizHawk.Emulation.Cores.Atari.A7800Hawk
+{
+ public partial class TIA : ISoundProvider
+ {
+ public bool CanProvideAsync => false;
+
+ public void SetSyncMode(SyncSoundMode mode)
+ {
+ if (mode != SyncSoundMode.Sync)
+ {
+ throw new InvalidOperationException("Only Sync mode is supported.");
+ }
+ }
+
+ public SyncSoundMode SyncMode => SyncSoundMode.Sync;
+
+ public void GetSamplesSync(out short[] samples, out int nsamp)
+ {
+ short[] ret = new short[_spf * 2];
+ GetSamples(ret);
+ samples = ret;
+ nsamp = _spf;
+ }
+
+ public void GetSamplesAsync(short[] samples)
+ {
+ throw new NotSupportedException("Async is not available");
+ }
+
+ public void DiscardSamples()
+ {
+ AudioClocks = 0;
+ }
+
+ private readonly int _spf;
+
+ // Exposing this as GetSamplesAsync would allow this to provide async sound
+ // However, it does nothing special for async sound so I don't see a point
+ private void GetSamples(short[] samples)
+ {
+ if (AudioClocks > 0)
+ {
+ var samples31Khz = new short[AudioClocks]; // mono
+
+ for (int i = 0; i < AudioClocks; i++)
+ {
+ samples31Khz[i] = LocalAudioCycles[i];
+ LocalAudioCycles[i] = 0;
+ }
+
+ // convert from 31khz to 44khz
+ for (var i = 0; i < samples.Length / 2; i++)
+ {
+ samples[i * 2] = samples31Khz[(int)(((double)samples31Khz.Length / (double)(samples.Length / 2)) * i)];
+ samples[(i * 2) + 1] = samples[i * 2];
+ }
+ }
+
+ AudioClocks = 0;
+ }
+ }
+}
diff --git a/BizHawk.Emulation.Cores/Consoles/Atari/A7800Hawk/TIA_Sound/Tia.SyncState.cs b/BizHawk.Emulation.Cores/Consoles/Atari/A7800Hawk/TIA_Sound/Tia.SyncState.cs
new file mode 100644
index 0000000000..100bdf5361
--- /dev/null
+++ b/BizHawk.Emulation.Cores/Consoles/Atari/A7800Hawk/TIA_Sound/Tia.SyncState.cs
@@ -0,0 +1,28 @@
+using BizHawk.Common;
+
+namespace BizHawk.Emulation.Cores.Atari.A7800Hawk
+{
+ public partial class TIA
+ {
+ public void SyncState(Serializer ser)
+ {
+ ser.BeginSection("TIA");
+
+ ser.Sync("hsyncCnt", ref _hsyncCnt);
+
+ // add everything to the state
+ ser.Sync("Bus_State", ref BusState);
+
+ ser.Sync("Ticks", ref _doTicks);
+
+ // some of these things weren't in the state because they weren't needed if
+ // states were always taken at frame boundaries
+ ser.Sync("capChargeStart", ref _capChargeStart);
+ ser.Sync("capCharging", ref _capCharging);
+ ser.Sync("AudioClocks", ref AudioClocks);
+ ser.Sync("FrameStartCycles", ref _frameStartCycles);
+ ser.Sync("FrameEndCycles", ref _frameEndCycles);
+
+ }
+ }
+}