BizHawk/BizHawk.Emulation.Cores/Sound/YM2612.cs

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();
}
}
}