/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* __ __ ________ ____ __ ____ _____ _______ _______ __ __ *
* / /\ / /\ / _ _ /\ /___ \ / /\ /_ /\ /____ \ / ___ /\ / ___ /\ / /\ / /\ *
* \ \_/ / // // // / / __\_/ /\ / / /_ \/ / / \___/ /\ ______ / /\_/ / // /\_/ / // / / / / / *
* \_ / // // // / / .'____.' // /_/ /_ / / / /__ / / /_____/\ / / // / // _____/ // / / / / / *
* / / // // // / / / /\___\.'/___ __/\ __/ /_/ ___\_/ / / \_____\/ / /_// / // /\____\// /_/__ / /_/__ *
* /_/ //_//_//_/ / /______/\ \__/_/\_\//______/\/_____.' / /______/ //_/ / /______/\/______/\ *
* \_\/ \_\\_\\_\/ \______\/ \_\/ \______\/\_____\.' \______\/ \_\/ \______\/\______\/ *
* *
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* 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;
}
}
}