/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * __ __ ________ ____ __ ____ _____ _______ _______ __ __ * * / /\ / /\ / _ _ /\ /___ \ / /\ /_ /\ /____ \ / ___ /\ / ___ /\ / /\ / /\ * * \ \_/ / // // // / / __\_/ /\ / / /_ \/ / / \___/ /\ ______ / /\_/ / // /\_/ / // / / / / / * * \_ / // // // / / .'____.' // /_/ /_ / / / /__ / / /_____/\ / / // / // _____/ // / / / / / * * / / // // // / / / /\___\.'/___ __/\ __/ /_/ ___\_/ / / \_____\/ / /_// / // /\____\// /_/__ / /_/__ * * /_/ //_//_//_/ / /______/\ \__/_/\_\//______/\/_____.' / /______/ //_/ / /______/\/______/\ * * \_\/ \_\\_\\_\/ \______\/ \_\/ \______\/\_____\.' \______\/ \_\/ \______\/\______\/ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * YM2413 FM Operator Type-LL (OPLL) emulation. Mainly based on the OPLL application manual, instrument presets from * * Mitsutaka Okazaki's EMU2413 source code. This is still in development! * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * Usage: Call OpllReset() to reset and initialise. Set OpllClock to the required clock speed and OpllSampleRate to * * the sample rate of the output (for VGMs, I'm assuming 44.1Khz). Then, for each sample you require, call OpllTick(). * * You have to then mix the audio yourself - I just sum up the 9 channels. The level of each channel can be extracted * * from the channel array: Level = OpllChannels[Index].Level; Write to registers using the function OpllWrite(). * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ using System; namespace VGM_Player { public partial class VgmPlayer { // Important variables: public static double OpllClock = 0.0d; // Clock rate of the YM2413 (usually ~3.5MHz) public static double OpllSampleRate = 44100.0d; // Rate at which we "tick" it. private static double TotalRunningTime = 0.0d; public static void OpllReset() { // Unpack the instrument data from the ROM to each instrument def. for (int i = 0; i < 19; ++i) { OpllInstruments[i] = new OpllPatch(); // Should replace some of these with "Bit()"... OpllInstruments[i].AM[0] = ((OpllInstrumentRom[i * 16 + 0] >> 7) & 1) != 0; OpllInstruments[i].AM[1] = ((OpllInstrumentRom[i * 16 + 1] >> 7) & 1) != 0; OpllInstruments[i].VIB[0] = ((OpllInstrumentRom[i * 16 + 0] >> 6) & 1) != 0; OpllInstruments[i].VIB[1] = ((OpllInstrumentRom[i * 16 + 1] >> 6) & 1) != 0; OpllInstruments[i].EG_TYP[0] = ((OpllInstrumentRom[i * 16 + 0] >> 5) & 1) != 0; OpllInstruments[i].EG_TYP[1] = ((OpllInstrumentRom[i * 16 + 1] >> 5) & 1) != 0; OpllInstruments[i].KSR[0] = ((OpllInstrumentRom[i * 16 + 0] >> 4) & 1) != 0; OpllInstruments[i].KSR[1] = ((OpllInstrumentRom[i * 16 + 1] >> 4) & 1) != 0; OpllInstruments[i].MUL[0] = (byte)(OpllInstrumentRom[i * 16 + 0] & 0xF); OpllInstruments[i].MUL[1] = (byte)(OpllInstrumentRom[i * 16 + 1] & 0xF); OpllInstruments[i].TL = (byte)(OpllInstrumentRom[i * 16 + 2] & 0x1F); } for (int i = 0; i < 9; i++) { OpllChannels[i] = new OpllChannel(); } // Wipe the registers to 0: for (byte i = 0; i < 0x39; ++i) { OpllWrite(i, 0x00); } } #region Instrument ROM // Taken from Mitsutaka Okazaki's emu2413 // At least, I *think* this is the instrument ROM. // For all I know, it could be anything, but sounds OK, even if it is entirely by accident. private static byte[] OpllInstrumentRom = { 0x49,0x4c,0x4c,0x32,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x61,0x61,0x1e,0x17,0xf0,0x7f,0x00,0x17,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x13,0x41,0x16,0x0e,0xfd,0xf4,0x23,0x23,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x03,0x01,0x9a,0x04,0xf3,0xf3,0x13,0xf3,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x11,0x61,0x0e,0x07,0xfa,0x64,0x70,0x17,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x22,0x21,0x1e,0x06,0xf0,0x76,0x00,0x28,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x21,0x22,0x16,0x05,0xf0,0x71,0x00,0x18,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x21,0x61,0x1d,0x07,0x82,0x80,0x17,0x17,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x23,0x21,0x2d,0x16,0x90,0x90,0x00,0x07,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x21,0x21,0x1b,0x06,0x64,0x65,0x10,0x17,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x21,0x21,0x0b,0x1a,0x85,0xa0,0x70,0x07,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x23,0x01,0x83,0x10,0xff,0xb4,0x10,0xf4,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x97,0xc1,0x20,0x07,0xff,0xf4,0x22,0x22,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x61,0x00,0x0c,0x05,0xc2,0xf6,0x40,0x44,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x01,0x01,0x56,0x03,0x94,0xc2,0x03,0x12,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x21,0x01,0x89,0x03,0xf1,0xe4,0xf0,0x23,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x07,0x21,0x14,0x00,0xee,0xf8,0xff,0xf8,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x01,0x31,0x00,0x00,0xf8,0xf7,0xf8,0xf7,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x25,0x11,0x00,0x00,0xf8,0xfa,0xf8,0x55,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00 }; #endregion private static OpllPatch[] OpllInstruments = new OpllPatch[19]; // 19 instruments private class OpllPatch { /// Amplitude Modulation. public bool[] AM = new bool[2]; /// Vibrato public bool[] VIB = new bool[2]; /// Multiplication factor. public int[] MUL = new int[2]; /// Switching between sustained tone and Percussive tone. public bool[] EG_TYP = new bool[2]; /// Key scale of rate. public bool[] KSR = new bool[2]; /// Key scaling public int[] KSL = new int[2]; /// Total Level public int TL = 0x00; /// Modulated wave rectified to half-wave. public bool DM = false; /// Carrier wave rectified to half-wave. public bool DC = false; /// Feedback FM modulation. public int FB = 0x00; /// Attack rate public int[] AR = new int[2]; /// Decay rate public int[] DR = new int[2]; /// Sustain level public int[] SL = new int[2]; /// Release rate public int[] RR = new int[2]; } public class OpllChannel { // Various properties of the channel... // The properties that require getters/setters are done that way so that in the event of // one of them changing, the phase increments are recalculated to reflect this new change. private int m_F_Number = 0; public int F_Number { get { return m_F_Number; } set { if (value != m_F_Number) { m_F_Number = value; RecalculatePhaseIncrement(0); RecalculatePhaseIncrement(1); } else { m_F_Number = value; } } } private int m_Block = 0; public int Block { get { return m_Block; } set { if (value != m_Block) { m_Block = value; RecalculatePhaseIncrement(0); RecalculatePhaseIncrement(1); } else { m_Block = value; } } } private int m_InstrumentIndex = 0; public int InstrumentIndex { get { return m_InstrumentIndex; } set { if (value != m_InstrumentIndex) { m_InstrumentIndex = value; RecalculatePhaseIncrement(0); RecalculatePhaseIncrement(1); } else { m_InstrumentIndex = value; } } } private OpllPatch CurrentInstrument { get { return OpllInstruments[InstrumentIndex]; } } public int Volume = 0; public bool KeyOn = false; public bool SustainOn = false; // Envelope stores the LINEAR envelope generator levels. public double[] Envelope = new double[2]; // C# get: "double x = OpllChannel.Level;" executes the code below. public double Level { get { if (KeyOn) { for (int i = 0; i < 2; i++) { if (CurrentInstrument.AM[i]) RecalculateEnvelope(i); } return DbToPcm(Volume * 3) * Envelope[1] * (ModulationIndices[CurrentInstrument.FB] + Sin(Phase[1] + Envelope[0] * Sin(Phase[0]))); } else { return 0.0d; } } } // Phase stores the total phase, phase increment the amount phase is altered by each sample. public double[] Phase = new double[2]; public double[] PhaseIncrement = new double[2]; // Call this if ANY factor that affects a phase increment changes. public void RecalculatePhaseIncrement(int IncrementIndex) { PhaseIncrement[IncrementIndex] = ((m_F_Number / ((262144.0d / (OpllClock / 72.0d)) / Blocks[m_Block])) * Multipliers[OpllInstruments[m_InstrumentIndex].MUL[IncrementIndex]]) / OpllSampleRate; RecalculateEnvelope(IncrementIndex); // If the F-number changes, so does the envelope (KSL) } public void RecalculateEnvelope(int EnvelopeIndex) { double NewLevel = 48.0d; // 48dB if (EnvelopeIndex == 0) { // Modulator NewLevel -= CurrentInstrument.TL * 0.75d; } if (CurrentInstrument.AM[EnvelopeIndex]) { // Drop off a little... NewLevel -= (Sin(3.7 * TotalRunningTime) / 2.0d); } if (NewLevel < 0.0d) NewLevel = 0.0d; Envelope[EnvelopeIndex] = DbToPcm(48.0d - NewLevel); } // Update the phases by one sample. public void Tick() { for (int i = 0; i < 2; ++i) { if (CurrentInstrument.VIB[i]) { Phase[i] += PhaseIncrement[i] * (1 + (Sin(6.4 * TotalRunningTime) / 128.0d)); } else { Phase[i] += PhaseIncrement[i]; } } } // Sine function based on a phase of whole waves [ Sin(1 phase) == Sin(360 degrees) == Sin(2pi radians) ] private double Sin(double Phase) { return Math.Sin(Phase * 2.0d * Math.PI); } private double[] Multipliers = // Multipliers (for the MUL factor) { 0.5d, 1.0d, 2.0d, 3.0d, 4.0d, 5.0d, 6.0d, 7.0d, 8.0d, 9.0d, 10.0d, 10.0d, 12.0d, 12.0d, 15.0d, 15.0d }; private double[] Blocks = // Block (octave multipliers) { 0.5d, 1.0d, 2.0d, 4.0d, 8.0d, 16.0d, 32.0d, 64.0d }; private double[] ModulationIndices = // "For the modulated wave of the first slot's feedback FM modulation." ??? { 0.0d, Math.PI / 16.0d, Math.PI / 8.0d, Math.PI / 4.0d, Math.PI / 2.0d, Math.PI, Math.PI * 2.0d, Math.PI * 4.0d }; private double DbToPcm(double Decibels) { return 1.0d / Math.Pow(10.0d, Decibels / 20.0d); } } public static OpllChannel[] OpllChannels = new OpllChannel[9]; // 9 different channels. public static void OpllTick() { foreach (OpllChannel C in OpllChannels) C.Tick(); TotalRunningTime += 1 / OpllSampleRate; } /// /// Write to a YM2413 register /// /// Register number to write to /// Value to load into the register public static void OpllWrite(byte Register, byte Value) { int ChannelNumber = Register & 0x0F; // In case we ned to use it. switch (Register & 0xF0) { case 0x00: // Setting up some part of the user-defined instrument. switch (Register) { case 0x00: case 0x01: // AM, VIB, EG-TYP, KSR and MUL. // The register is used as an index as there are two waves. OpllInstruments[0].MUL[Register] = Value & 0x0F; OpllInstruments[0].KSR[Register] = Bit(Value, 4); OpllInstruments[0].EG_TYP[Register] = Bit(Value, 5); OpllInstruments[0].VIB[Register] = Bit(Value, 6); OpllInstruments[0].AM[Register] = Bit(Value, 7); foreach (OpllChannel C in OpllChannels) { if (C.InstrumentIndex == 0) { C.RecalculatePhaseIncrement(Register); } } break; case 0x02: // KSL[0], TL. OpllInstruments[0].TL = Value & 0x3F; OpllInstruments[0].KSL[0] = (Value >> 6) & 0x3; break; case 0x03: // KSL[1], DC, DM, FB OpllInstruments[0].FB = Value & 0x7; OpllInstruments[0].DM = Bit(Value, 3); OpllInstruments[0].DC = Bit(Value, 4); OpllInstruments[0].KSL[1] = (Value >> 6) & 0x2; break; case 0x04: case 0x05: // AR, DR OpllInstruments[0].DR[Register - 0x4] = Value & 0x0F; OpllInstruments[0].AR[Register - 0x4] = (Value >> 4) & 0x0F; break; case 0x06: case 0x07: // SL, RR OpllInstruments[0].RR[Register - 0x6] = Value & 0x0F; OpllInstruments[0].SL[Register - 0x6] = (Value >> 4) & 0x0F; break; } break; case 0x10: // Setting the LSB of an F-number if (ChannelNumber < 9) { OpllChannels[ChannelNumber].F_Number &= 0x0100; // Clear the lower 8 bits. OpllChannels[ChannelNumber].F_Number |= Value; // Set the lower 8 bits. } break; case 0x20: // Setting the MSB of an F-number, block, key and sustain. if (ChannelNumber < 9) { OpllChannels[ChannelNumber].F_Number &= 0x00FF; // Clear the upper 9th bit. OpllChannels[ChannelNumber].F_Number |= ((Value & 1) << 8); // Set the 9th bit. OpllChannels[ChannelNumber].Block = (Value >> 1) & 0x7; // Set the block OpllChannels[ChannelNumber].KeyOn = Bit(Value, 4); OpllChannels[ChannelNumber].SustainOn = Bit(Value, 5); } break; case 0x30: // Setting the instrument and volume data. if (ChannelNumber < 9) { OpllChannels[ChannelNumber].Volume = Value & 0x0F; OpllChannels[ChannelNumber].InstrumentIndex = (Value >> 4) & 0x0F; } break; } } private static bool Bit(byte Value, int Index) { return (Value & (1 << Index)) != 0; } } }