From 32bc79be06f6eda2ea8001203c5bfbbbe646bda8 Mon Sep 17 00:00:00 2001 From: beirich Date: Tue, 7 Aug 2012 05:48:30 +0000 Subject: [PATCH] ym2612 mothaaaaafukkkkaaaaaaa --- BizHawk.Emulation/BizHawk.Emulation.csproj | 4 - BizHawk.Emulation/Sound/YM2612.Channel.cs | 80 -- BizHawk.Emulation/Sound/YM2612.IO.cs | 231 ----- BizHawk.Emulation/Sound/YM2612.Operator.cs | 90 -- BizHawk.Emulation/Sound/YM2612.Timers.cs | 104 -- BizHawk.Emulation/Sound/YM2612.cs | 1015 +++++++++++++++++++- 6 files changed, 970 insertions(+), 554 deletions(-) delete mode 100644 BizHawk.Emulation/Sound/YM2612.Channel.cs delete mode 100644 BizHawk.Emulation/Sound/YM2612.IO.cs delete mode 100644 BizHawk.Emulation/Sound/YM2612.Operator.cs delete mode 100644 BizHawk.Emulation/Sound/YM2612.Timers.cs diff --git a/BizHawk.Emulation/BizHawk.Emulation.csproj b/BizHawk.Emulation/BizHawk.Emulation.csproj index e08c7f161c..b926856155 100644 --- a/BizHawk.Emulation/BizHawk.Emulation.csproj +++ b/BizHawk.Emulation/BizHawk.Emulation.csproj @@ -354,9 +354,6 @@ - - - @@ -390,7 +387,6 @@ - diff --git a/BizHawk.Emulation/Sound/YM2612.Channel.cs b/BizHawk.Emulation/Sound/YM2612.Channel.cs deleted file mode 100644 index 4f66d2d38c..0000000000 --- a/BizHawk.Emulation/Sound/YM2612.Channel.cs +++ /dev/null @@ -1,80 +0,0 @@ -using System; - -namespace BizHawk.Emulation.Sound -{ - public partial class YM2612 - { - public sealed class Channel - { - public readonly Operator[] Operators; - - public int FrequencyNumber; - public int Block; - public int Feedback; - public int Algorithm; - - public bool SpecialMode; // Enables separate frequency for each operator, available on CH3 and CH6 only - // TODO. CSM. Pg6 details CSM mode. - public bool LeftOutput; - public bool RightOutput; - - public int AMS_AmplitudeModulationSensitivity; - public int FMS_FrequencyModulationSensitivity; - - public Channel() - { - Operators = new Operator[4]; - Operators[0] = new Operator(); - Operators[1] = new Operator(); - Operators[2] = new Operator(); - Operators[3] = new Operator(); - - LeftOutput = true; // Revenge of Shinobi does not output DAC if these arent initialized ?? - RightOutput = true; - } - - public void WriteFrequencyLow(byte value) - { - FrequencyNumber &= 0x700; - FrequencyNumber |= value; - - // TODO maybe its 4-frequency mode - // TODO is this right, only reflect change when writing LSB? - Operators[0].FrequencyNumber = FrequencyNumber; - Operators[1].FrequencyNumber = FrequencyNumber; - Operators[2].FrequencyNumber = FrequencyNumber; - Operators[3].FrequencyNumber = FrequencyNumber; - } - - public void WriteFrequencyHigh(byte value) - { - FrequencyNumber &= 0x0FF; - FrequencyNumber |= (value & 15) << 8; - Block = (value >> 3) & 7; - } - - public void Write_Feedback_Algorithm(byte value) - { - Algorithm = value & 7; - Feedback = (value >> 3) & 7; - } - - public void Write_Stereo_LfoSensitivy(byte value) - { - FMS_FrequencyModulationSensitivity = value & 3; - AMS_AmplitudeModulationSensitivity = (value >> 3) & 7; - RightOutput = (value & 0x40) != 0; - LeftOutput = (value & 0x80) != 0; - } - - //---------------------- - //|Mode| Behaviour | - //|----|---------------| - //| 00 | Normal | - //| 01 | Special | - //| 10 | Special + CSM | - //| 11 | Special | - //---------------------- - } - } -} \ No newline at end of file diff --git a/BizHawk.Emulation/Sound/YM2612.IO.cs b/BizHawk.Emulation/Sound/YM2612.IO.cs deleted file mode 100644 index 8260bb23ec..0000000000 --- a/BizHawk.Emulation/Sound/YM2612.IO.cs +++ /dev/null @@ -1,231 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace BizHawk.Emulation.Sound -{ - // We process TIMER writes immediately when writes come in. - // All other writes are queued up with a timestamp, so that we - // can sift through them when we're rendering audio for the frame. - - public partial class YM2612 - { - byte PartSelect; - byte RegisterSelect; - bool DacEnable; - byte DacValue; - - Queue commands = new Queue(); - - 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); } // don't return on this one; we process immediately AND enqueue command for port $27. - } - - 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) - { - 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: Console.WriteLine("Operator Key On/Off Ctrl {0:X2}", 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 - - default: - int chan, oper; - GetChanOpP1(register, out chan, out oper); - if (chan < 0) break; // abort if invalid port number - switch (register & 0xF0) - { - case 0x30: Channels[chan].Operators[oper].Write_MUL_DT1(value); break; - case 0x40: Channels[chan].Operators[oper].Write_TL(value); break; - case 0x50: Channels[chan].Operators[oper].Write_AR_KS(value); break; - case 0x60: Channels[chan].Operators[oper].Write_DR_AM(value); break; - case 0x70: Channels[chan].Operators[oper].Write_SR(value); break; - case 0x80: Channels[chan].Operators[oper].Write_RR_SL(value); break; - case 0x90: Channels[chan].Operators[oper].Write_SSGEG(value); break; - case 0xA0: - case 0xB0: WriteHighBlockP1(register, value); break; - } - break; - - // "In MAME OPN emulation code register pairs for multi-frequency mode are A6A2, ACA8, AEAA, ADA9". - - //D7 - operator, which frequency defined by A6A2 - //D6 - .. ACA8 - //D5 - .. AEAA - //D4 - .. ADA9 - //Where D7=op4, D6=op3, D5=op2, and D4=op1. That matches the YM2608 document. At least that's confirmed then. - - // PG4 has some info on frquency calculations - } - } - - void Part2_WriteRegister(byte register, byte value) - { - // NOTE. Only first bank has multi-frequency CSM/Special mode. This mode can't work on CH6. - - int chan, oper; - GetChanOpP2(register, out chan, out oper); - if (chan < 0) return; // abort if invalid port number - switch (register & 0xF0) - { - case 0x30: Channels[chan].Operators[oper].Write_MUL_DT1(value); break; - case 0x40: Channels[chan].Operators[oper].Write_TL(value); break; - case 0x50: Channels[chan].Operators[oper].Write_AR_KS(value); break; - case 0x60: Channels[chan].Operators[oper].Write_DR_AM(value); break; - case 0x70: Channels[chan].Operators[oper].Write_SR(value); break; - case 0x80: Channels[chan].Operators[oper].Write_RR_SL(value); break; - case 0x90: Channels[chan].Operators[oper].Write_SSGEG(value); break; - case 0xA0: - case 0xB0: WriteHighBlockP2(register, value); break; - } - } - - void WriteHighBlockP1(byte register, byte value) - { - switch (register) - { - case 0xA0: Channels[0].WriteFrequencyLow(value); break; - case 0xA1: Channels[1].WriteFrequencyLow(value); break; - case 0xA2: Channels[2].WriteFrequencyLow(value); break; - - case 0xA4: Channels[0].WriteFrequencyHigh(value); break; - case 0xA5: Channels[1].WriteFrequencyHigh(value); break; - case 0xA6: Channels[2].WriteFrequencyHigh(value); break; - - case 0xB0: Channels[0].Write_Feedback_Algorithm(value); break; - case 0xB1: Channels[1].Write_Feedback_Algorithm(value); break; - case 0xB2: Channels[2].Write_Feedback_Algorithm(value); break; - - case 0xB4: Channels[0].Write_Stereo_LfoSensitivy(value); break; - case 0xB5: Channels[1].Write_Stereo_LfoSensitivy(value); break; - case 0xB6: Channels[2].Write_Stereo_LfoSensitivy(value); break; - } - } - - void WriteHighBlockP2(byte register, byte value) - { - switch (register) - { - case 0xA0: Channels[3].WriteFrequencyLow(value); break; - case 0xA1: Channels[4].WriteFrequencyLow(value); break; - case 0xA2: Channels[5].WriteFrequencyLow(value); break; - - case 0xA4: Channels[3].WriteFrequencyHigh(value); break; - case 0xA5: Channels[4].WriteFrequencyHigh(value); break; - case 0xA6: Channels[5].WriteFrequencyHigh(value); break; - - case 0xB0: Channels[3].Write_Feedback_Algorithm(value); break; - case 0xB1: Channels[4].Write_Feedback_Algorithm(value); break; - case 0xB2: Channels[5].Write_Feedback_Algorithm(value); break; - - case 0xB4: Channels[3].Write_Stereo_LfoSensitivy(value); break; - case 0xB5: Channels[4].Write_Stereo_LfoSensitivy(value); break; - case 0xB6: Channels[5].Write_Stereo_LfoSensitivy(value); break; - } - } - - public class QueuedCommand - { - public byte Part; - public byte Register; - public byte Data; - public int Clock; - } - } -} \ No newline at end of file diff --git a/BizHawk.Emulation/Sound/YM2612.Operator.cs b/BizHawk.Emulation/Sound/YM2612.Operator.cs deleted file mode 100644 index 53f94d0f2e..0000000000 --- a/BizHawk.Emulation/Sound/YM2612.Operator.cs +++ /dev/null @@ -1,90 +0,0 @@ -using System; - -namespace BizHawk.Emulation.Sound -{ - public partial class YM2612 - { - public sealed class Operator - { - // External Settings - public int TL_TotalLevel; - public int SL_SustainLevel; - public int AR_AttackRate; - public int DR_DecayRate; - public int SR_SustainRate; - public int RR_ReleaseRate; - public int KS_KeyScale; - public int SSG_EG; - - public int DT1_Detune; - public int MUL_Multiple; - - public bool AM_AmplitudeModulation; - - public int FrequencyNumber; - public int Block; - - // Internal State - public int PhaseCounter; - - // I/O - public void Write_MUL_DT1(byte value) - { - MUL_Multiple = value & 15; - DT1_Detune = (value >> 4) & 7; - } - - public void Write_TL(byte value) - { - TL_TotalLevel = value & 127; - } - - public void Write_AR_KS(byte value) - { - AR_AttackRate = value & 31; - KS_KeyScale = value >> 6; - } - - public void Write_DR_AM(byte value) - { - DR_DecayRate = value & 31; - AM_AmplitudeModulation = (value & 128) != 0; - } - - public void Write_SR(byte value) - { - SR_SustainRate = value & 31; - } - - public void Write_RR_SL(byte value) - { - RR_ReleaseRate = value & 15; - SL_SustainLevel = value >> 4; - } - - public void Write_SSGEG(byte value) - { - SSG_EG = value & 15; - } - - public void UpdateFrequency(int frequencyNumber, int block) - { - FrequencyNumber = frequencyNumber; - Block = block; - } - } - } - //TODO "the shape of the waves of the envelope changes in a exponential when attacking it, and it changes in the straight line at other rates." - - // pg 8, read it - // pg 11, detailed overview of how operator works. - // pg 12, detailed description of phase generator. - - //TL Total Level 7 bits - //SL Sustain Level 4 bits - //AR Attack Rate 5 bits - //DR Decay Rate 5 bits - //SR Sustain Rate 5 bits - //RR Release Rate 4 bits - //SSG-EG SSG-EG Mode 4 bits -} \ No newline at end of file diff --git a/BizHawk.Emulation/Sound/YM2612.Timers.cs b/BizHawk.Emulation/Sound/YM2612.Timers.cs deleted file mode 100644 index 2de1cd1b39..0000000000 --- a/BizHawk.Emulation/Sound/YM2612.Timers.cs +++ /dev/null @@ -1,104 +0,0 @@ -using System; - -namespace BizHawk.Emulation.Sound -{ - // 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) - // 3,579,545 Z80 cycles / sec (15 MCLK divisor) - - // YM2612 is fed by EXT CLOCK: 7,670,454 ECLK / sec (NTSC) - // (Same clock on 68000) 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. - - public partial class YM2612 - { - const float timerAZ80Factor = 67.2f; - const float timerBZ80Factor = 1075.2f; - - 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; - } - } -} \ No newline at end of file diff --git a/BizHawk.Emulation/Sound/YM2612.cs b/BizHawk.Emulation/Sound/YM2612.cs index 021c59acc4..5bf9d071e8 100644 --- a/BizHawk.Emulation/Sound/YM2612.cs +++ b/BizHawk.Emulation/Sound/YM2612.cs @@ -1,32 +1,48 @@ using System; using System.Diagnostics; +using System.Collections.Generic; namespace BizHawk.Emulation.Sound { - public sealed partial class YM2612 : ISoundProvider + // ====================================================================== + // 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: 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 + + public sealed class YM2612 : ISoundProvider { - public readonly Channel[] Channels; - - int frameStartClock; - int frameEndClock; + public readonly Channel[] Channels = { new Channel(), new Channel(), new Channel(), new Channel(), new Channel(), new Channel() }; public YM2612() { - Channels = new Channel[6]; - Channels[0] = new Channel(); - Channels[1] = new Channel(); - Channels[2] = new Channel(); - Channels[3] = new Channel(); - Channels[4] = new Channel(); - Channels[5] = new Channel(); - InitTimers(); + MaxVolume = short.MaxValue; + //Channels[2].Operators[3].Debug = true; } - public void Reset() - { - throw new Exception("something is resetting the ym2612"); - } + // ==================================================================================== + + int frameStartClock; + int frameEndClock; public void BeginFrame(int clock) { @@ -43,10 +59,926 @@ namespace BizHawk.Emulation.Sound frameEndClock = clock; } - public void DiscardSamples() { } - public int MaxVolume { get; set; } + // ==================================================================================== + // YM2612 I/O + // ==================================================================================== - public void GetSamples(short[] samples) + public class QueuedCommand + { + public byte Part; + public byte Register; + public byte Data; + public int Clock; + } + + byte PartSelect; + byte RegisterSelect; + bool DacEnable; + byte DacValue; + + 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_DT1(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_DT1(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? + + channel.Operators[0].FrequencyNumber = channel.FrequencyNumber; channel.Operators[0].Block = channel.Block; + channel.Operators[1].FrequencyNumber = channel.FrequencyNumber; channel.Operators[1].Block = channel.Block; + channel.Operators[2].FrequencyNumber = channel.FrequencyNumber; channel.Operators[2].Block = channel.Block; + channel.Operators[3].FrequencyNumber = channel.FrequencyNumber; channel.Operators[3].Block = channel.Block; + } + + static void WriteFrequencyHigh(Channel channel, byte value) + { + channel.FrequencyNumber &= 0x0FF; + channel.FrequencyNumber |= (value & 15) << 8; + channel.Block = (value >> 3) & 7; + } + + 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_DT1(int chan, int op, byte value) + { + if (chan < 0) return; + var oper = Channels[chan].Operators[op]; + oper.MUL_Multiple = value & 15; + oper.DT1_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; + } + + 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; + } + + // ==================================================================================== + // Envelope Generator + // ==================================================================================== + + #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 + }; + #endregion + + 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.Debug) Console.WriteLine("Switch to " + op.EnvelopeState); + } + + if (op.EnvelopeState == EnvelopeState.Decay && op.EgAttenuation >= op.Normalized10BitSL) + { + // Switch to Sustain phase + op.EnvelopeState = EnvelopeState.Sustain; + if (op.Debug) Console.WriteLine("Switch to 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 KeyScale. + rate = Math.Min((rate * 2) + op.KS_KeyScale, 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 -= attenuationAdjustment * (((op.EgAttenuation) / 16) + 1); + else // One of the decay phases + op.EgAttenuation += attenuationAdjustment; + + //if (op.Debug) Console.WriteLine("Attn {0} Adj {1} Cycle {2} Rate {3} Shift {4} Offset {5}", op.EgAttenuation, op.AdjustedEGOutput, egCycleCounter, rate, shiftValue, updateCycleOffset); + } + } + + static void KeyOn(Operator op) + { + op.PhaseCounter = 0; // Reset Phase Generator + //Console.WriteLine("key on"); + 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; + // It's notable, though unsurprising, that we do not reset attenuation to max attenuation during a standard KeyOn. + + } + } + + static void KeyOff(Operator op) + { + // TODO I feel like it should do more than this. But maybe it doesn't. + 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; + + + int operator_output = OperatorCalc(phase10, op.AdjustedEGOutput); + //if (op.Debug) Log.Error("YM2612","SLOT4 eg_out {0} state _ TL {1} volume {2} SL {3} op_out {4} phase {5}", op.AdjustedEGOutput, op.Normalized10BitTL, op.EgAttenuation, op.Normalized10BitSL, operator_output, phase10); + + return operator_output; + } + + 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; + } + + // TODO: Detune + + // 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 DT1_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 + + // 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; } } // TODO no idea if this is correct, it's probably not. + public int AdjustedEGOutput { get { return Math.Min(egAttenuation + Normalized10BitTL, 1023); } } + + public bool Debug = false; + } + + 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. + // TODO this is not good resampling code. I'm not rightly sure it's even correct as linear interpolation goes. + int offset = 0; + for (int i = 0; i < numStereoSamples; i++) + { + int nativeOffset = ((i * ntscOutputRate) / 44100) * 2; + samples[offset++] += nativeSamples[nativeOffset++]; // left + samples[offset++] += nativeSamples[nativeOffset]; // right + } + } + + void GetSamplesNative(short[] samples) { int elapsedCycles = frameEndClock - frameStartClock; int start = 0; @@ -65,38 +997,31 @@ namespace BizHawk.Emulation.Sound { int channelVolume = MaxVolume / 6; - for (int i=0; i