/* * * * * * * * * * * * * * * * * * * * ______ ______ ______ * * / __ /\ / ____/\ / __ /\ * * / /\/ / // /____\// /\/ / / * * / ____/ //___ /\ /___ / / * * / /\___\/_\__/ / /__\_/ / / * * /_/ / /_____/ //_____/ / * * \_\/ \_____\/ \_____\/ * * * * * * * * * * * * * * * * * * * * * * * PSG emulation Ben Ryves 2005-2006 * * Many thanks to Maxim for his PSG * * notes which proved invaluable! * * * * * * * * * * * * * * * * * * * * * Usage: * * Set PsgClock to the correct value * * then call PsgReset(). * * Write to the PSG - PsgWriteByte() * * or PsgWriteStereoByte(). * * Once every sample, PsgGetOutput() * * will return a double[] containing * * the [left,right] levels of output * * * * * * * * * * * * * * * * * * * */ using System; namespace VGM_Player { public partial class VgmPlayer { // Set these properties to affect the sound of the noise channel (defaults to SMS) public int PsgTappedBits { get { return PsgNoiseTapped; } set { PsgNoiseTapped = value; } } public int PsgLsfrLength { get { return PsgNoiseLength; } set { PsgNoiseLength = value; } } // Property for setting the clock rate (defaults to SMS) private static uint _PsgClockFrequency = 3579540; public static uint PsgClock { get { return _PsgClockFrequency; } set { _PsgClockFrequency = value; PsgFreqScalerNum = _PsgClockFrequency >> 3; PsgFreqScalerDen = PsgFreqScalerNum >> 1; } } // If disabled, outputs linearly (incorrectly). // Linear output is louder and (IMO) generally sounds better. // Logarithmic output is supposedly correct, so who am I to argue with that? private static bool _PsgLogOutput = true; public bool PsgLogarithmicOutput { get { return _PsgLogOutput; } set { _PsgLogOutput = value; } } // Current state public static int[] PsgStatus = { 0, 0, 0, 0 }; // Voltage level (0=low, 1=high) public static int[,] PsgStereo = new int[4, 2]; // 1 or 0 (for stereo writes) public static int[] PsgLevels = { 15, 15, 15, 15 }; // Volume level of the 4 channels. private static int[] PsgToneReg = { 0, 0, 0, 0 }; // 10-bit tone registers. // Internal variables private static int[] PsgToneCounters = { 0, 0, 0, 0 }; private static int PsgLatchedChannel = 0; private static bool PsgLatchedTone = false; private static bool PsgNoiseLatch = false; private static int PsgNoiseValue = 0; private static int PsgNoiseLength = 16; private static int PsgNoiseTapped = 0x9; // Used for fixed-point matching of clock rate to sound sample rate private static uint PsgFreqScalerNum; private static uint PsgFreqScalerDen; // Internal use private static int PsgParity(int val) { val ^= val >> 8; val ^= val >> 4; val ^= val >> 2; val ^= val >> 1; return val & 1; } // Call to reset the PSG's state public static void PsgReset() { PsgClock = _PsgClockFrequency; for (int i = 0; i < 4; ++i) { PsgToneReg[i] = 0; PsgLevels[i] = 15; PsgStatus[i] = 0; PsgToneCounters[i] = 0; for (int c = 0; c < 2; ++c) { PsgStereo[i, c] = 1; } } PsgLatchedChannel = 0; PsgLatchedTone = false; PsgNoiseLatch = false; PsgNoiseValue = 1 << (PsgNoiseLength - 1); } // Internal use (run the PSG for a single cycle) private static void PsgTick() { for (int i = 0; i < 4; ++i) { if (PsgToneCounters[i] <= 0) { if (i != 3) { // Tone channels: PsgToneCounters[i] = PsgToneReg[i]; if (PsgToneReg[i] != 0) { PsgStatus[i] = 1 - PsgStatus[i]; } else { PsgStatus[i] = 0; } } else { // Noise channel: int NoiseVal = PsgToneReg[3] & 0x3; switch (NoiseVal) { case 0: PsgToneCounters[i] = 0x10; break; case 1: PsgToneCounters[i] = 0x20; break; case 2: PsgToneCounters[i] = 0x40; break; case 3: PsgToneCounters[i] = PsgToneReg[2]; break; } PsgNoiseLatch = !PsgNoiseLatch; if (PsgNoiseLatch) { PsgNoiseValue = (PsgNoiseValue >> 1) | ((((PsgToneReg[3] & 0x4) != 0) ? PsgParity(PsgNoiseValue & PsgNoiseTapped) : PsgNoiseValue & 1) << (PsgNoiseLength - 1)); PsgStatus[3] = (PsgNoiseValue & 0x1); } } } --PsgToneCounters[i]; } } // Call to write a byte to the PSG public static void PsgWriteByte(byte Command) { if ((Command & 0x80) != 0) { // It is a LATCH command PsgLatchedChannel = (Command >> 5) & 0x3; if ((Command & 0x10) == 0) { PsgLatchedTone = true; PsgToneReg[PsgLatchedChannel] &= 0xFFF0; PsgToneReg[PsgLatchedChannel] |= (Command & 0xF); // Reset the LSFR? if (PsgLatchedChannel == 3) { PsgNoiseValue = 1 << (PsgNoiseLength - 1); } } else { PsgLatchedTone = false; PsgLevels[PsgLatchedChannel] = Command & 0xF; } } else { // It is a DATA command if (PsgLatchedTone) { PsgToneReg[PsgLatchedChannel] &= 0xF; PsgToneReg[PsgLatchedChannel] |= (Command & 0x3F) << 4; } else { PsgLevels[PsgLatchedChannel] = Command & 0xF; } } } // Write to the Game Gear's stereo thingummy public static void PsgWriteStereoByte(byte Command) { for (int c = 0; c < 2; ++c) { for (int r = 0; r < 4; ++r) { PsgStereo[r, c] = (Command & 1); Command >>= 1; } } } private static double[] PsgLogScale = { 1.0d, 0.794335765d, 0.630970183d, 0.501174963d, 0.398113956d, 0.316232795d, 0.251197851d, 0.20044557d, 0.15848262d, 0.125888852d, 0.100009156d, 0.07943968d, 0.063081759d, 0.050111393d, 0.039796136d, 0.0d }; // Call to run the PSG and return the output level. // SampleRate is the current audio rate (eg 44100Hz) public static double[] PsgGetOutput(uint SampleRate) { bool GenerateSquares = true; int SquaresGenerated = 0; double OutR = 0; // Clock up the outputs of both channels double OutL = 0; while (GenerateSquares) { // Run the PSG for one clock cycle: PsgTick(); // Update the output sound values: ++SquaresGenerated; for (int i = 0; i < 4; ++i) { if (_PsgLogOutput) { OutR += 0.5d - PsgLogScale[PsgLevels[i]] * (double)(PsgStatus[i] * PsgStereo[i, 0]); OutL += 0.5d - PsgLogScale[PsgLevels[i]] * (double)(PsgStatus[i] * PsgStereo[i, 1]); } else { OutR += 0.5d - (double)((0x0F - PsgLevels[i]) * PsgStatus[i] * PsgStereo[i, 0]) / 16.0d; OutL += 0.5d - (double)((0x0F - PsgLevels[i]) * PsgStatus[i] * PsgStereo[i, 1]) / 16.0d; } } // Do we need to create any more squares? PsgFreqScalerNum += SampleRate; if (PsgFreqScalerNum > PsgFreqScalerDen) { PsgFreqScalerNum -= PsgFreqScalerDen; GenerateSquares = false; } } // Average them: if (SquaresGenerated == 0) SquaresGenerated = 1; // Avoid divide-by-zero OutL /= SquaresGenerated; OutR /= SquaresGenerated; return new double[2] { OutL, OutR}; } } }