1294 lines
44 KiB
C#
1294 lines
44 KiB
C#
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<QueuedCommand> commands = new Queue<QueuedCommand>();
|
|
|
|
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();
|
|
}
|
|
}
|
|
} |