446 lines
16 KiB
C#
446 lines
16 KiB
C#
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<QueuedCommand> commands = new Queue<QueuedCommand>(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;
|
|
}
|
|
}
|
|
} |