using System; using System.Collections.Generic; using System.IO; using BizHawk.Emulation.Cores.Components; namespace BizHawk.Emulation.Common.Components { // ====================================================================== // Yamaha YM2612 Emulation Core // Primarily sourced from Nemesis's documentation on Sprite's Mind forums: // http://gendev.spritesmind.net/forum/viewtopic.php?t=386 // // Notes: // - In order to facilitate asynchronous sound generation, timer commands // and reads are emulated immediately, while all other commands are // queued together with a timestamp and processed at the end of the frame. // - Commands are stretched in time to match the number of samples requested // for the frame. For accurate, synchronous sound, simply request the correct // number of samples for each frame. // - Output is emulated at native output rate and downsampled (badly) to 44100hz. // ====================================================================== // TODO: Finish testing Envelope generator // TODO: maybe add guards when changing envelope parameters to immediately change envelope state (ie dont wait for EG cycle?) // TODO: Detune // TODO: LFO // TODO: Switch from Perfect Operator to Accurate Operator. // TODO: Operator1 Self-Feedback // TODO: MEM delayed samples // TODO: CSM mode // TODO: SSG-EG // TODO: Seriously, I think we need better resampling code. // TODO: Experiment with low-pass filters, etc. public sealed class YM2612 : IMixedSoundProvider { public readonly Channel[] Channels = { new Channel(), new Channel(), new Channel(), new Channel(), new Channel(), new Channel() }; public YM2612() { InitTimers(); MaxVolume = short.MaxValue; } // ==================================================================================== int frameStartClock; int frameEndClock; public void BeginFrame(int clock) { frameStartClock = clock; while (commands.Count > 0) { var cmd = commands.Dequeue(); WriteCommand(cmd); } } public void EndFrame(int clock) { frameEndClock = clock; } // ==================================================================================== // YM2612 I/O // ==================================================================================== public class QueuedCommand { public byte Part; public byte Register; public byte Data; public int Clock; } byte PartSelect; byte RegisterSelect; bool DacEnable; byte DacValue; readonly Queue commands = new Queue(); const int Slot1 = 0; const int Slot2 = 2; const int Slot3 = 1; const int Slot4 = 3; public byte ReadStatus(int clock) { UpdateTimers(clock); byte retval = 0; if (TimerATripped) retval |= 1; if (TimerBTripped) retval |= 2; return retval; } public void Write(int addr, byte value, int clock) { UpdateTimers(clock); if (addr == 0) { PartSelect = 1; RegisterSelect = value; return; } else if (addr == 2) { PartSelect = 2; RegisterSelect = value; return; } if (PartSelect == 1) { if (RegisterSelect == 0x24) { WriteTimerA_MSB_24(value, clock); return; } if (RegisterSelect == 0x25) { WriteTimerA_LSB_25(value, clock); return; } if (RegisterSelect == 0x26) { WriteTimerB_26(value, clock); return; } if (RegisterSelect == 0x27) { WriteTimerControl_27(value, clock); } // we process immediately AND enqueue command for port $27. Allows accurate tracking of CH3 special modes. } // If its not timer related just queue the command write var cmd = new QueuedCommand { Part = PartSelect, Register = RegisterSelect, Data = value, Clock = clock - frameStartClock }; commands.Enqueue(cmd); } void WriteCommand(QueuedCommand cmd) { if (cmd.Part == 1) Part1_WriteRegister(cmd.Register, cmd.Data); else Part2_WriteRegister(cmd.Register, cmd.Data); } static void GetChanOpP1(byte value, out int channel, out int oper) { value &= 15; switch (value) { case 0: channel = 0; oper = 0; return; case 4: channel = 0; oper = 2; return; case 8: channel = 0; oper = 1; return; case 12: channel = 0; oper = 3; return; case 1: channel = 1; oper = 0; return; case 5: channel = 1; oper = 2; return; case 9: channel = 1; oper = 1; return; case 13: channel = 1; oper = 3; return; case 2: channel = 2; oper = 0; return; case 6: channel = 2; oper = 2; return; case 10: channel = 2; oper = 1; return; case 14: channel = 2; oper = 3; return; default: channel = -1; oper = -1; return; } } static void GetChanOpP2(byte value, out int channel, out int oper) { value &= 15; switch (value) { case 0: channel = 3; oper = 0; return; case 4: channel = 3; oper = 2; return; case 8: channel = 3; oper = 1; return; case 12: channel = 3; oper = 3; return; case 1: channel = 4; oper = 0; return; case 5: channel = 4; oper = 2; return; case 9: channel = 4; oper = 1; return; case 13: channel = 4; oper = 3; return; case 2: channel = 5; oper = 0; return; case 6: channel = 5; oper = 2; return; case 10: channel = 5; oper = 1; return; case 14: channel = 5; oper = 3; return; default: channel = -1; oper = -1; return; } } void Part1_WriteRegister(byte register, byte value) { int chan, oper; GetChanOpP1(register, out chan, out oper); switch (register & 0xF0) { case 0x20: WriteLowBlock(register, value); break; case 0x30: Write_MUL_DT(chan, oper, value); break; case 0x40: Write_TL(chan, oper, value); break; case 0x50: Write_AR_KS(chan, oper, value); break; case 0x60: Write_DR_AM(chan, oper, value); break; case 0x70: Write_SR(chan, oper, value); break; case 0x80: Write_RR_SL(chan, oper, value); break; case 0x90: Write_SSGEG(chan, oper, value); break; case 0xA0: case 0xB0: WriteHighBlockP1(register, value); break; } } void Part2_WriteRegister(byte register, byte value) { int chan, oper; GetChanOpP2(register, out chan, out oper); switch (register & 0xF0) { case 0x30: Write_MUL_DT(chan, oper, value); break; case 0x40: Write_TL(chan, oper, value); break; case 0x50: Write_AR_KS(chan, oper, value); break; case 0x60: Write_DR_AM(chan, oper, value); break; case 0x70: Write_SR(chan, oper, value); break; case 0x80: Write_RR_SL(chan, oper, value); break; case 0x90: Write_SSGEG(chan, oper, value); break; case 0xA0: case 0xB0: WriteHighBlockP2(register, value); break; } } void WriteLowBlock(byte register, byte value) { switch (register) { //case 0x22: Console.WriteLine("LFO Control {0:X2}", value); break; case 0x24: break; // Timer A MSB, handled immediately case 0x25: break; // Timer A LSB, handled immediately case 0x26: break; // Timer B, handled immediately //case 0x27: Console.WriteLine("$27: Ch3 Mode / Timer Control {0:X2}", value); break; // determines if CH3 has 1 frequency or 4 frequencies. case 0x28: KeyOnOff(value); break; case 0x2A: DacValue = value; break; case 0x2B: DacEnable = (value & 0x80) != 0; break; case 0x2C: throw new Exception("something wrote to ym2612 port $2C!"); //http://forums.sonicretro.org/index.php?showtopic=28589 } } void WriteHighBlockP1(byte register, byte value) { switch (register) { case 0xA0: WriteFrequencyLow(Channels[0], value); break; case 0xA1: WriteFrequencyLow(Channels[1], value); break; case 0xA2: WriteFrequencyLow(Channels[2], value); break; case 0xA4: WriteFrequencyHigh(Channels[0], value); break; case 0xA5: WriteFrequencyHigh(Channels[1], value); break; case 0xA6: WriteFrequencyHigh(Channels[2], value); break; case 0xB0: Write_Feedback_Algorithm(Channels[0], value); break; case 0xB1: Write_Feedback_Algorithm(Channels[1], value); break; case 0xB2: Write_Feedback_Algorithm(Channels[2], value); break; case 0xB4: Write_Stereo_LfoSensitivy(Channels[0], value); break; case 0xB5: Write_Stereo_LfoSensitivy(Channels[1], value); break; case 0xB6: Write_Stereo_LfoSensitivy(Channels[2], value); break; } } void WriteHighBlockP2(byte register, byte value) { switch (register) { case 0xA0: WriteFrequencyLow(Channels[3], value); break; case 0xA1: WriteFrequencyLow(Channels[4], value); break; case 0xA2: WriteFrequencyLow(Channels[5], value); break; case 0xA4: WriteFrequencyHigh(Channels[3], value); break; case 0xA5: WriteFrequencyHigh(Channels[4], value); break; case 0xA6: WriteFrequencyHigh(Channels[5], value); break; case 0xB0: Write_Feedback_Algorithm(Channels[3], value); break; case 0xB1: Write_Feedback_Algorithm(Channels[4], value); break; case 0xB2: Write_Feedback_Algorithm(Channels[5], value); break; case 0xB4: Write_Stereo_LfoSensitivy(Channels[3], value); break; case 0xB5: Write_Stereo_LfoSensitivy(Channels[4], value); break; case 0xB6: Write_Stereo_LfoSensitivy(Channels[5], value); break; } } void KeyOnOff(byte value) { int channel = value & 3; if (channel == 3) return; // illegal channel number, abort if ((value & 4) != 0) channel += 3; // select part 2 var chan = Channels[channel]; //Console.WriteLine("KeyOnOff for channel {0}", channel); if ((value & 0x10) != 0) KeyOn(chan.Operators[Slot1]); else KeyOff(chan.Operators[Slot1]); if ((value & 0x20) != 0) KeyOn(chan.Operators[Slot2]); else KeyOff(chan.Operators[Slot2]); if ((value & 0x40) != 0) KeyOn(chan.Operators[Slot3]); else KeyOff(chan.Operators[Slot3]); if ((value & 0x80) != 0) KeyOn(chan.Operators[Slot4]); else KeyOff(chan.Operators[Slot4]); } static void WriteFrequencyLow(Channel channel, byte value) { channel.FrequencyNumber &= 0x700; channel.FrequencyNumber |= value; // TODO maybe its 4-frequency mode // TODO is this right, only reflect change when writing LSB? Operator op; op = channel.Operators[0]; op.FrequencyNumber = channel.FrequencyNumber; op.Block = channel.Block; CalcKeyCode(op); CalcRks(op); op = channel.Operators[1]; op.FrequencyNumber = channel.FrequencyNumber; op.Block = channel.Block; CalcKeyCode(op); CalcRks(op); op = channel.Operators[2]; op.FrequencyNumber = channel.FrequencyNumber; op.Block = channel.Block; CalcKeyCode(op); CalcRks(op); op = channel.Operators[3]; op.FrequencyNumber = channel.FrequencyNumber; op.Block = channel.Block; CalcKeyCode(op); CalcRks(op); } static void WriteFrequencyHigh(Channel channel, byte value) { channel.FrequencyNumber &= 0x0FF; channel.FrequencyNumber |= (value & 15) << 8; channel.Block = (value >> 3) & 7; } static void CalcKeyCode(Operator op) { int freq = op.FrequencyNumber; bool f11 = ((freq >> 10) & 1) != 0; bool f10 = ((freq >> 9) & 1) != 0; bool f09 = ((freq >> 8) & 1) != 0; bool f08 = ((freq >> 7) & 1) != 0; bool n3a = f11 & (f10 | f09 | f08); bool n3b = !f11 & f10 & f09 & f08; bool n3 = n3a | n3b; op.KeyCode = (op.Block << 2) | (f11 ? 2 : 0) | (n3 ? 1 : 0); } static void CalcRks(Operator op) { int shiftVal = 3 - op.KS_KeyScale; op.Rks = op.KeyCode >> shiftVal; } static void Write_Feedback_Algorithm(Channel channel, byte value) { channel.Algorithm = value & 7; channel.Feedback = (value >> 3) & 7; } static void Write_Stereo_LfoSensitivy(Channel channel, byte value) { channel.FMS_FrequencyModulationSensitivity = value & 3; channel.AMS_AmplitudeModulationSensitivity = (value >> 3) & 7; channel.RightOutput = (value & 0x40) != 0; channel.LeftOutput = (value & 0x80) != 0; } void Write_MUL_DT(int chan, int op, byte value) { if (chan < 0) return; var oper = Channels[chan].Operators[op]; oper.MUL_Multiple = value & 15; oper.DT_Detune = (value >> 4) & 7; } public void Write_TL(int chan, int op, byte value) { if (chan < 0) return; var oper = Channels[chan].Operators[op]; oper.TL_TotalLevel = value & 127; } public void Write_AR_KS(int chan, int op, byte value) { if (chan < 0) return; var oper = Channels[chan].Operators[op]; oper.AR_AttackRate = value & 31; oper.KS_KeyScale = value >> 6; CalcRks(oper); } public void Write_DR_AM(int chan, int op, byte value) { if (chan < 0) return; var oper = Channels[chan].Operators[op]; oper.DR_DecayRate = value & 31; oper.AM_AmplitudeModulation = (value & 128) != 0; } public void Write_SR(int chan, int op, byte value) { if (chan < 0) return; var oper = Channels[chan].Operators[op]; oper.SR_SustainRate = value & 31; } public void Write_RR_SL(int chan, int op, byte value) { if (chan < 0) return; var oper = Channels[chan].Operators[op]; oper.RR_ReleaseRate = value & 15; oper.SL_SustainLevel = value >> 4; } public void Write_SSGEG(int chan, int op, byte value) { if (chan < 0) return; var oper = Channels[chan].Operators[op]; oper.SSG_EG = value & 15; } // ==================================================================================== // Timers // ==================================================================================== // Assuming this is connected to a Genesis/MegaDrive: // The master clock on the Genesis is 53,693,175 MCLK / sec (NTSC) // 53,203,424 MCLK / sec (PAL) // 7,670,454 68K cycles / sec (7 MCLK divisor) (NTSC) // 3,579,545 Z80 cycles / sec (15 MCLK divisor) (NTSC) // YM2612 is fed by 68000 Clock: 7,670,454 ECLK / sec (NTSC) // 7,600,489 ECLK / sec (PAL) // YM2612 has /6 divisor on the EXT CLOCK. // YM2612 takes 24 cycles to generate a sample. 6*24 = 144. This is where the /144 divisor comes from. // YM2612 native output rate is 7670454 / 144 = 53267 hz (NTSC), 52781 hz (PAL) // Timer A ticks at the native output rate (53267 times per second for NTSC). // Timer B ticks down with a /16 divisor. (3329 times per second for NTSC). // Ergo, Timer A ticks every 67.2 Z80 cycles. Timer B ticks every 1075.2 Z80 cycles. // TODO: make this not hardcoded to Genesis timing. const float timerAZ80Factor = 67.2f; const float timerBZ80Factor = 1075.2f; const int ntscOutputRate = 53267; const int palOutputRate = 52781; const float ntsc44100Factor = 1.20786848f; const float pal44100Factor = 1.19684807f; int TimerAPeriod, TimerBPeriod; bool TimerATripped, TimerBTripped; int TimerAResetClock, TimerBResetClock; int TimerALastReset, TimerBLastReset; byte TimerControl27; bool TimerALoad { get { return (TimerControl27 & 1) != 0; } } bool TimerBLoad { get { return (TimerControl27 & 2) != 0; } } bool TimerAEnable { get { return (TimerControl27 & 4) != 0; } } bool TimerBEnable { get { return (TimerControl27 & 8) != 0; } } bool TimerAReset { get { return (TimerControl27 & 16) != 0; } } bool TimerBReset { get { return (TimerControl27 & 32) != 0; } } void InitTimers() { TimerAResetClock = 68812; TimerBResetClock = 275200; } void UpdateTimers(int clock) { int elapsedCyclesSinceLastTimerAReset = clock - TimerALastReset; if (elapsedCyclesSinceLastTimerAReset > TimerAResetClock) { if (TimerAEnable) TimerATripped = true; int numTimesTripped = elapsedCyclesSinceLastTimerAReset / TimerAResetClock; TimerALastReset += (TimerAResetClock * numTimesTripped); } int elapsedCyclesSinceLastTimerBReset = clock - TimerBLastReset; if (elapsedCyclesSinceLastTimerBReset > TimerBResetClock) { if (TimerBEnable) TimerBTripped = true; int numTimesTripped = elapsedCyclesSinceLastTimerBReset / TimerBResetClock; TimerBLastReset += (TimerBResetClock * numTimesTripped); } } void WriteTimerA_MSB_24(byte value, int clock) { TimerAPeriod = (value << 2) | (TimerAPeriod & 3); TimerAResetClock = (int)((1024 - TimerAPeriod) * timerAZ80Factor); } void WriteTimerA_LSB_25(byte value, int clock) { TimerAPeriod = (TimerAPeriod & 0x3FC) | (value & 3); TimerAResetClock = (int)((1024 - TimerAPeriod) * timerAZ80Factor); } void WriteTimerB_26(byte value, int clock) { TimerBPeriod = value; TimerBResetClock = (int)((256 - TimerBPeriod) * timerBZ80Factor); } void WriteTimerControl_27(byte value, int clock) { bool lagALoad = TimerALoad; bool lagBLoad = TimerBLoad; TimerControl27 = value; if (!lagALoad && TimerALoad) TimerALastReset = clock; if (!lagBLoad && TimerBLoad) TimerBLastReset = clock; if (TimerAReset) TimerATripped = false; if (TimerBReset) TimerBTripped = false; } // ==================================================================================== // Support Tables // ==================================================================================== #region tables static readonly byte[] egRateCounterShiftValues = { 11, 11, 11, 11, // Rates 0-3 10, 10, 10, 10, // Rates 4-7 9, 9, 9, 9, // Rates 8-11 8, 8, 8, 8, // Rates 12-15 7, 7, 7, 7, // Rates 16-19 6, 6, 6, 6, // Rates 20-23 5, 5, 5, 5, // Rates 24-27 4, 4, 4, 4, // Rates 28-31 3, 3, 3, 3, // Rates 32-35 2, 2, 2, 2, // Rates 36-39 1, 1, 1, 1, // Rates 40-43 0, 0, 0, 0, // Rates 44-47 0, 0, 0, 0, // Rates 48-51 0, 0, 0, 0, // Rates 52-55 0, 0, 0, 0, // Rates 56-59 0, 0, 0, 0 // Rates 60-63 }; static readonly byte[] egRateIncrementValues = { 0,0,0,0,0,0,0,0, // Rate 0 0,0,0,0,0,0,0,0, // Rate 1 0,1,0,1,0,1,0,1, // Rate 2 0,1,0,1,0,1,0,1, // Rate 3 0,1,0,1,0,1,0,1, // Rate 4 0,1,0,1,0,1,0,1, // Rate 5 0,1,1,1,0,1,1,1, // Rate 6 0,1,1,1,0,1,1,1, // Rate 7 0,1,0,1,0,1,0,1, // Rate 8 0,1,0,1,1,1,0,1, // Rate 9 0,1,1,1,0,1,1,1, // Rate 10 0,1,1,1,1,1,1,1, // Rate 11 0,1,0,1,0,1,0,1, // Rate 12 0,1,0,1,1,1,0,1, // Rate 13 0,1,1,1,0,1,1,1, // Rate 14 0,1,1,1,1,1,1,1, // Rate 15 0,1,0,1,0,1,0,1, // Rate 16 0,1,0,1,1,1,0,1, // Rate 17 0,1,1,1,0,1,1,1, // Rate 18 0,1,1,1,1,1,1,1, // Rate 19 0,1,0,1,0,1,0,1, // Rate 20 0,1,0,1,1,1,0,1, // Rate 21 0,1,1,1,0,1,1,1, // Rate 22 0,1,1,1,1,1,1,1, // Rate 23 0,1,0,1,0,1,0,1, // Rate 24 0,1,0,1,1,1,0,1, // Rate 25 0,1,1,1,0,1,1,1, // Rate 26 0,1,1,1,1,1,1,1, // Rate 27 0,1,0,1,0,1,0,1, // Rate 28 0,1,0,1,1,1,0,1, // Rate 29 0,1,1,1,0,1,1,1, // Rate 30 0,1,1,1,1,1,1,1, // Rate 31 0,1,0,1,0,1,0,1, // Rate 32 0,1,0,1,1,1,0,1, // Rate 33 0,1,1,1,0,1,1,1, // Rate 34 0,1,1,1,1,1,1,1, // Rate 35 0,1,0,1,0,1,0,1, // Rate 36 0,1,0,1,1,1,0,1, // Rate 37 0,1,1,1,0,1,1,1, // Rate 38 0,1,1,1,1,1,1,1, // Rate 39 0,1,0,1,0,1,0,1, // Rate 40 0,1,0,1,1,1,0,1, // Rate 41 0,1,1,1,0,1,1,1, // Rate 42 0,1,1,1,1,1,1,1, // Rate 43 0,1,0,1,0,1,0,1, // Rate 44 0,1,0,1,1,1,0,1, // Rate 45 0,1,1,1,0,1,1,1, // Rate 46 0,1,1,1,1,1,1,1, // Rate 47 1,1,1,1,1,1,1,1, // Rate 48 1,1,1,2,1,1,1,2, // Rate 49 1,2,1,2,1,2,1,2, // Rate 50 1,2,2,2,1,2,2,2, // Rate 51 2,2,2,2,2,2,2,2, // Rate 52 2,2,2,4,2,2,2,4, // Rate 53 2,4,2,4,2,4,2,4, // Rate 54 2,4,4,4,2,4,4,4, // Rate 55 4,4,4,4,4,4,4,4, // Rate 56 4,4,4,8,4,4,4,8, // Rate 57 4,8,4,8,4,8,4,8, // Rate 58 4,8,8,8,4,8,8,8, // Rate 59 8,8,8,8,8,8,8,8, // Rate 60 8,8,8,8,8,8,8,8, // Rate 61 8,8,8,8,8,8,8,8, // Rate 62 8,8,8,8,8,8,8,8 // Rate 63 }; static readonly int[] slTable = // translates a 4-bit SL value into a 10-bit attenuation value { 0x000, 0x020, 0x040, 0x060, 0x080, 0x0A0, 0x0C0, 0x0E0, 0x100, 0x120, 0x140, 0x160, 0x180, 0x1A0, 0x1C0, 0x3FF }; static readonly int[] detuneTable = { 0, 0, 1, 2, // Key-Code 0 0, 0, 1, 2, // Key-Code 1 0, 0, 1, 2, // Key-Code 2 0, 0, 1, 2, // Key-Code 3 0, 1, 2, 2, // Key-Code 4 0, 1, 2, 3, // Key-Code 5 0, 1, 2, 3, // Key-Code 6 0, 1, 2, 3, // Key-Code 7 0, 1, 2, 4, // Key-Code 8 0, 1, 3, 4, // Key-Code 9 0, 1, 3, 4, // Key-Code 10 0, 1, 3, 5, // Key-Code 11 0, 2, 4, 5, // Key-Code 12 0, 2, 4, 6, // Key-Code 13 0, 2, 4, 6, // Key-Code 14 0, 2, 5, 7, // Key-Code 15 0, 2, 5, 8, // Key-Code 16 0, 3, 6, 8, // Key-Code 17 0, 3, 6, 9, // Key-Code 18 0, 3, 7, 10, // Key-Code 19 0, 4, 8, 11, // Key-Code 20 0, 4, 8, 12, // Key-Code 21 0, 4, 9, 13, // Key-Code 22 0, 5, 10, 14, // Key-Code 23 0, 5, 11, 16, // Key-Code 24 0, 6, 12, 17, // Key-Code 25 0, 6, 13, 19, // Key-Code 26 0, 7, 14, 20, // Key-Code 27 0, 8, 16, 22, // Key-Code 28 0, 8, 16, 22, // Key-Code 29 0, 8, 16, 22, // Key-Code 30 0, 8, 16, 22 // Key-Code 31 }; #endregion // ==================================================================================== // Envelope Generator // ==================================================================================== int egDivisorCounter; // This provides the /3 divisor to run the envelope generator once for every 3 FM sample output ticks. int egCycleCounter; // This provides a rolling counter of the envelope generator update ticks. (/3 divisor already applied) const int MaxAttenuation = 1023; void MaybeRunEnvelopeGenerator() { if (egDivisorCounter == 0) { for (int c = 0; c < 6; c++) for (int o = 0; o < 4; o++) EnvelopeGeneratorTick(Channels[c].Operators[o]); egCycleCounter++; } egDivisorCounter++; if (egDivisorCounter == 3) egDivisorCounter = 0; } void EnvelopeGeneratorTick(Operator op) { // First, let's handle envelope generator phase transitions. if (op.EnvelopeState == EnvelopeState.Off) return; if (op.EnvelopeState != EnvelopeState.Attack && op.EgAttenuation == MaxAttenuation) { op.EnvelopeState = EnvelopeState.Off; return; } if (op.EnvelopeState == EnvelopeState.Attack && op.EgAttenuation == 0) { op.EnvelopeState = EnvelopeState.Decay; if (op.SL_SustainLevel == 0) // If Sustain Level is 0, we skip Decay and go straight to Sustain phase. op.EnvelopeState = EnvelopeState.Sustain; } if (op.EnvelopeState == EnvelopeState.Decay && op.EgAttenuation >= op.Normalized10BitSL) { op.EnvelopeState = EnvelopeState.Sustain; } // At this point, we've determined what envelope phase we're in. Lets do the update. // Start by calculating Rate. int rate = 0; switch (op.EnvelopeState) { case EnvelopeState.Attack: rate = op.AR_AttackRate; break; case EnvelopeState.Decay: rate = op.DR_DecayRate; break; case EnvelopeState.Sustain: rate = op.SR_SustainRate; break; case EnvelopeState.Release: rate = (op.RR_ReleaseRate << 1) + 1; break; } if (rate != 0) // rate=0 is 0 no matter the value of Rks. rate = Math.Min((rate * 2) + op.Rks, 63); // Now we have rate. figure out shift value and cycle offset int shiftValue = egRateCounterShiftValues[rate]; if (egCycleCounter % (1 << shiftValue) == 0) { // Update attenuation value this tick int updateCycleOffset = (egCycleCounter >> shiftValue) & 7; // gives the offset within the 8-step cycle int attenuationAdjustment = egRateIncrementValues[(rate * 8) + updateCycleOffset]; if (op.EnvelopeState == EnvelopeState.Attack) op.EgAttenuation += (~op.EgAttenuation * attenuationAdjustment) >> 4; else // One of the decay phases op.EgAttenuation += attenuationAdjustment; } } static void KeyOn(Operator op) { op.PhaseCounter = 0; // Reset Phase Generator if (op.AR_AttackRate >= 30) { // AR of 30 or 31 skips attack phase op.EgAttenuation = 0; // Force minimum attenuation op.EnvelopeState = EnvelopeState.Decay; if (op.SL_SustainLevel == 0) // If Sustain Level is 0, we skip Decay and go straight to Sustain phase. op.EnvelopeState = EnvelopeState.Sustain; } else { // Regular Key-On op.EnvelopeState = EnvelopeState.Attack; } } static void KeyOff(Operator op) { op.EnvelopeState = EnvelopeState.Release; } // ==================================================================================== // Operator Unit // ==================================================================================== int GetOperatorOutput(Operator op, int phaseModulationInput14) { if (op.EgAttenuation == MaxAttenuation) return 0; RunPhaseGenerator(op); int phase10 = op.PhaseCounter >> 10; // operators return a 14-bit output, but take a 10-bit input. What 4 bits are discarded? not the obvious ones... // the input is shifted right by one; the least significant bit and the 3 most significant bits are discarded. int phaseModulationInput10 = (phaseModulationInput14 >> 1) & 0x3FF; phase10 += phaseModulationInput10; phase10 &= 0x3FF; return OperatorCalc(phase10, op.AdjustedEGOutput); } static void RunPhaseGenerator(Operator op) { // Take the Frequency Number & shift based on Block int phaseIncrement = op.FrequencyNumber; switch (op.Block) { case 0: phaseIncrement >>= 1; break; case 1: break; default: phaseIncrement <<= op.Block - 1; break; } // Apply Detune int detuneAdjustment = detuneTable[(op.KeyCode * 4) + (op.DT_Detune & 3)]; if ((op.DT_Detune & 4) != 0) detuneAdjustment = -detuneAdjustment; phaseIncrement += detuneAdjustment; phaseIncrement &= 0x1FFFF; // mask to 17-bits, which is the current size of the register at this point in the calculation. This allows proper detune overflow. // Apply MUL switch (op.MUL_Multiple) { case 0: phaseIncrement /= 2; break; default: phaseIncrement *= op.MUL_Multiple; break; } op.PhaseCounter += phaseIncrement; op.PhaseCounter &= 0xFFFFF; } static int OperatorCalc(int phase10, int attenuation) { // calculate sin double phaseNormalized = (phase10 / 1023d); double sinResult = Math.Sin(phaseNormalized * Math.PI * 2); // convert attenuation into linear power representation const double attenuationIndividualBitWeighting = 48.0 / 1024.0; double attenuationInBels = (((double)attenuation * attenuationIndividualBitWeighting) / 10.0); double powerLinear = Math.Pow(10.0, -attenuationInBels); // attenuate result double resultNormalized = sinResult * powerLinear; // calculate 14-bit operator output const int maxOperatorOutput = 8191; return (int)(resultNormalized * maxOperatorOutput); } // ==================================================================================== // Channel Unit // ==================================================================================== const int max14bitValue = 0x1FFF; // maximum signed value int GetChannelOutput(Channel channel, int maxVolume) { int outc = 0; switch (channel.Algorithm) { case 0: { int out1; out1 = GetOperatorOutput(channel.Operators[0], 0); out1 = GetOperatorOutput(channel.Operators[1], out1); out1 = GetOperatorOutput(channel.Operators[2], out1); outc = GetOperatorOutput(channel.Operators[3], out1); break; } case 1: { int out1, out2; out1 = GetOperatorOutput(channel.Operators[0], 0); out2 = GetOperatorOutput(channel.Operators[1], 0); outc = GetOperatorOutput(channel.Operators[2], Limit(out1 + out2, -8191, 8191)); // TODO test whether these Limit calls are actually correct. technically I expect it to be overflowing in a 10-bit space. outc = GetOperatorOutput(channel.Operators[3], outc); break; } case 2: { int out1, out2; out1 = GetOperatorOutput(channel.Operators[0], 0); out2 = GetOperatorOutput(channel.Operators[1], 0); out2 = GetOperatorOutput(channel.Operators[2], out2); outc = GetOperatorOutput(channel.Operators[3], Limit(out1 + out2, -8191, 8191)); break; } case 3: { int out1, out2; out1 = GetOperatorOutput(channel.Operators[0], 0); out1 = GetOperatorOutput(channel.Operators[1], out1); out2 = GetOperatorOutput(channel.Operators[2], 0); outc = GetOperatorOutput(channel.Operators[3], Limit(out1 + out2, -8191, 8191)); break; } case 4: { int out1, out2; out1 = GetOperatorOutput(channel.Operators[0], 0); out1 = GetOperatorOutput(channel.Operators[1], out1); out2 = GetOperatorOutput(channel.Operators[2], 0); out2 = GetOperatorOutput(channel.Operators[3], out2); outc = Limit(out1 + out2, -8191, 8191); break; } case 5: { int out1, out2, out3, out4; out1 = GetOperatorOutput(channel.Operators[0], 0); out2 = GetOperatorOutput(channel.Operators[1], out1); out3 = GetOperatorOutput(channel.Operators[2], out1); out4 = GetOperatorOutput(channel.Operators[3], out1); outc = Limit(out2 + out3 + out4, -8191, 8191); break; } case 6: { int out1, out2, out3, out4; out1 = GetOperatorOutput(channel.Operators[0], 0); out2 = GetOperatorOutput(channel.Operators[1], out1); out3 = GetOperatorOutput(channel.Operators[2], out1); out4 = GetOperatorOutput(channel.Operators[3], out1); outc = Limit(out2 + out3 + out4, -8191, 8191); break; } case 7: { int out1, out2, out3, out4; out1 = GetOperatorOutput(channel.Operators[0], 0); out2 = GetOperatorOutput(channel.Operators[1], out1); out3 = GetOperatorOutput(channel.Operators[2], 0); out4 = GetOperatorOutput(channel.Operators[3], 0); outc = Limit(out2 + out3 + out4, -8191, 8191); break; } } return outc * maxVolume / max14bitValue; } static int Limit(int value, int min, int max) { if (value < min) return min; if (value > max) return max; return value; } // ==================================================================================== // Support Classes/Structs/Enums // ==================================================================================== public enum EnvelopeState { Attack, Decay, Sustain, Release, Off } public sealed class Operator { // External Settings public int TL_TotalLevel; // 7 bits public int SL_SustainLevel; // 4 bits public int AR_AttackRate; // 5 bits public int DR_DecayRate; // 5 bits public int SR_SustainRate; // 5 bits public int RR_ReleaseRate; // 4 bits public int KS_KeyScale; // 2 bits public int SSG_EG; // 4 bits public int DT_Detune; // 3 bits public int MUL_Multiple; // 4 bits public bool AM_AmplitudeModulation; // 1 bit public int FrequencyNumber; // 11 bits public int Block; // 3 bits public int KeyCode; // 5 bits (described on pg 25 of YM2608 docs) public int Rks; // 5 bits (described on pg 29 of YM2608 docs) // Internal State public int PhaseCounter; // 20 bits, where the 10 most significant bits are output to the operator. public EnvelopeState EnvelopeState = EnvelopeState.Off; private int egAttenuation = MaxAttenuation; // 10-bit attenuation value output from envelope generator public int EgAttenuation { get { return egAttenuation; } set { egAttenuation = value; if (egAttenuation < 0) egAttenuation = 0; if (egAttenuation > 1023) egAttenuation = 1023; } } public int Normalized10BitSL { get { return slTable[SL_SustainLevel]; } } public int Normalized10BitTL { get { return TL_TotalLevel << 3; } } public int AdjustedEGOutput { get { return Math.Min(egAttenuation + Normalized10BitTL, 1023); } } } public sealed class Channel { public readonly Operator[] Operators = { new Operator(), new Operator(), new Operator(), new Operator() }; public int FrequencyNumber; // 11 bits public int Block; // 3 bits public int Feedback; // 3 bits public int Algorithm; // 3 bits (algorithms 0 - 7) public bool SpecialMode; // TODO, there are 2 special modes, a bool is not going to do the trick. public bool LeftOutput = true; // These apparently need to be initialized on. public bool RightOutput = true; // These apparently need to be initialized on. public int AMS_AmplitudeModulationSensitivity; // 3 bits public int FMS_FrequencyModulationSensitivity; // 2 bits } // ==================================================================================== // ISoundProvider // ==================================================================================== public void GetSamples(short[] samples) { // Generate raw samples at native sampling rate (~53hz) int numStereoSamples = samples.Length / 2; int nativeStereoSamples = (int)(numStereoSamples * ntsc44100Factor) * 2; short[] nativeSamples = new short[nativeStereoSamples]; GetSamplesNative(nativeSamples); // downsample from native output rate to 44100. //CrappyNaiveResampler(nativeSamples, samples); MaybeBetterDownsampler(nativeSamples, samples); //LinearDownsampler(nativeSamples, samples); } static void CrappyNaiveResampler(short[] input, short[] output) { // this is not good resampling code. int numStereoSamples = output.Length / 2; int offset = 0; for (int i = 0; i < numStereoSamples; i++) { int nativeOffset = ((i * ntscOutputRate) / 44100) * 2; output[offset++] += input[nativeOffset++]; // left output[offset++] += input[nativeOffset]; // right } } static double Fraction(double value) { return value - Math.Floor(value); } static void LinearDownsampler(short[] input, short[] output) { double samplefactor = input.Length / (double)output.Length; for (int i = 0; i < output.Length / 2; i++) { // exact position on input stream double inpos = i * samplefactor; // selected interpolation points and weights int pt0 = (int)inpos; // pt1 = pt0 + 1 double wt1 = inpos - pt0; // wt0 = 1 - wt1 double wt0 = 1.0 - wt1; output[i * 2 + 0] = (short)(input[pt0 * 2 + 0] * wt0 + input[pt0 * 2 + 2] * wt1); output[i * 2 + 1] = (short)(input[pt0 * 2 + 1] * wt0 + input[pt0 * 2 + 3] * wt1); } } static void MaybeBetterDownsampler(short[] input, short[] output) { // This is still not a good resampler. But it's better than the other one. Unsure how much difference it makes. // The difference with this one is that all source samples will be sampled and weighted, none skipped over. double nativeSamplesPerOutputSample = (double)input.Length / (double)output.Length; int outputStereoSamples = output.Length / 2; int inputStereoSamples = input.Length / 2; int offset = 0; for (int i = 0; i < outputStereoSamples; i++) { double startSample = nativeSamplesPerOutputSample * i; double endSample = nativeSamplesPerOutputSample * (i + 1); int iStartSample = (int)Math.Floor(startSample); int iEndSample = (int)Math.Floor(endSample); double leftSample = 0; double rightSample = 0; for (int j = iStartSample; j <= iEndSample; j++) { if (j == inputStereoSamples) break; double weight = 1.0; if (j == iStartSample) weight = 1.0 - Fraction(startSample); else if (j == iEndSample) weight = Fraction(endSample); leftSample += ((double)input[(j * 2) + 0] * weight); rightSample += ((double)input[(j * 2) + 1] * weight); } output[offset++] = (short)leftSample; output[offset++] = (short)rightSample; } } void GetSamplesNative(short[] samples) { int elapsedCycles = frameEndClock - frameStartClock; int start = 0; while (commands.Count > 0) { var cmd = commands.Dequeue(); int pos = ((cmd.Clock * samples.Length) / elapsedCycles) & ~1; GetSamplesImmediate(samples, start, pos - start); start = pos; WriteCommand(cmd); } GetSamplesImmediate(samples, start, samples.Length - start); } void GetSamplesImmediate(short[] samples, int pos, int length) { int channelVolume = MaxVolume / 6; for (int i = 0; i < length / 2; i++) { MaybeRunEnvelopeGenerator(); // Generate FM output for (int ch = 0; ch < 6; ch++) { short sample = (short)GetChannelOutput(Channels[ch], channelVolume); if (ch < 5 || DacEnable == false) { if (Channels[ch].LeftOutput) samples[pos] += sample; if (Channels[ch].RightOutput) samples[pos + 1] += sample; } else { short dacValue = (short)(((DacValue - 80) * channelVolume) / 80); if (Channels[5].LeftOutput) samples[pos] += dacValue; if (Channels[5].RightOutput) samples[pos + 1] += dacValue; } } pos += 2; } } public void DiscardSamples() { } public int MaxVolume { get; set; } // ==================================================================================== // Save States // ==================================================================================== public void SaveStateBinary(BinaryWriter writer) { writer.Write(TimerAPeriod); writer.Write(TimerBPeriod); writer.Write(TimerATripped); writer.Write(TimerBTripped); writer.Write(TimerAResetClock); writer.Write(TimerBResetClock); writer.Write(TimerALastReset); writer.Write(TimerBLastReset); writer.Write(TimerControl27); writer.Write(egDivisorCounter); writer.Write(egCycleCounter); writer.Write(PartSelect); writer.Write(RegisterSelect); writer.Write(DacEnable); writer.Write(DacValue); for (int i = 0; i < 6; i++) ChannelSaveStateBinary(writer, Channels[i]); } public void LoadStateBinary(BinaryReader reader) { TimerAPeriod = reader.ReadInt32(); TimerBPeriod = reader.ReadInt32(); TimerATripped = reader.ReadBoolean(); TimerBTripped = reader.ReadBoolean(); TimerAResetClock = reader.ReadInt32(); TimerBResetClock = reader.ReadInt32(); TimerALastReset = reader.ReadInt32(); TimerBLastReset = reader.ReadInt32(); TimerControl27 = reader.ReadByte(); egDivisorCounter = reader.ReadInt32(); egCycleCounter = reader.ReadInt32(); PartSelect = reader.ReadByte(); RegisterSelect = reader.ReadByte(); DacEnable = reader.ReadBoolean(); DacValue = reader.ReadByte(); for (int i = 0; i < 6; i++) ChannelLoadStateBinary(reader, Channels[i]); } void ChannelSaveStateBinary(BinaryWriter writer, Channel c) { // TODO reduce size of state via casting writer.Write(c.FrequencyNumber); writer.Write(c.Block); writer.Write(c.Feedback); writer.Write(c.Algorithm); writer.Write(c.SpecialMode); writer.Write(c.LeftOutput); writer.Write(c.RightOutput); writer.Write(c.AMS_AmplitudeModulationSensitivity); writer.Write(c.FMS_FrequencyModulationSensitivity); for (int i = 0; i < 4; i++) OperatorSaveStateBinary(writer, c.Operators[i]); } void ChannelLoadStateBinary(BinaryReader reader, Channel c) { c.FrequencyNumber = reader.ReadInt32(); c.Block = reader.ReadInt32(); c.Feedback = reader.ReadInt32(); c.Algorithm = reader.ReadInt32(); c.SpecialMode = reader.ReadBoolean(); c.LeftOutput = reader.ReadBoolean(); c.RightOutput = reader.ReadBoolean(); c.AMS_AmplitudeModulationSensitivity = reader.ReadInt32(); c.FMS_FrequencyModulationSensitivity = reader.ReadInt32(); for (int i = 0; i < 4; i++) OperatorLoadStateBinary(reader, c.Operators[i]); } void OperatorSaveStateBinary(BinaryWriter writer, Operator op) { // TODO, size of states could be shrunken by using casts. writer.Write(op.TL_TotalLevel); writer.Write(op.SL_SustainLevel); writer.Write(op.AR_AttackRate); writer.Write(op.DR_DecayRate); writer.Write(op.SR_SustainRate); writer.Write(op.RR_ReleaseRate); writer.Write(op.KS_KeyScale); writer.Write(op.SSG_EG); writer.Write(op.DT_Detune); writer.Write(op.MUL_Multiple); writer.Write(op.AM_AmplitudeModulation); writer.Write(op.FrequencyNumber); writer.Write(op.Block); writer.Write(op.KeyCode); writer.Write(op.Rks); writer.Write(op.PhaseCounter); writer.Write((byte)op.EnvelopeState); writer.Write(op.EgAttenuation); } void OperatorLoadStateBinary(BinaryReader reader, Operator op) { op.TL_TotalLevel = reader.ReadInt32(); op.SL_SustainLevel = reader.ReadInt32(); op.AR_AttackRate = reader.ReadInt32(); op.DR_DecayRate = reader.ReadInt32(); op.SR_SustainRate = reader.ReadInt32(); op.RR_ReleaseRate = reader.ReadInt32(); op.KS_KeyScale = reader.ReadInt32(); op.SSG_EG = reader.ReadInt32(); op.DT_Detune = reader.ReadInt32(); op.MUL_Multiple = reader.ReadInt32(); op.AM_AmplitudeModulation = reader.ReadBoolean(); op.FrequencyNumber = reader.ReadInt32(); op.Block = reader.ReadInt32(); op.KeyCode = reader.ReadInt32(); op.Rks = reader.ReadInt32(); op.PhaseCounter = reader.ReadInt32(); op.EnvelopeState = (EnvelopeState)Enum.ToObject(typeof(EnvelopeState), reader.ReadByte()); op.EgAttenuation = reader.ReadInt32(); } } }