using System; using System.Collections.Generic; using System.Globalization; using System.IO; // Emulates a Texas Instruments SN76489 // TODO the freq->note translation should be moved to a separate utility class. namespace BizHawk.Emulation.Sound { public sealed class SN76489 : ISoundProvider { public sealed class Channel { public ushort Frequency; public byte Volume; public short[] Wave; public bool Noise; public byte NoiseType; public float WaveOffset; public bool Left = true; public bool Right = true; private const int SampleRate = 44100; private static byte[] LogScale = { 0, 10, 13, 16, 20, 26, 32, 40, 51, 64, 81, 102, 128, 161, 203, 255 }; public void Mix(short[] samples, int start, int len) { if (Volume == 0) return; float adjustedWaveLengthInSamples = SampleRate / (Noise ? (Frequency / (float)Wave.Length) : Frequency); float moveThroughWaveRate = Wave.Length / adjustedWaveLengthInSamples; int end = start + len; for (int i = start; i < end; ) { short value = Wave[(int)WaveOffset]; samples[i++] += (short)(Left ? (value / 4 * LogScale[Volume] / 0x1FF) : 0); samples[i++] += (short)(Right ? (value / 4 * LogScale[Volume] / 0x1FF) : 0); WaveOffset += moveThroughWaveRate; if (WaveOffset >= Wave.Length) WaveOffset %= Wave.Length; } } } public Channel[] Channels = new Channel[4]; public byte PsgLatch; private Queue commands = new Queue(256); private int frameStartTime, frameStopTime; private const int PsgBase = 111861; public SN76489() { Waves.InitWaves(); for (int i=0; i<4; i++) { Channels[i] = new Channel(); switch (i) { case 0: case 1: case 2: Channels[i].Wave = Waves.ImperfectSquareWave; break; case 3: Channels[i].Wave = Waves.NoiseWave; Channels[i].Noise = true; break; } } } public void Reset() { PsgLatch = 0; foreach (var channel in Channels) { channel.Frequency = 0; channel.Volume = 0; channel.NoiseType = 0; channel.WaveOffset = 0f; } } public void BeginFrame(int cycles) { while (commands.Count > 0) { var cmd = commands.Dequeue(); WritePsgDataImmediate(cmd.Value); } frameStartTime = cycles; } public void EndFrame(int cycles) { frameStopTime = cycles; } public void WritePsgData(byte value, int cycles) { commands.Enqueue(new QueuedCommand {Value = value, Time = cycles-frameStartTime}); } private void UpdateNoiseType(int value) { Channels[3].NoiseType = (byte)(value & 0x07); switch (Channels[3].NoiseType & 3) { case 0: Channels[3].Frequency = PsgBase / 16; break; case 1: Channels[3].Frequency = PsgBase / 32; break; case 2: Channels[3].Frequency = PsgBase / 64; break; case 3: Channels[3].Frequency = Channels[2].Frequency; break; } var newWave = (value & 4) == 0 ? Waves.PeriodicWave16 : Waves.NoiseWave; if (newWave != Channels[3].Wave) { Channels[3].Wave = newWave; Channels[3].WaveOffset = 0f; } } private void WritePsgDataImmediate(byte value) { switch (value & 0xF0) { case 0x80: case 0xA0: case 0xC0: PsgLatch = value; break; case 0xE0: PsgLatch = value; UpdateNoiseType(value); break; case 0x90: Channels[0].Volume = (byte)(~value & 15); PsgLatch = value; break; case 0xB0: Channels[1].Volume = (byte)(~value & 15); PsgLatch = value; break; case 0xD0: Channels[2].Volume = (byte)(~value & 15); PsgLatch = value; break; case 0xF0: Channels[3].Volume = (byte)(~value & 15); PsgLatch = value; break; default: byte channel = (byte) ((PsgLatch & 0x60) >> 5); if ((PsgLatch & 16) == 0) // Tone latched { int f = PsgBase/(((value & 0x03F)*16) + (PsgLatch & 0x0F) + 1); if (f > 15000) f = 0; // upper bound of playable frequency Channels[channel].Frequency = (ushort) f; if ((Channels[3].NoiseType & 3) == 3 && channel == 2) Channels[3].Frequency = (ushort) f; } else { // volume latched Channels[channel].Volume = (byte)(~value & 15); } break; } } public byte StereoPanning { get { byte value = 0; if (Channels[0].Left) value |= 0x10; if (Channels[0].Right) value |= 0x01; if (Channels[1].Left) value |= 0x20; if (Channels[1].Right) value |= 0x02; if (Channels[2].Left) value |= 0x40; if (Channels[2].Right) value |= 0x04; if (Channels[3].Left) value |= 0x80; if (Channels[3].Right) value |= 0x08; return value; } set { Channels[0].Left = (value & 0x10) != 0; Channels[0].Right = (value & 0x01) != 0; Channels[1].Left = (value & 0x20) != 0; Channels[1].Right = (value & 0x02) != 0; Channels[2].Left = (value & 0x40) != 0; Channels[2].Right = (value & 0x04) != 0; Channels[3].Left = (value & 0x80) != 0; Channels[3].Right = (value & 0x08) != 0; } } public void SaveStateText(TextWriter writer) { writer.WriteLine("[PSG]"); writer.WriteLine("Volume0 {0:X2}", Channels[0].Volume); writer.WriteLine("Volume1 {0:X2}", Channels[1].Volume); writer.WriteLine("Volume2 {0:X2}", Channels[2].Volume); writer.WriteLine("Volume3 {0:X2}", Channels[3].Volume); writer.WriteLine("Freq0 {0:X4}", Channels[0].Frequency); writer.WriteLine("Freq1 {0:X4}", Channels[1].Frequency); writer.WriteLine("Freq2 {0:X4}", Channels[2].Frequency); writer.WriteLine("Freq3 {0:X4}", Channels[3].Frequency); writer.WriteLine("NoiseType {0:X}", Channels[3].NoiseType); writer.WriteLine("PsgLatch {0:X2}", PsgLatch); writer.WriteLine("Panning {0:X2}", StereoPanning); writer.WriteLine("[/PSG]"); writer.WriteLine(); } public void LoadStateText(TextReader reader) { while (true) { string[] args = reader.ReadLine().Split(' '); if (args[0].Trim() == "") continue; if (args[0] == "[/PSG]") break; if (args[0] == "Volume0") Channels[0].Volume = byte.Parse(args[1], NumberStyles.HexNumber); else if (args[0] == "Volume1") Channels[1].Volume = byte.Parse(args[1], NumberStyles.HexNumber); else if (args[0] == "Volume2") Channels[2].Volume = byte.Parse(args[1], NumberStyles.HexNumber); else if (args[0] == "Volume3") Channels[3].Volume = byte.Parse(args[1], NumberStyles.HexNumber); else if (args[0] == "Freq0") Channels[0].Frequency = ushort.Parse(args[1], NumberStyles.HexNumber); else if (args[0] == "Freq1") Channels[1].Frequency = ushort.Parse(args[1], NumberStyles.HexNumber); else if (args[0] == "Freq2") Channels[2].Frequency = ushort.Parse(args[1], NumberStyles.HexNumber); else if (args[0] == "Freq3") Channels[3].Frequency = ushort.Parse(args[1], NumberStyles.HexNumber); else if (args[0] == "NoiseType") Channels[3].NoiseType = byte.Parse(args[1], NumberStyles.HexNumber); else if (args[0] == "PsgLatch") PsgLatch = byte.Parse(args[1], NumberStyles.HexNumber); else if (args[0] == "Panning") StereoPanning = byte.Parse(args[1], NumberStyles.HexNumber); else Console.WriteLine("Skipping unrecognized identifier " + args[0]); } UpdateNoiseType(Channels[3].NoiseType); } public void SaveStateBinary(BinaryWriter writer) { writer.Write(Channels[0].Volume); writer.Write(Channels[1].Volume); writer.Write(Channels[2].Volume); writer.Write(Channels[3].Volume); writer.Write(Channels[0].Frequency); writer.Write(Channels[1].Frequency); writer.Write(Channels[2].Frequency); writer.Write(Channels[3].Frequency); writer.Write(Channels[3].NoiseType); writer.Write(PsgLatch); writer.Write(StereoPanning); } public void LoadStateBinary(BinaryReader reader) { Channels[0].Volume = reader.ReadByte(); Channels[1].Volume = reader.ReadByte(); Channels[2].Volume = reader.ReadByte(); Channels[3].Volume = reader.ReadByte(); Channels[0].Frequency = reader.ReadUInt16(); Channels[1].Frequency = reader.ReadUInt16(); Channels[2].Frequency = reader.ReadUInt16(); Channels[3].Frequency = reader.ReadUInt16(); UpdateNoiseType(reader.ReadByte()); PsgLatch = reader.ReadByte(); StereoPanning = reader.ReadByte(); } #region Frequency -> Note Conversion (for interested humans) public static string GetNote(int freq) { if (freq < 26) return "LOW"; if (freq > 4435) return "HIGH"; for (int i = 0; i < frequencies.Length - 1; i++) { if (freq >= frequencies[i + 1]) continue; int nextNoteDistance = frequencies[i + 1] - frequencies[i]; int distance = freq - frequencies[i]; if (distance < nextNoteDistance / 2) { // note identified return notes[i]; } } return "?"; } // For the curious, A4 = 440hz. Every octave is a doubling, so A5=880, A3=220 // Each next step is a factor of the 12-root of 2. So to go up a step you multiply by 1.0594630943592952645618252949463 // Next step from A4 is A#4. A#4 = (440.00 * 1.05946...) = 466.163... // Note that because frequencies must be integers, SMS games will be slightly out of pitch to a normally tuned instrument, especially at the low end. private static readonly int[] frequencies = { 27, // A0 29, // A#0 31, // B0 33, // C1 35, // C#1 37, // D1 39, // D#1 41, // E1 44, // F1 46, // F#1 49, // G1 52, // G#1 55, // A1 58, // A#1 62, // B1 65, // C2 69, // C#2 73, // D2 78, // D#2 82, // E2 87, // F2 92, // F#2 98, // G2 104, // G#2 110, // A2 117, // A#2 123, // B2 131, // C3 139, // C#3 147, // D3 156, // D#3 165, // E3 175, // F3 185, // F#3 196, // G3 208, // G#3 220, // A3 233, // A#3 247, // B3 262, // C4 277, // C#4 294, // D4 311, // D#4 330, // E4 349, // F4 370, // F#4 392, // G4 415, // G#4 440, // A4 466, // A#4 494, // B4 523, // C5 554, // C#5 587, // D5 622, // D#5 659, // E5 698, // F5 740, // F#5 784, // G5 831, // G#5 880, // A5 932, // A#5 988, // B5 1046, // C6 1109, // C#6 1175, // D6 1245, // D#6 1319, // E6 1397, // F6 1480, // F#6 1568, // G6 1661, // G#6 1760, // A6 1865, // A#6 1976, // B6 2093, // C7 2217, // C#7 2349, // D7 2489, // D#7 2637, // E7 2794, // F7 2960, // F#7 3136, // G7 3322, // G#7 3520, // A7 3729, // A#7 3951, // B7 4186, // C8 4435 // C#8 }; private static readonly string[] notes = { "A0","A#0","B0", "C1","C#1","D1","D#1","E1","F1","F#1","G1","G#1","A1","A#1","B1", "C2","C#2","D2","D#2","E2","F2","F#2","G2","G#2","A2","A#2","B2", "C3","C#3","D3","D#3","E3","F3","F#3","G3","G#3","A3","A#3","B3", "C4","C#4","D4","D#4","E4","F4","F#4","G4","G#4","A4","A#4","B4", "C5","C#5","D5","D#5","E5","F5","F#5","G5","G#5","A5","A#5","B5", "C6","C#6","D6","D#6","E6","F6","F#6","G6","G#6","A6","A#6","B6", "C7","C#7","D7","D#7","E7","F7","F#7","G7","G#7","A7","A#7","B7", "C8","HIGH" }; #endregion public void GetSamples(short[] samples) { int elapsedCycles = frameStopTime - frameStartTime; int start = 0; while (commands.Count > 0) { var cmd = commands.Dequeue(); int pos = ((cmd.Time*samples.Length)/elapsedCycles) & ~1; GetSamplesImmediate(samples, start, pos-start); start = pos; WritePsgDataImmediate(cmd.Value); } GetSamplesImmediate(samples, start, samples.Length - start); } public void GetSamplesImmediate(short[] samples, int start, int len) { for (int i = 0; i < 4; i++) Channels[i].Mix(samples, start, len); } class QueuedCommand { public byte Value; public int Time; } } }