258 lines
6.8 KiB
C#
258 lines
6.8 KiB
C#
using System.Collections.Generic;
|
|
|
|
using BizHawk.Common;
|
|
using BizHawk.Emulation.Common;
|
|
|
|
namespace BizHawk.Emulation.Cores.Components
|
|
{
|
|
public sealed class SN76489 : IMixedSoundProvider
|
|
{
|
|
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;
|
|
|
|
const int SampleRate = 44100;
|
|
private static readonly 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, int maxVolume)
|
|
{
|
|
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] / 0xFF * maxVolume / short.MaxValue) : 0);
|
|
samples[i++] += (short)(Right ? (value / 4 * LogScale[Volume] / 0xFF * maxVolume / short.MaxValue) : 0);
|
|
WaveOffset += moveThroughWaveRate;
|
|
if (WaveOffset >= Wave.Length)
|
|
WaveOffset %= Wave.Length;
|
|
}
|
|
}
|
|
}
|
|
|
|
public Channel[] Channels = new Channel[4];
|
|
public byte PsgLatch;
|
|
|
|
private readonly Queue<QueuedCommand> commands = new Queue<QueuedCommand>(256);
|
|
int frameStartTime, frameStopTime;
|
|
|
|
const int PsgBase = 111861;
|
|
|
|
public SN76489()
|
|
{
|
|
MaxVolume = short.MaxValue * 2 / 3;
|
|
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 });
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
byte stereoPanning = 0xFF;
|
|
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;
|
|
stereoPanning = value;
|
|
}
|
|
}
|
|
|
|
public void SyncState(Serializer ser)
|
|
{
|
|
ser.BeginSection("PSG");
|
|
ser.Sync("Volume0", ref Channels[0].Volume);
|
|
ser.Sync("Volume1", ref Channels[1].Volume);
|
|
ser.Sync("Volume2", ref Channels[2].Volume);
|
|
ser.Sync("Volume3", ref Channels[3].Volume);
|
|
ser.Sync("Freq0", ref Channels[0].Frequency);
|
|
ser.Sync("Freq1", ref Channels[1].Frequency);
|
|
ser.Sync("Freq2", ref Channels[2].Frequency);
|
|
ser.Sync("Freq3", ref Channels[3].Frequency);
|
|
ser.Sync("NoiseType", ref Channels[3].NoiseType);
|
|
ser.Sync("PsgLatch", ref PsgLatch);
|
|
ser.Sync("Panning", ref stereoPanning);
|
|
ser.EndSection();
|
|
|
|
if (ser.IsReader)
|
|
{
|
|
StereoPanning = stereoPanning;
|
|
UpdateNoiseType(Channels[3].NoiseType);
|
|
}
|
|
}
|
|
|
|
public int MaxVolume { get; set; }
|
|
public void DiscardSamples() { commands.Clear(); }
|
|
public void GetSamples(short[] samples)
|
|
{
|
|
int elapsedCycles = frameStopTime - frameStartTime;
|
|
if (elapsedCycles == 0)
|
|
elapsedCycles = 1; // hey it's better than diving by zero
|
|
|
|
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, MaxVolume);
|
|
}
|
|
|
|
class QueuedCommand
|
|
{
|
|
public byte Value;
|
|
public int Time;
|
|
}
|
|
}
|
|
} |