From c35e4e45176fee0ec73a3b0f6566bce8b3d4abdc Mon Sep 17 00:00:00 2001 From: Maximilien Noal Date: Sun, 8 Sep 2024 19:01:24 +0200 Subject: [PATCH 1/5] chore: remove current opl3 chip emu Signed-off-by: Maximilien Noal --- .../Devices/Sound/Ymf262Emu/AdsrCalculator.cs | 475 ------------------ .../Sound/Ymf262Emu/Channels/BassDrum.cs | 27 - .../Sound/Ymf262Emu/Channels/Channel.cs | 129 ----- .../Sound/Ymf262Emu/Channels/Channel2.cs | 112 ----- .../Sound/Ymf262Emu/Channels/Channel4.cs | 185 ------- .../Sound/Ymf262Emu/Channels/NullChannel.cs | 44 -- .../Sound/Ymf262Emu/Channels/RhythmChannel.cs | 52 -- .../Devices/Sound/Ymf262Emu/FmSynthesizer.cs | 473 ----------------- .../Devices/Sound/Ymf262Emu/Intrinsics.cs | 35 -- .../Devices/Sound/Ymf262Emu/OPL3FM.cs | 7 +- .../Sound/Ymf262Emu/Operators/HighHat.cs | 34 -- .../Sound/Ymf262Emu/Operators/Operator.cs | 245 --------- .../Sound/Ymf262Emu/Operators/SnareDrum.cs | 52 -- .../Sound/Ymf262Emu/Operators/TopCymbal.cs | 63 --- .../Sound/Ymf262Emu/VibratoGenerator.cs | 46 -- 15 files changed, 4 insertions(+), 1975 deletions(-) delete mode 100644 src/Spice86.Core/Emulator/Devices/Sound/Ymf262Emu/AdsrCalculator.cs delete mode 100644 src/Spice86.Core/Emulator/Devices/Sound/Ymf262Emu/Channels/BassDrum.cs delete mode 100644 src/Spice86.Core/Emulator/Devices/Sound/Ymf262Emu/Channels/Channel.cs delete mode 100644 src/Spice86.Core/Emulator/Devices/Sound/Ymf262Emu/Channels/Channel2.cs delete mode 100644 src/Spice86.Core/Emulator/Devices/Sound/Ymf262Emu/Channels/Channel4.cs delete mode 100644 src/Spice86.Core/Emulator/Devices/Sound/Ymf262Emu/Channels/NullChannel.cs delete mode 100644 src/Spice86.Core/Emulator/Devices/Sound/Ymf262Emu/Channels/RhythmChannel.cs delete mode 100644 src/Spice86.Core/Emulator/Devices/Sound/Ymf262Emu/FmSynthesizer.cs delete mode 100644 src/Spice86.Core/Emulator/Devices/Sound/Ymf262Emu/Intrinsics.cs delete mode 100644 src/Spice86.Core/Emulator/Devices/Sound/Ymf262Emu/Operators/HighHat.cs delete mode 100644 src/Spice86.Core/Emulator/Devices/Sound/Ymf262Emu/Operators/Operator.cs delete mode 100644 src/Spice86.Core/Emulator/Devices/Sound/Ymf262Emu/Operators/SnareDrum.cs delete mode 100644 src/Spice86.Core/Emulator/Devices/Sound/Ymf262Emu/Operators/TopCymbal.cs delete mode 100644 src/Spice86.Core/Emulator/Devices/Sound/Ymf262Emu/VibratoGenerator.cs diff --git a/src/Spice86.Core/Emulator/Devices/Sound/Ymf262Emu/AdsrCalculator.cs b/src/Spice86.Core/Emulator/Devices/Sound/Ymf262Emu/AdsrCalculator.cs deleted file mode 100644 index dc7023678c..0000000000 --- a/src/Spice86.Core/Emulator/Devices/Sound/Ymf262Emu/AdsrCalculator.cs +++ /dev/null @@ -1,475 +0,0 @@ -namespace Spice86.Core.Emulator.Devices.Sound.Ymf262Emu; - -/// -/// Generates an ADSR envelope for waveform data. -/// -internal sealed class AdsrCalculator -{ - private double _xAttackIncrement, _xMinimumInAttack; - private double _dBdecayIncrement; - private double _dBreleaseIncrement; - private double _attenuation, _totalLevel, _sustainLevel; - private double _x, _envelope; - private readonly FmSynthesizer _opl; - - /// - /// Initializes a new instance of the AdsrCalculator class. - /// - /// FmSynthesizer instance which owns the AdsrCalculator. - public AdsrCalculator(FmSynthesizer opl) - { - _x = DecibelsToX(-96); - _envelope = -96; - _opl = opl; - } - - /// - /// Gets or sets the current ADSR state. - /// - public AdsrState State { get; set; } - - /// - /// Gets or sets the current sustain level. - /// - public int SustainLevel - { - get - { - if (_sustainLevel == -93) { - return 0x0F; - } else { - return (int)_sustainLevel / -3; - } - } - set - { - if (value == 0x0F) { - _sustainLevel = -93; - } else { - _sustainLevel = -3 * value; - } - } - } - - /// - /// Gets or sets the total output level. - /// - public int TotalLevel - { - get => (int)(_totalLevel / -0.75); - set => _totalLevel = value * -0.75; - } - - public void SetAtennuation(int frequencyNumber, int block, int ksl) - { - int hi4Bits = (frequencyNumber >> 6) & 0x0F; - int index = (hi4Bits * 8) + block; - switch (ksl) - { - case 0: - _attenuation = 0; - break; - case 1: - // ~3 dB/Octave - _attenuation = Ksl3dBtable[index]; - break; - case 2: - // ~1.5 dB/Octave - _attenuation = Ksl3dBtable[index] / 2; - break; - case 3: - // ~6 dB/Octave - _attenuation = Ksl3dBtable[index] * 2; - break; - } - } - - public void SetActualAttackRate(int attackRate, int ksr, int keyScaleNumber) - { - // According to the YMF278B manual's OPL3 section, the attack curve is exponential, - // with a dynamic range from -96 dB to 0 dB and a resolution of 0.1875 dB - // per level. - // - // This method sets an attack increment and attack minimum value - // that creates a exponential dB curve with 'period0to100' seconds in length - // and 'period10to90' seconds between 10% and 90% of the curve total level. - int actualAttackRate = CalculateActualRate(attackRate, ksr, keyScaleNumber); - ReadOnlySpan attackTimeValues = GetAttackTimeValues(actualAttackRate); - - int period0To100InSamples = (int)(attackTimeValues[0] * _opl.SampleRate); - int period10To90InSamples = (int)(attackTimeValues[1] * _opl.SampleRate); - - // The x increment is dictated by the period between 10% and 90%: - _xAttackIncrement = _opl.CalculateIncrement(PercentageToX10, PercentageToX90, attackTimeValues[1]); - // Discover how many samples are still from the top. - // It cannot reach 0 dB, since x is a logarithmic parameter and would be - // negative infinity. So we will use -0.1875 dB as the resolution - // maximum. - // - // percentageToX(0.9) + samplesToTheTop*xAttackIncrement = dBToX(-0.1875); -> - // samplesToTheTop = (dBtoX(-0.1875) - percentageToX(0.9)) / xAttackIncrement); -> - // period10to100InSamples = period10to90InSamples + samplesToTheTop; -> - int period10To100InSamples = (int)(period10To90InSamples + ((DecibelsToXN01875 - PercentageToX90) / _xAttackIncrement)); - // Discover the minimum x that, through the attackIncrement value, keeps - // the 10%-90% period, and reaches 0 dB at the total period: - _xMinimumInAttack = PercentageToX10 - ((period0To100InSamples - period10To100InSamples) * _xAttackIncrement); - } - - private static readonly double DecibelsToXN01875 = DecibelsToX(-0.1875); - private static readonly double PercentageToX10 = PercentageToX(0.1); - private static readonly double PercentageToX90 = PercentageToX(0.9); - - public void SetActualDecayRate(int decayRate, int ksr, int keyScaleNumber) - { - int actualDecayRate = CalculateActualRate(decayRate, ksr, keyScaleNumber); - double period10To90InSeconds = DecayAndReleaseTimeValuesTable[actualDecayRate]; - // Differently from the attack curve, the decay/release curve is linear. - // The dB increment is dictated by the period between 10% and 90%: - _dBdecayIncrement = _opl.CalculateIncrement(PercentageToDb(0.1), PercentageToDb(0.9), period10To90InSeconds); - } - - public void SetActualReleaseRate(int releaseRate, int ksr, int keyScaleNumber) - { - int actualReleaseRate = CalculateActualRate(releaseRate, ksr, keyScaleNumber); - double period10To90InSeconds = DecayAndReleaseTimeValuesTable[actualReleaseRate]; - _dBreleaseIncrement = _opl.CalculateIncrement(PercentageToDb(0.1), PercentageToDb(0.9), period10To90InSeconds); - } - - public double GetEnvelope(int egt, int am) - { - // The datasheets attenuation values - // must be halved to match the real OPL3 output. - double envelopeSustainLevel = _sustainLevel / 2; - double envelopeTremolo = _opl.GetTremoloValue(_opl.Dam, _opl.TremoloIndex) / 2; - double envelopeAttenuation = _attenuation / 2; - double envelopeTotalLevel = _totalLevel / 2; - - const double envelopeMinimum = -96; - const double envelopeResolution = 0.1875; - - double outputEnvelope; - // - // Envelope Generation - // - switch (State) - { - case AdsrState.Attack: - // Since the attack is exponential, it will never reach 0 dB, so - // we´ll work with the next to maximum in the envelope resolution. - if (_envelope < -envelopeResolution && _xAttackIncrement != -double.PositiveInfinity) - { - // The attack is exponential. - _envelope = -Math.Pow(2, _x); - _x += _xAttackIncrement; - break; - } - else - { - // It is needed here to explicitly set envelope = 0, since - // only the attack can have a period of - // 0 seconds and produce a infinity envelope increment. - _envelope = 0; - State = AdsrState.Decay; - } - goto case AdsrState.Decay; - - case AdsrState.Decay: - // The decay and release are linear. - if (_envelope > envelopeSustainLevel) - { - _envelope -= _dBdecayIncrement; - break; - } - else - { - State = AdsrState.Sustain; - } - goto case AdsrState.Sustain; - - case AdsrState.Sustain: - // The Sustain stage is mantained all the time of the Key ON, - // even if we are in non-sustaining mode. - // This is necessary because, if the key is still pressed, we can - // change back and forth the state of EGT, and it will release and - // hold again accordingly. - if (egt == 1) - { - break; - } - else - { - if (_envelope > envelopeMinimum) { - _envelope -= _dBreleaseIncrement; - } else { - State = AdsrState.Off; - } - } - break; - case AdsrState.Release: - // If we have Key OFF, only here we are in the Release stage. - // Now, we can turn EGT back and forth and it will have no effect,i.e., - // it will release inexorably to the Off stage. - if (_envelope > envelopeMinimum) { - _envelope -= _dBreleaseIncrement; - } else { - State = AdsrState.Off; - } - - break; - } - - // Ongoing original envelope - outputEnvelope = _envelope; - - //Tremolo - if (am == 1) { - outputEnvelope += envelopeTremolo; - } - - //Attenuation - outputEnvelope += envelopeAttenuation; - - //Total Level - outputEnvelope += envelopeTotalLevel; - - // The envelope has a resolution of 0.1875 dB: - return ((int)(outputEnvelope / envelopeResolution)) * envelopeResolution; - } - - public void KeyOn() - { - double xCurrent = Intrinsics.Log2(-_envelope); - _x = Math.Min(xCurrent, _xMinimumInAttack); - State = AdsrState.Attack; - } - - public void KeyOff() - { - if (State != AdsrState.Off) { - State = AdsrState.Release; - } - } - - private int CalculateActualRate(int rate, int ksr, int keyScaleNumber) - { - int rof = (int)((uint)keyScaleNumber >> (ksr << 1)); - - int actualRate = (rate * 4) + rof; - - // If, as an example at the maximum, rate is 15 and the rate offset is 15, - // the value would - // be 75, but the maximum allowed is 63: - if (actualRate > 63) { - actualRate = 63; - } - - return actualRate; - } - - private static double DecibelsToX(double dB) => Intrinsics.Log2(-dB); - private static double PercentageToDb(double percentage) => Math.Log10(percentage) * 10.0; - private static double PercentageToX(double percentage) => DecibelsToX(PercentageToDb(percentage)); - - #region Lookup Tables - - private static ReadOnlySpan GetAttackTimeValues(int i) => new(AttackTimeValuesTable, i * 2, 2); - - // These attack periods in milliseconds were taken from the YMF278B manual. - // The attack actual rates range from 0 to 63, with different data for - // 0%-100% and for 10%-90%: - private static readonly double[] AttackTimeValuesTable = - { - double.PositiveInfinity, double.PositiveInfinity, - double.PositiveInfinity, double.PositiveInfinity, - double.PositiveInfinity, double.PositiveInfinity, - double.PositiveInfinity, double.PositiveInfinity, - 2.82624, 1.48275, - 2.2528, 1.15507, - 1.88416, 0.99123, - 1.59744, 0.86835, - 1.41312, 0.74138, - 1.1264, 0.57754, - 0.94208, 0.49562, - 0.79872, 0.43418, - 0.70656, 0.37069, - 0.5632, 0.28877, - 0.47104, 0.24781, - 0.39936, 0.21709, - - 0.35328, 0.18534, - 0.2816, 0.14438, - 0.23552, 0.1239, - 0.19968, 0.10854, - 0.17676, 0.09267, - 0.1408, 0.07219, - 0.11776, 0.06195, - 0.09984, 0.05427, - 0.08832, 0.04634, - 0.0704, 0.0361, - 0.05888, 0.03098, - 0.04992, 0.02714, - 0.04416, 0.02317, - 0.0352, 0.01805, - 0.02944, 0.01549, - 0.02496, 0.01357, - - 0.02208, 0.01158, - 0.0176, 0.00902, - 0.01472, 0.00774, - 0.01248, 0.00678, - 0.01104, 0.00579, - 0.0088, 0.00451, - 0.00736, 0.00387, - 0.00624, 0.00339, - 0.00552, 0.0029, - 0.0044, 0.00226, - 0.00368, 0.00194, - 0.00312, 0.0017, - 0.00276, 0.00145, - 0.0022, 0.00113, - 0.00184, 0.00097, - 0.00156, 0.00085, - - 0.0014, 0.00073, - 0.00112, 0.00061, - 0.00092, 0.00049, - 0.0008, 0.00043, - 0.0007, 0.00037, - 0.00056, 0.00031, - 0.00046, 0.00026, - 0.00042, 0.00022, - 0.00038, 0.00019, - 0.0003, 0.00014, - 0.00024, 0.00011, - 0.0002, 0.00011, - 0.00, 0.00, - 0.00, 0.00, - 0.00, 0.00, - 0.00, 0.00 - }; - - // These decay and release periods in milliseconds were taken from the YMF278B manual. - // The rate index range from 0 to 63, with different data for - // 0%-100% and for 10%-90%: - private static readonly double[] DecayAndReleaseTimeValuesTable = - { - double.PositiveInfinity, - double.PositiveInfinity, - double.PositiveInfinity, - double.PositiveInfinity, - 8.21248, - 6.57408, - 5.50912, - 4.73088, - 4.10624, - 3.28704, - 2.75456, - 2.36544, - 2.05312, - 1.64352, - 1.37728, - 1.18272, - - 1.02656, - 0.82176, - 0.68864, - 0.59136, - 0.51328, - 0.41088, - 0.34434, - 0.29568, - 0.25664, - 0.20544, - 0.17216, - 0.14784, - 0.12832, - 0.10272, - 0.08608, - 0.07392, - - 0.06416, - 0.05136, - 0.04304, - 0.03696, - 0.03208, - 0.02568, - 0.02152, - 0.01848, - 0.01604, - 0.01284, - 0.01076, - 0.00924, - 0.00802, - 0.00642, - 0.00538, - 0.00462, - - 0.00402, - 0.00322, - 0.00268, - 0.00232, - 0.00202, - 0.00162, - 0.00135, - 0.00115, - 0.00101, - 0.00081, - 0.00069, - 0.00058, - 0.00051, - 0.00051, - 0.00051, - 0.00051 - }; - - private static readonly double[] Ksl3dBtable = - { - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0 , 0 , 0, -3, -6, -9, - 0, 0, 0, 0, -3, -6, -9, -12, - 0, 0, 0, -1.875, -4.875, -7.875, -10.875, -13.875, - - 0, 0, 0, -3, -6, -9, -12, -15, - 0, 0, -1.125, -4.125, -7.125, -10.125, -13.125, -16.125, - 0, 0, -1.875, -4.875, -7.875, -10.875, -13.875, -16.875, - 0, 0, -2.625, -5.625, -8.625, -11.625, -14.625, -17.625, - - 0, 0, -3, -6, -9, -12, -15, -18, - 0, -0.750, -3.750, -6.750, -9.750, -12.750, -15.750, -18.750, - 0, -1.125, -4.125, -7.125, -10.125, -13.125, -16.125, -19.125, - 0, -1.500, -4.500, -7.500, -10.500, -13.500, -16.500, -19.500, - - 0, -1.875, -4.875, -7.875, -10.875, -13.875, -16.875, -19.875, - 0, -2.250, -5.250, -8.250, -11.250, -14.250, -17.250, -20.250, - 0, -2.625, -5.625, -8.625, -11.625, -14.625, -17.625, -20.625, - 0, -3, -6, -9, -12, -15, -18, -21 - }; - #endregion -} - -/// -/// Specifies the current state of an ADSR envelope. -/// -internal enum AdsrState -{ - /// - /// The channel is off. - /// - Off, - /// - /// The envelope is in the attack phase. - /// - Attack, - /// - /// The envelope is in the decay phase. - /// - Decay, - /// - /// The envelope is in the sustain phase. - /// - Sustain, - /// - /// The envelope is in the release phase. - /// - Release -} diff --git a/src/Spice86.Core/Emulator/Devices/Sound/Ymf262Emu/Channels/BassDrum.cs b/src/Spice86.Core/Emulator/Devices/Sound/Ymf262Emu/Channels/BassDrum.cs deleted file mode 100644 index 44a076ee48..0000000000 --- a/src/Spice86.Core/Emulator/Devices/Sound/Ymf262Emu/Channels/BassDrum.cs +++ /dev/null @@ -1,27 +0,0 @@ -namespace Spice86.Core.Emulator.Devices.Sound.Ymf262Emu.Channels; - -using Spice86.Core.Emulator.Devices.Sound.Ymf262Emu.Operators; - -internal sealed class BassDrum : Channel2 { - public BassDrum(FmSynthesizer opl) - : base(6, new Operator(0x10, opl), new Operator(0x13, opl), opl) { - } - - public override void GetChannelOutput(Span output) { - // Bass Drum ignores first operator, when it is in series. - if (Cnt == 1) { - if (Op1 != null) { - Op1.Ar = 0; - } - } - - base.GetChannelOutput(output); - } - - // Key ON and OFF are unused in rhythm channels. - public override void KeyOn() { - } - - public override void KeyOff() { - } -} diff --git a/src/Spice86.Core/Emulator/Devices/Sound/Ymf262Emu/Channels/Channel.cs b/src/Spice86.Core/Emulator/Devices/Sound/Ymf262Emu/Channels/Channel.cs deleted file mode 100644 index c7b34991f7..0000000000 --- a/src/Spice86.Core/Emulator/Devices/Sound/Ymf262Emu/Channels/Channel.cs +++ /dev/null @@ -1,129 +0,0 @@ -namespace Spice86.Core.Emulator.Devices.Sound.Ymf262Emu.Channels; -using System; -using System.Runtime.CompilerServices; - -/// -/// Emulates an OPL channel. -/// -internal abstract class Channel -{ - public const int Chd1Chc1Chb1Cha1Fb3Cnt1Offset = 0xC0; - - // Factor to convert between normalized amplitude to normalized - // radians. The amplitude maximum is equivalent to 8*Pi radians. - protected const double ToPhase = 4; - protected const int _2_KON1_BLOCK3_FNUMH2_Offset = 0xB0; - protected const int Fnuml8Offset = 0xA0; - - public readonly int ChannelBaseAddress; - protected readonly FmSynthesizer Opl; - protected double Feedback0; - protected double Feedback1; - protected int Fnuml, Fnumh, Kon, Block, Cha, Chb, Chc, Chd, Fb, Cnt; - - // Feedback rate in fractions of 2*Pi, normalized to (0,1): - // 0, Pi/16, Pi/8, Pi/4, Pi/2, Pi, 2*Pi, 4*Pi turns to be: - protected static readonly double[] FeedbackTable = { 0, 1 / 32d, 1 / 16d, 1 / 8d, 1 / 4d, 1 / 2d, 1, 2 }; - - /// - /// Initializes a new instance of the Channel class. - /// - /// Base address of the channel's registers. - /// FmSynthesizer instance which owns the channel. - public Channel(int baseAddress, FmSynthesizer opl) - { - ChannelBaseAddress = baseAddress; - Opl = opl; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void GetFourChannelOutput(double channelOutput, Span output) - { - if (Opl.IsOpl3Mode == 0) - { - output.Fill(channelOutput); - } - else - { - output[0] = (Cha == 1) ? channelOutput : 0; - output[1] = (Chb == 1) ? channelOutput : 0; - output[2] = (Chc == 1) ? channelOutput : 0; - output[3] = (Chd == 1) ? channelOutput : 0; - } - } - - /// - /// Returns an array containing the channel's output values. - /// - /// Array containing the channel's output values. - public abstract void GetChannelOutput(Span output); - - /// - /// Activates channel output. - /// - public abstract void KeyOn(); - - /// - /// Disables channel output. - /// - public abstract void KeyOff(); - - /// - /// Updates the state of all of the operators in the channel. - /// - public abstract void UpdateOperators(); - - public void Update_2_KON1_BLOCK3_FNUMH2() - { - int _2_kon1_block3_fnumh2 = Opl.Registers[ChannelBaseAddress + _2_KON1_BLOCK3_FNUMH2_Offset]; - - // Frequency Number (hi-register) and Block. These two registers, together with fnuml, - // sets the Channel´s base frequency; - Block = (_2_kon1_block3_fnumh2 & 0x1C) >> 2; - Fnumh = _2_kon1_block3_fnumh2 & 0x03; - UpdateOperators(); - - // Key On. If changed, calls Channel.keyOn() / keyOff(). - int newKon = (_2_kon1_block3_fnumh2 & 0x20) >> 5; - if (newKon != Kon) - { - if (newKon == 1) { - KeyOn(); - } else { - KeyOff(); - } - - Kon = newKon; - } - } - - public void Update_FNUML8() - { - int fnuml8 = Opl.Registers[ChannelBaseAddress + Fnuml8Offset]; - // Frequency Number, low register. - Fnuml = fnuml8 & 0xFF; - UpdateOperators(); - } - - public void Update_CHD1_CHC1_CHB1_CHA1_FB3_CNT1() - { - int chd1Chc1Chb1Cha1Fb3Cnt1 = Opl.Registers[ChannelBaseAddress + Chd1Chc1Chb1Cha1Fb3Cnt1Offset]; - Chd = (chd1Chc1Chb1Cha1Fb3Cnt1 & 0x80) >> 7; - Chc = (chd1Chc1Chb1Cha1Fb3Cnt1 & 0x40) >> 6; - Chb = (chd1Chc1Chb1Cha1Fb3Cnt1 & 0x20) >> 5; - Cha = (chd1Chc1Chb1Cha1Fb3Cnt1 & 0x10) >> 4; - Fb = (chd1Chc1Chb1Cha1Fb3Cnt1 & 0x0E) >> 1; - Cnt = chd1Chc1Chb1Cha1Fb3Cnt1 & 0x01; - UpdateOperators(); - } - - /// - /// Updates the state of the channel. - /// - public void UpdateChannel() - { - Update_2_KON1_BLOCK3_FNUMH2(); - Update_FNUML8(); - Update_CHD1_CHC1_CHB1_CHA1_FB3_CNT1(); - } -} diff --git a/src/Spice86.Core/Emulator/Devices/Sound/Ymf262Emu/Channels/Channel2.cs b/src/Spice86.Core/Emulator/Devices/Sound/Ymf262Emu/Channels/Channel2.cs deleted file mode 100644 index 515c7ecb23..0000000000 --- a/src/Spice86.Core/Emulator/Devices/Sound/Ymf262Emu/Channels/Channel2.cs +++ /dev/null @@ -1,112 +0,0 @@ -namespace Spice86.Core.Emulator.Devices.Sound.Ymf262Emu.Channels; -using System; - -using Spice86.Core.Emulator.Devices.Sound.Ymf262Emu.Operators; - -/// -/// Emulates a 2-operator OPL channel. -/// -internal class Channel2 : Channel -{ - public readonly Operator? Op1; - public readonly Operator? Op2; - - /// - /// Initializes a new instance of the Channel2 class. - /// - /// Base address of the channel's registers. - /// First operator in the channel. - /// Second operator in the channel. - /// FmSynthesizer instance which owns the channel. - public Channel2(int baseAddress, Operator? o1, Operator? o2, FmSynthesizer opl) - : base(baseAddress, opl) - { - Op1 = o1; - Op2 = o2; - } - - /// - /// Returns an array containing the channel's output values. - /// - /// Array containing the channel's output values. - public override void GetChannelOutput(Span output) - { - double channelOutput = 0, op1Output = 0; - double feedbackOutput = (Feedback0 + Feedback1) / 2; - - switch (Cnt) - { - // CNT = 0, the operators are in series, with the first in feedback. - case 0: - if (Op2 != null && Op2.EnvelopeGenerator.State == AdsrState.Off) - { - GetFourChannelOutput(0, output); - return; - } - - if (Op1 != null) { - op1Output = Op1.GetOperatorOutput(feedbackOutput); - } - - if (Op2 != null) { - channelOutput = Op2.GetOperatorOutput(op1Output * ToPhase); - } - - break; - - // CNT = 1, the operators are in parallel, with the first in feedback. - case 1: - if (Op2 != null && Op1 is {EnvelopeGenerator.State: AdsrState.Off} && Op2.EnvelopeGenerator.State == AdsrState.Off) - { - GetFourChannelOutput(0, output); - return; - } - - if (Op1 != null) { - op1Output = Op1.GetOperatorOutput(feedbackOutput); - } - - if (Op2 != null) { - double op2Output = Op2.GetOperatorOutput(Operator.NoModulator); - channelOutput = (op1Output + op2Output) / 2; - } - - break; - } - - Feedback0 = Feedback1; - Feedback1 = (op1Output * FeedbackTable[Fb]) % 1; - GetFourChannelOutput(channelOutput, output); - } - - /// - /// Activates channel output. - /// - public override void KeyOn() - { - Op1?.KeyOn(); - Op2?.KeyOn(); - Feedback0 = 0; - Feedback1 = 0; - } - - /// - /// Disables channel output. - /// - public override void KeyOff() - { - Op1?.KeyOff(); - Op2?.KeyOff(); - } - - /// - /// Updates the state of all of the operators in the channel. - /// - public override void UpdateOperators() - { - int keyScaleNumber = (Block * 2) + ((Fnumh >> Opl.Nts) & 0x01); - int fNumber = (Fnumh << 8) | Fnuml; - Op1?.UpdateOperator(keyScaleNumber, fNumber, Block); - Op2?.UpdateOperator(keyScaleNumber, fNumber, Block); - } -} diff --git a/src/Spice86.Core/Emulator/Devices/Sound/Ymf262Emu/Channels/Channel4.cs b/src/Spice86.Core/Emulator/Devices/Sound/Ymf262Emu/Channels/Channel4.cs deleted file mode 100644 index 2f20bb9fbf..0000000000 --- a/src/Spice86.Core/Emulator/Devices/Sound/Ymf262Emu/Channels/Channel4.cs +++ /dev/null @@ -1,185 +0,0 @@ -namespace Spice86.Core.Emulator.Devices.Sound.Ymf262Emu.Channels; -using System; - -using Spice86.Core.Emulator.Devices.Sound.Ymf262Emu.Operators; - -/// -/// Emulates a 4-operator OPL channel. -/// -internal sealed class Channel4 : Channel { - private readonly Operator? _op1; - private readonly Operator? _op2; - private readonly Operator? _op3; - private readonly Operator? _op4; - - /// - /// Initializes a new instance of the Channel4 class. - /// - /// Base address of the channel's registers. - /// First operator in the channel. - /// Second operator in the channel. - /// Third operator in the channel. - /// Fourth operator in the channel. - /// FmSynthesizer instance which owns the channel. - public Channel4(int baseAddress, Operator? o1, Operator? o2, Operator? o3, Operator? o4, FmSynthesizer opl) - : base(baseAddress, opl) { - _op1 = o1; - _op2 = o2; - _op3 = o3; - _op4 = o4; - } - - /// - /// Returns an array containing the channel's output values. - /// - /// Array containing the channel's output values. - public override void GetChannelOutput(Span output) { - double channelOutput = 0, op1Output = 0; - int secondChannelBaseAddress = ChannelBaseAddress + 3; - int secondCnt = Opl.Registers[secondChannelBaseAddress + Chd1Chc1Chb1Cha1Fb3Cnt1Offset] & 0x1; - int cnt4Op = (Cnt << 1) | secondCnt; - double feedbackOutput = (Feedback0 + Feedback1) / 2; - double op2Output = 0; - double op3Output = 0; - double op4Output = 0; - - switch (cnt4Op) { - case 0: - if (_op4 is {EnvelopeGenerator.State: AdsrState.Off}) { - GetFourChannelOutput(0, output); - return; - } - - if (_op1 != null) { - op1Output = _op1.GetOperatorOutput(feedbackOutput); - } - - if (_op2 != null) { - op2Output = _op2.GetOperatorOutput(op1Output * ToPhase); - } - - if (_op3 != null) { - op3Output = _op3.GetOperatorOutput(op2Output * ToPhase); - } - - if (_op4 != null) { - channelOutput = _op4.GetOperatorOutput(op3Output * ToPhase); - } - - break; - - case 1: - if (_op4 != null && _op2 is {EnvelopeGenerator.State: AdsrState.Off} && _op4.EnvelopeGenerator.State == AdsrState.Off) { - GetFourChannelOutput(0, output); - return; - } - - if (_op1 != null) { - op1Output = _op1.GetOperatorOutput(feedbackOutput); - } - - if (_op2 != null) { - op2Output = _op2.GetOperatorOutput(op1Output * ToPhase); - } - - if (_op3 != null) { - op3Output = _op3.GetOperatorOutput(Operator.NoModulator); - } - - if (_op4 != null) { - op4Output = _op4.GetOperatorOutput(op3Output * ToPhase); - } - - channelOutput = (op2Output + op4Output) / 2; - break; - - case 2: - if (_op1 != null && _op4 != null && _op1.EnvelopeGenerator.State == AdsrState.Off && _op4.EnvelopeGenerator.State == AdsrState.Off) { - GetFourChannelOutput(0, output); - return; - } - - if (_op1 != null) { - op1Output = _op1.GetOperatorOutput(feedbackOutput); - } - - if (_op2 != null) { - op2Output = _op2.GetOperatorOutput(Operator.NoModulator); - } - - if (_op3 != null) { - op3Output = _op3.GetOperatorOutput(op2Output * ToPhase); - } - - if (_op4 != null) { - op4Output = _op4.GetOperatorOutput(op3Output * ToPhase); - } - - channelOutput = (op1Output + op4Output) / 2; - break; - - case 3: - if (_op3 != null && _op4 != null && _op1 is {EnvelopeGenerator.State: AdsrState.Off} && _op3.EnvelopeGenerator.State == AdsrState.Off && _op4.EnvelopeGenerator.State == AdsrState.Off) { - GetFourChannelOutput(0, output); - return; - } - - if (_op1 != null) { - op1Output = _op1.GetOperatorOutput(feedbackOutput); - } - - if (_op2 != null) { - op2Output = _op2.GetOperatorOutput(Operator.NoModulator); - } - - if (_op3 != null) { - op3Output = _op3.GetOperatorOutput(op2Output * ToPhase); - } - - if (_op4 != null) { - op4Output = _op4.GetOperatorOutput(Operator.NoModulator); - } - - channelOutput = (op1Output + op3Output + op4Output) / 3; - break; - } - - Feedback0 = Feedback1; - Feedback1 = (op1Output * FeedbackTable[Fb]) % 1; - - GetFourChannelOutput(channelOutput, output); - } - - /// - /// Activates channel output. - /// - public override void KeyOn() { - _op1?.KeyOn(); - _op2?.KeyOn(); - _op3?.KeyOn(); - _op4?.KeyOn(); - Feedback0 = Feedback1 = 0; - } - - /// - /// Disables channel output. - /// - public override void KeyOff() { - _op1?.KeyOff(); - _op2?.KeyOff(); - _op3?.KeyOff(); - _op4?.KeyOff(); - } - - /// - /// Updates the state of all of the operators in the channel. - /// - public override void UpdateOperators() { - int keyScaleNumber = (Block * 2) + ((Fnumh >> Opl.Nts) & 0x01); - int fNumber = (Fnumh << 8) | Fnuml; - _op1?.UpdateOperator(keyScaleNumber, fNumber, Block); - _op2?.UpdateOperator(keyScaleNumber, fNumber, Block); - _op3?.UpdateOperator(keyScaleNumber, fNumber, Block); - _op4?.UpdateOperator(keyScaleNumber, fNumber, Block); - } -} diff --git a/src/Spice86.Core/Emulator/Devices/Sound/Ymf262Emu/Channels/NullChannel.cs b/src/Spice86.Core/Emulator/Devices/Sound/Ymf262Emu/Channels/NullChannel.cs deleted file mode 100644 index 847f0ae382..0000000000 --- a/src/Spice86.Core/Emulator/Devices/Sound/Ymf262Emu/Channels/NullChannel.cs +++ /dev/null @@ -1,44 +0,0 @@ -namespace Spice86.Core.Emulator.Devices.Sound.Ymf262Emu.Channels; -using System; - -/// -/// Placeholder OPL channel that generates no output. -/// -internal sealed class NullChannel : Channel -{ - /// - /// Initializes a new instance of the NullChannel class. - /// - /// FmSynthesizer instance which owns the channel. - public NullChannel(FmSynthesizer opl) - : base(0, opl) - { - } - - /// - /// Returns an array containing the channel's output values. - /// - /// Array containing the channel's output values. - public override void GetChannelOutput(Span output) => output.Clear(); - - /// - /// Activates channel output. - /// - public override void KeyOn() - { - } - - /// - /// Disables channel output. - /// - public override void KeyOff() - { - } - - /// - /// Updates the state of all of the operators in the channel. - /// - public override void UpdateOperators() - { - } -} diff --git a/src/Spice86.Core/Emulator/Devices/Sound/Ymf262Emu/Channels/RhythmChannel.cs b/src/Spice86.Core/Emulator/Devices/Sound/Ymf262Emu/Channels/RhythmChannel.cs deleted file mode 100644 index dba3a155f3..0000000000 --- a/src/Spice86.Core/Emulator/Devices/Sound/Ymf262Emu/Channels/RhythmChannel.cs +++ /dev/null @@ -1,52 +0,0 @@ -namespace Spice86.Core.Emulator.Devices.Sound.Ymf262Emu.Channels; -using System; - -using Spice86.Core.Emulator.Devices.Sound.Ymf262Emu.Operators; - -/// -/// Emulates a 2-operator rhythm OPL channel. -/// -internal sealed class RhythmChannel : Channel2 { - /// - /// Initializes a new instance of the RhythmChannel class. - /// - /// Base address of the channel's registers. - /// First operator in the channel. - /// Second operator in the channel. - /// FmSynthesizer instance which owns the channel. - public RhythmChannel(int baseAddress, Operator? o1, Operator? o2, FmSynthesizer opl) - : base(baseAddress, o1, o2, opl) { - } - - /// - /// Returns an array containing the channel's output values. - /// - /// Array containing the channel's output values. - public override void GetChannelOutput(Span output) { - if (Op1 == null) { - return; - } - - double op1Output = Op1.GetOperatorOutput(Operator.NoModulator); - if (Op2 == null) { - return; - } - - double op2Output = Op2.GetOperatorOutput(Operator.NoModulator); - double channelOutput = (op1Output + op2Output) / 2; - - GetFourChannelOutput(channelOutput, output); - } - - /// - /// Activates channel output. - /// - public override void KeyOn() { - } - - /// - /// Disables channel output. - /// - public override void KeyOff() { - } -} diff --git a/src/Spice86.Core/Emulator/Devices/Sound/Ymf262Emu/FmSynthesizer.cs b/src/Spice86.Core/Emulator/Devices/Sound/Ymf262Emu/FmSynthesizer.cs deleted file mode 100644 index 78889f5dd0..0000000000 --- a/src/Spice86.Core/Emulator/Devices/Sound/Ymf262Emu/FmSynthesizer.cs +++ /dev/null @@ -1,473 +0,0 @@ -namespace Spice86.Core.Emulator.Devices.Sound.Ymf262Emu; - -using Spice86.Core.Emulator.Devices.Sound.Ymf262Emu.Channels; -using Spice86.Core.Emulator.Devices.Sound.Ymf262Emu.Operators; - -/// -/// Emulates a YMF262 OPL3 device. -/// -public sealed class FmSynthesizer { - private const double TremoloDepth0 = -1; - private const double TremoloDepth1 = -4.8; - private readonly double _tremoloIncrement0; - private readonly double _tremoloIncrement1; - private readonly int _tremoloTableLength; - - internal readonly int[] Registers = new int[0x200]; - internal readonly HighHat? HighHatOperator; - private readonly SnareDrum? SnareDrumOperator; - private readonly Operator? TomTomOperator; - internal readonly TopCymbal? TopCymbalOperator; - internal int Nts, Dam, Dvb, Ryt, Bd, Sd, Tom, Tc, Hh, IsOpl3Mode, Connectionsel; - internal int VibratoIndex, TremoloIndex; - - private const double TremoloFrequency = 3.7; - private const int _1_NTS1_6_Offset = 0x08; - private const int Dam1Dvb1Ryt1Bd1Sd1Tom1Tc1Hh1Offset = 0xBD; - private const int _7_NEW1_Offset = 0x105; - private const int _2_CONNECTIONSEL6_Offset = 0x104; - - // The YMF262 has 36 operators. - private readonly Operator?[,] _operators = new Operator[2, 0x20]; - // The YMF262 has 3 4-op channels in each array. - private readonly Channel4[,] _channels4Op = new Channel4[2, 3]; - private readonly Channel[,] _channels = new Channel[2, 9]; - private readonly BassDrum _bassDrumChannel; - private readonly RhythmChannel _highHatSnareDrumChannel; - private readonly RhythmChannel _tomTomTopCymbalChannel; - private Operator? _highHatOperatorInNonRhythmMode; - private Operator? _snareDrumOperatorInNonRhythmMode; - private Operator? _tomTomOperatorInNonRhythmMode; - private Operator? _topCymbalOperatorInNonRhythmMode; - - // The YMF262 has 18 2-op channels. - // Each 2-op channel can be at a serial or parallel operator configuration. - private static readonly Channel2[,] Channels2Op = new Channel2[2, 9]; - - /// - /// Initializes a new instance of the class. - /// - /// Sample rate in Hz of the generated waveform data. - public FmSynthesizer(int sampleRate = 44100) { - if (sampleRate < 1000) { - throw new ArgumentOutOfRangeException(nameof(sampleRate)); - } - - SampleRate = sampleRate; - - _tremoloTableLength = (int)(sampleRate / TremoloFrequency); - _tremoloIncrement0 = CalculateIncrement(TremoloDepth0, 0, 1 / (2 * TremoloFrequency)); - _tremoloIncrement1 = CalculateIncrement(TremoloDepth1, 0, 1 / (2 * TremoloFrequency)); - - InitializeOperators(); - InitializeChannels2Op(); - InitializeChannels4Op(); - InitializeChannels(); - HighHatOperator = new HighHat(this); - TomTomOperator = new Operator(0x12, this); - TopCymbalOperator = new TopCymbal(this); - _bassDrumChannel = new BassDrum(this); - SnareDrumOperator = new SnareDrum(this); - _highHatSnareDrumChannel = new RhythmChannel(7, HighHatOperator, SnareDrumOperator, this); - _tomTomTopCymbalChannel = new RhythmChannel(8, TomTomOperator, TopCymbalOperator, this); - } - - /// - /// Gets the sample rate of the output waveform data in Hz. - /// - public int SampleRate { get; } - - /// - /// Fills with 16-bit mono samples. - /// - /// Buffer to fill with 16-bit waveform data. - public void GetData(Span buffer) { - for (int i = 0; i < buffer.Length; i++) { - buffer[i] = (short)(GetNextSample() * 32767); - } - } - /// - /// Fills with 32-bit mono samples. - /// - /// Buffer to fill with 32-bit waveform data. - public void GetData(Span buffer) { - for (int i = 0; i < buffer.Length; i++) { - buffer[i] = (float)GetNextSample(); - } - } - - /// - /// Writes a value to one of the emulated device's registers. - /// - /// Register array (may be 0 or 1). - /// Register address in the range 0-0x200. - /// Register value. - public void SetRegisterValue(int array, int address, int value) { - // The OPL3 has two registers arrays, each with adresses ranging - // from 0x00 to 0xF5. - // This emulator uses one array, with the two original register arrays - // starting at 0x00 and at 0x100. - int registerAddress = (array << 8) | address; - // If the address is out of the OPL3 memory map, returns. - if (registerAddress is < 0 or >= 0x200) { - return; - } - - Registers[registerAddress] = value; - switch (address & 0xE0) { - // The first 3 bits masking gives the type of the register by using its base address: - // 0x00, 0x20, 0x40, 0x60, 0x80, 0xA0, 0xC0, 0xE0 - // When it is needed, we further separate the register type inside each base address, - // which is the case of 0x00 and 0xA0. - - // Through out this emulator we will use the same name convention to - // reference a byte with several bit registers. - // The name of each bit register will be followed by the number of bits - // it occupies inside the byte. - // Numbers without accompanying names are unused bits. - case 0x00: - // Unique registers for the entire OPL3: - if (array == 1) { - if (address == 0x04) { - Update_2_CONNECTIONSEL6(); - } else if (address == 0x05) { - Update_7_NEW1(); - } - } - else if (address == 0x08) { - Update_1_NTS1_6(); - } - break; - - case 0xA0: - // 0xBD is a control register for the entire OPL3: - if (address == 0xBD) { - if (array == 0) { - Update_DAM1_DVB1_RYT1_BD1_SD1_TOM1_TC1_HH1(); - } - - break; - } - // Registers for each channel are in A0-A8, B0-B8, C0-C8, in both register arrays. - // 0xB0...0xB8 keeps kon,block,fnum(h) for each channel. - if ((address & 0xF0) == 0xB0 && address <= 0xB8) { - // If the address is in the second register array, adds 9 to the channel number. - // The channel number is given by the last four bits, like in A0,...,A8. - _channels[array, address & 0x0F].Update_2_KON1_BLOCK3_FNUMH2(); - break; - } - // 0xA0...0xA8 keeps fnum(l) for each channel. - if ((address & 0xF0) == 0xA0 && address <= 0xA8) { - _channels[array, address & 0x0F].Update_FNUML8(); - } - - break; - // 0xC0...0xC8 keeps cha,chb,chc,chd,fb,cnt for each channel: - case 0xC0: - if (address <= 0xC8) { - _channels[array, address & 0x0F].Update_CHD1_CHC1_CHB1_CHA1_FB3_CNT1(); - } - - break; - - // Registers for each of the 36 Operators: - default: - int operatorOffset = address & 0x1F; - - switch (address & 0xE0) { - // 0x20...0x35 keeps am,vib,egt,ksr,mult for each operator: - case 0x20: - _operators[array, operatorOffset]?.Update_AM1_VIB1_EGT1_KSR1_MULT4(); - break; - // 0x40...0x55 keeps ksl,tl for each operator: - case 0x40: - _operators[array, operatorOffset]?.Update_KSL2_TL6(); - break; - // 0x60...0x75 keeps ar,dr for each operator: - case 0x60: - _operators[array, operatorOffset]?.Update_AR4_DR4(); - break; - // 0x80...0x95 keeps sl,rr for each operator: - case 0x80: - _operators[array, operatorOffset]?.Update_SL4_RR4(); - break; - // 0xE0...0xF5 keeps ws for each operator: - case 0xE0: - _operators[array, operatorOffset]?.Update_5_WS3(); - break; - } - break; - } - } - - internal double CalculateIncrement(double begin, double end, double period) => (end - begin) / SampleRate * (1 / period); - - private double GetNextSample() { - Span channelOutput = stackalloc double[4]; - - Span outputBuffer = stackalloc double[4] { 0, 0, 0, 0 }; - - // If IsOpl3Mode = 0, use OPL2 mode with 9 channels. If IsOpl3Mode = 1, use OPL3 18 channels; - for (int array = 0; array < (IsOpl3Mode + 1); array++) { - for (int channelNumber = 0; channelNumber < 9; channelNumber++) { - // Reads output from each OPL3 channel, and accumulates it in the output buffer: - _channels[array, channelNumber].GetChannelOutput(channelOutput); - for (int i = 0; i < channelOutput.Length; i++) { - outputBuffer[i] += channelOutput[i]; - } - } - } - - Span output = stackalloc double[4]; - - const double ratio = 1.0 / 18.0; - - // Normalizes the output buffer after all channels have been added, - // with a maximum of 18 channels, - // and multiplies it to get the 16 bit signed output. - for (int i = 0; i < 4; i++) { - output[i] = (float)(outputBuffer[i] * ratio); - } - - // Advances the OPL3-wide vibrato index, which is used by - // PhaseGenerator.getPhase() in each Operator. - VibratoIndex++; - if (VibratoIndex >= VibratoGenerator.Length) { - VibratoIndex = 0; - } - // Advances the OPL3-wide tremolo index, which is used by - // EnvelopeGenerator.getEnvelope() in each Operator. - TremoloIndex++; - if (TremoloIndex >= _tremoloTableLength) { - TremoloIndex = 0; - } - - return (float)(output[0] + output[1] + output[2] + output[3]); - } - - internal double GetTremoloValue(int damValue, int i) { - if (i < _tremoloTableLength / 2) { - if (damValue == 0) { - return TremoloDepth0 + (_tremoloIncrement0 * i); - } else { - return TremoloDepth1 + (_tremoloIncrement1 * i); - } - } - else { - if (damValue == 0) { - return -_tremoloIncrement0 * i; - } else { - return -_tremoloIncrement1 * i; - } - } - } - - private void InitializeOperators() { - // The YMF262 has 36 operators: - for (int array = 0; array < 2; array++) { - for (int group = 0; group <= 0x10; group += 8) { - for (int offset = 0; offset < 6; offset++) { - int baseAddress = (array << 8) | (group + offset); - _operators[array, group + offset] = new Operator(baseAddress, this); - } - } - } - - // Save operators when they are in non-rhythm mode: - // Channel 7: - _highHatOperatorInNonRhythmMode = _operators[0, 0x11]; - _snareDrumOperatorInNonRhythmMode = _operators[0, 0x14]; - // Channel 8: - _tomTomOperatorInNonRhythmMode = _operators[0, 0x12]; - _topCymbalOperatorInNonRhythmMode = _operators[0, 0x15]; - } - - private void InitializeChannels2Op() { - // The YMF262 has 18 2-op channels. - // Each 2-op channel can be at a serial or parallel operator configuration: - for (int array = 0; array < 2; array++) { - for (int channelNumber = 0; channelNumber < 3; channelNumber++) { - int baseAddress = (array << 8) | channelNumber; - // Channels 1, 2, 3 -> Operator offsets 0x0,0x3; 0x1,0x4; 0x2,0x5 - Channels2Op[array, channelNumber] = new Channel2(baseAddress, _operators[array, channelNumber], _operators[array, channelNumber + 0x3], this); - // Channels 4, 5, 6 -> Operator offsets 0x8,0xB; 0x9,0xC; 0xA,0xD - Channels2Op[array, channelNumber + 3] = new Channel2(baseAddress + 3, _operators[array, channelNumber + 0x8], _operators[array, channelNumber + 0xB], this); - // Channels 7, 8, 9 -> Operators 0x10,0x13; 0x11,0x14; 0x12,0x15 - Channels2Op[array, channelNumber + 6] = new Channel2(baseAddress + 6, _operators[array, channelNumber + 0x10], _operators[array, channelNumber + 0x13], this); - } - } - } - private void InitializeChannels4Op() { - // The YMF262 has 3 4-op channels in each array: - for (int array = 0; array < 2; array++) { - for (int channelNumber = 0; channelNumber < 3; channelNumber++) { - int baseAddress = (array << 8) | channelNumber; - // Channels 1, 2, 3 -> Operators 0x0,0x3,0x8,0xB; 0x1,0x4,0x9,0xC; 0x2,0x5,0xA,0xD; - _channels4Op[array, channelNumber] = new Channel4(baseAddress, _operators[array, channelNumber], _operators[array, channelNumber + 0x3], _operators[array, channelNumber + 0x8], _operators[array, channelNumber + 0xB], this); - } - } - } - - private void InitializeChannels() { - // Channel is an abstract class that can be a 2-op, 4-op, rhythm or disabled channel, - // depending on the OPL3 configuration at the time. - // channels[] inits as a 2-op serial channel array: - for (int array = 0; array < 2; array++) { - for (int i = 0; i < 9; i++) { - _channels[array, i] = Channels2Op[array, i]; - } - } - } - - private void Update_1_NTS1_6() { - int _1_nts1_6 = Registers[_1_NTS1_6_Offset]; - // Note Selection. This register is used in Channel.updateOperators() implementations, - // to calculate the channel´s Key Scale Number. - // The value of the actual envelope rate follows the value of - // OPL3.nts,Operator.keyScaleNumber and Operator.ksr - Nts = (_1_nts1_6 & 0x40) >> 6; - } - - private void Update_DAM1_DVB1_RYT1_BD1_SD1_TOM1_TC1_HH1() { - int dam1Dvb1Ryt1Bd1Sd1Tom1Tc1Hh1 = Registers[Dam1Dvb1Ryt1Bd1Sd1Tom1Tc1Hh1Offset]; - // Depth of amplitude. This register is used in EnvelopeGenerator.getEnvelope(); - Dam = (dam1Dvb1Ryt1Bd1Sd1Tom1Tc1Hh1 & 0x80) >> 7; - - // Depth of vibrato. This register is used in PhaseGenerator.getPhase(); - Dvb = (dam1Dvb1Ryt1Bd1Sd1Tom1Tc1Hh1 & 0x40) >> 6; - - int newRyt = (dam1Dvb1Ryt1Bd1Sd1Tom1Tc1Hh1 & 0x20) >> 5; - if (newRyt != Ryt) { - Ryt = newRyt; - SetRhythmMode(); - } - - int newBd = (dam1Dvb1Ryt1Bd1Sd1Tom1Tc1Hh1 & 0x10) >> 4; - if (newBd != Bd) { - Bd = newBd; - if (Bd == 1) { - _bassDrumChannel.Op1?.KeyOn(); - _bassDrumChannel.Op2?.KeyOn(); - } - } - - int newSd = (dam1Dvb1Ryt1Bd1Sd1Tom1Tc1Hh1 & 0x08) >> 3; - if (newSd != Sd) { - Sd = newSd; - if (Sd == 1) { - SnareDrumOperator?.KeyOn(); - } - } - - int newTom = (dam1Dvb1Ryt1Bd1Sd1Tom1Tc1Hh1 & 0x04) >> 2; - if (newTom != Tom) { - Tom = newTom; - if (Tom == 1) { - TomTomOperator?.KeyOn(); - } - } - - int newTc = (dam1Dvb1Ryt1Bd1Sd1Tom1Tc1Hh1 & 0x02) >> 1; - if (newTc != Tc) { - Tc = newTc; - if (Tc == 1) { - TopCymbalOperator?.KeyOn(); - } - } - - int newHh = dam1Dvb1Ryt1Bd1Sd1Tom1Tc1Hh1 & 0x01; - if (newHh != Hh) { - Hh = newHh; - if (Hh == 1) { - HighHatOperator?.KeyOn(); - } - } - } - - private void Update_7_NEW1() { - int _7_new1 = Registers[_7_NEW1_Offset]; - // OPL2/OPL3 mode selection. This register is used in - // OPL3.read(), OPL3.write() and Operator.getOperatorOutput(); - IsOpl3Mode = (_7_new1 & 0x01); - if (IsOpl3Mode == 1) { - SetEnabledChannels(); - } - - Set4OpConnections(); - } - - private void SetEnabledChannels() { - for (int array = 0; array < 2; array++) { - for (int i = 0; i < 9; i++) { - int baseAddress = _channels[array, i].ChannelBaseAddress; - Registers[baseAddress + Channel.Chd1Chc1Chb1Cha1Fb3Cnt1Offset] |= 0xF0; - _channels[array, i].Update_CHD1_CHC1_CHB1_CHA1_FB3_CNT1(); - } - } - } - - private void Update_2_CONNECTIONSEL6() { - // This method is called only if IsOpl3Mode is set. - int _2_connectionsel6 = Registers[_2_CONNECTIONSEL6_Offset]; - // 2-op/4-op channel selection. This register is used here to configure the OPL3.channels[] array. - Connectionsel = (_2_connectionsel6 & 0x3F); - Set4OpConnections(); - } - - private void Set4OpConnections() { - var disabledChannel = new NullChannel(this); - - // bits 0, 1, 2 sets respectively 2-op channels (1,4), (2,5), (3,6) to 4-op operation. - // bits 3, 4, 5 sets respectively 2-op channels (10,13), (11,14), (12,15) to 4-op operation. - for (int array = 0; array < 2; array++) { - for (int i = 0; i < 3; i++) { - if (IsOpl3Mode == 1) { - int shift = (array * 3) + i; - int connectionBit = (Connectionsel >> shift) & 0x01; - if (connectionBit == 1) { - _channels[array, i] = _channels4Op[array, i]; - _channels[array, i + 3] = disabledChannel; - _channels[array, i].UpdateChannel(); - continue; - } - } - _channels[array, i] = Channels2Op[array, i]; - _channels[array, i + 3] = Channels2Op[array, i + 3]; - _channels[array, i].UpdateChannel(); - _channels[array, i + 3].UpdateChannel(); - } - } - } - - private void SetRhythmMode() { - if (Ryt == 1) { - _channels[0, 6] = _bassDrumChannel; - _channels[0, 7] = _highHatSnareDrumChannel; - _channels[0, 8] = _tomTomTopCymbalChannel; - _operators[0, 0x11] = HighHatOperator; - _operators[0, 0x14] = SnareDrumOperator; - _operators[0, 0x12] = TomTomOperator; - _operators[0, 0x15] = TopCymbalOperator; - } - else { - for (int i = 6; i <= 8; i++) { - _channels[0, i] = Channels2Op[0, i]; - } - if(_highHatOperatorInNonRhythmMode is not null) { - _operators[0, 0x11] = _highHatOperatorInNonRhythmMode; - } - if(_snareDrumOperatorInNonRhythmMode is not null) { - _operators[0, 0x14] = _snareDrumOperatorInNonRhythmMode; - } - if(_tomTomOperatorInNonRhythmMode is not null) { - _operators[0, 0x12] = _tomTomOperatorInNonRhythmMode; - } - if(_topCymbalOperatorInNonRhythmMode is not null) { - _operators[0, 0x15] = _topCymbalOperatorInNonRhythmMode; - } - } - for (int i = 6; i <= 8; i++) { - _channels[0, i].UpdateChannel(); - } - } -} diff --git a/src/Spice86.Core/Emulator/Devices/Sound/Ymf262Emu/Intrinsics.cs b/src/Spice86.Core/Emulator/Devices/Sound/Ymf262Emu/Intrinsics.cs deleted file mode 100644 index ec55bfca3b..0000000000 --- a/src/Spice86.Core/Emulator/Devices/Sound/Ymf262Emu/Intrinsics.cs +++ /dev/null @@ -1,35 +0,0 @@ -namespace Spice86.Core.Emulator.Devices.Sound.Ymf262Emu; - -using System.Runtime.Intrinsics.X86; - -/// -/// Provides low-level hardware intrinsics for use in performance-critical code. -/// -internal static class Intrinsics { - - /// - /// Extracts a contiguous sequence of bits from a given integer value, starting from a specified index - /// and with a specified length. If the BMI1 instruction set is supported by the current CPU, this method - /// will use hardware acceleration via the BitFieldExtract method provided by the X86.Bmi1 class. Otherwise, - /// this method will perform the extraction using software logic. - /// - /// The integer value from which to extract the bits. - /// The index of the starting bit to extract. - /// The number of bits to extract. - /// A mask to apply to the value before extraction. - /// The extracted bits, packed into an unsigned integer. - public static uint ExtractBits(uint value, byte start, byte length, uint mask) { - if (Bmi1.IsSupported) { - return Bmi1.BitFieldExtract(value, start, length); - } else { - return (value & mask) >> start; - } - } - - /// - /// Returns the base-2 logarithm of a specified number. - /// - /// The number whose logarithm is to be computed. - /// The base-2 logarithm of x. - public static double Log2(double x) => Math.Log2(x); -} \ No newline at end of file diff --git a/src/Spice86.Core/Emulator/Devices/Sound/Ymf262Emu/OPL3FM.cs b/src/Spice86.Core/Emulator/Devices/Sound/Ymf262Emu/OPL3FM.cs index 2401f6a44e..549a3de22f 100644 --- a/src/Spice86.Core/Emulator/Devices/Sound/Ymf262Emu/OPL3FM.cs +++ b/src/Spice86.Core/Emulator/Devices/Sound/Ymf262Emu/OPL3FM.cs @@ -15,7 +15,8 @@ public class OPL3FM : DefaultIOPortHandler, IDisposable { private const byte Timer2Mask = 0xA0; private readonly SoundChannel _soundChannel; - private readonly FmSynthesizer? _synth; + //TODO: replace it with nukedOpl3. + private readonly dynamic? _synth; private readonly IPauseHandler _pauseHandler; private int _currentAddress; private volatile bool _endThread; @@ -45,7 +46,7 @@ public class OPL3FM : DefaultIOPortHandler, IDisposable { public OPL3FM(SoundChannel fmSynthSoundChannel, State state, IOPortDispatcher ioPortDispatcher, bool failOnUnhandledPort, ILoggerService loggerService, IPauseHandler pauseHandler) : base(state, failOnUnhandledPort, loggerService) { _pauseHandler = pauseHandler; _soundChannel = fmSynthSoundChannel; - _synth = new(48000); + //_synth = new(48000); _playbackThread = new Thread(GenerateWaveforms) { Name = nameof(OPL3FM) }; @@ -148,7 +149,7 @@ private void GenerateWaveforms() { } private void FillBuffer(Span buffer, Span playBuffer) { - _synth?.GetData(buffer); + //_synth?.GetData(buffer); ChannelAdapter.MonoToStereo(buffer, playBuffer); } diff --git a/src/Spice86.Core/Emulator/Devices/Sound/Ymf262Emu/Operators/HighHat.cs b/src/Spice86.Core/Emulator/Devices/Sound/Ymf262Emu/Operators/HighHat.cs deleted file mode 100644 index 3b048a6dea..0000000000 --- a/src/Spice86.Core/Emulator/Devices/Sound/Ymf262Emu/Operators/HighHat.cs +++ /dev/null @@ -1,34 +0,0 @@ -namespace Spice86.Core.Emulator.Devices.Sound.Ymf262Emu.Operators; -using System; - -/// -/// Emulates the highhat OPL operator. -/// -internal sealed class HighHat : TopCymbal { - /// - /// Initializes a new instance of the HighHat class. - /// - /// FmSynthesizer instance which owns the operator. - public HighHat(FmSynthesizer opl) - : base(0x11, opl) { - } - - /// - /// Returns the current output value of the operator. - /// - /// Modulation factor to apply to the output. - /// Current output value of the operator. - public override double GetOperatorOutput(double modulator) { - if (Opl.TopCymbalOperator == null) { - return 0d; - } - - double topCymbalOperatorPhase = Opl.TopCymbalOperator.Phase * PhaseMultiplierTable[Opl.TopCymbalOperator.Mult]; - double operatorOutput = GetOperatorOutput(modulator, topCymbalOperatorPhase); - if (operatorOutput == 0) { - operatorOutput = Random.Shared.NextDouble() * Envelope; - } - - return operatorOutput; - } -} diff --git a/src/Spice86.Core/Emulator/Devices/Sound/Ymf262Emu/Operators/Operator.cs b/src/Spice86.Core/Emulator/Devices/Sound/Ymf262Emu/Operators/Operator.cs deleted file mode 100644 index e2c01930d4..0000000000 --- a/src/Spice86.Core/Emulator/Devices/Sound/Ymf262Emu/Operators/Operator.cs +++ /dev/null @@ -1,245 +0,0 @@ -namespace Spice86.Core.Emulator.Devices.Sound.Ymf262Emu.Operators; -using System; - -/// -/// Emulates a single OPL operator. -/// -internal class Operator { - public readonly AdsrCalculator EnvelopeGenerator; - public double Phase; - public int Mult, Ar; - - protected double Envelope; - protected readonly FmSynthesizer Opl; - protected int Am, Egt, Ws; - - protected static readonly double[] PhaseMultiplierTable = { 0.5, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 10, 12, 12, 15, 15 }; - - private readonly int _operatorBaseAddress; - private int _ksr, _ksl, _tl, _dr, _sl, _rr, _vib; - private int _keyScaleNumber, _fNumber, _block; - private double _phaseIncrement; - - public const double NoModulator = 0; - - private const int Wavelength = 1024; - private const int Am1Vib1Egt1Ksr1Mult4Offset = 0x20; - private const int Ksl2Tl6Offset = 0x40; - private const int Ar4Dr4Offset = 0x60; - private const int Sl4Rr4Offset = 0x80; - private const int _5_WS3_Offset = 0xE0; - - /// - /// Initializes a new instance of the Operator class. - /// - /// Base operator register address. - /// FmSynthesizer instance which owns the operator. - public Operator(int baseAddress, FmSynthesizer opl) { - _operatorBaseAddress = baseAddress; - Opl = opl; - EnvelopeGenerator = new AdsrCalculator(opl); - } - - public void Update_AM1_VIB1_EGT1_KSR1_MULT4() - { - int am1Vib1Egt1Ksr1Mult4 = Opl.Registers[_operatorBaseAddress + Am1Vib1Egt1Ksr1Mult4Offset]; - - // Amplitude Modulation. This register is used int EnvelopeGenerator.getEnvelope(); - Am = (am1Vib1Egt1Ksr1Mult4 & 0x80) >> 7; - // Vibrato. This register is used in PhaseGenerator.getPhase(); - _vib = (am1Vib1Egt1Ksr1Mult4 & 0x40) >> 6; - // Envelope Generator Type. This register is used in EnvelopeGenerator.getEnvelope(); - Egt = (am1Vib1Egt1Ksr1Mult4 & 0x20) >> 5; - // Key Scale Rate. Sets the actual envelope rate together with rate and keyScaleNumber. - // This register os used in EnvelopeGenerator.setActualAttackRate(). - _ksr = (am1Vib1Egt1Ksr1Mult4 & 0x10) >> 4; - // Multiple. Multiplies the Channel.baseFrequency to get the Operator.operatorFrequency. - // This register is used in PhaseGenerator.setFrequency(). - Mult = am1Vib1Egt1Ksr1Mult4 & 0x0F; - - UpdateFrequency(); - EnvelopeGenerator.SetActualAttackRate(Ar, _ksr, _keyScaleNumber); - EnvelopeGenerator.SetActualDecayRate(_dr, _ksr, _keyScaleNumber); - EnvelopeGenerator.SetActualReleaseRate(_rr, _ksr, _keyScaleNumber); - } - - public void Update_KSL2_TL6() - { - int ksl2Tl6 = Opl.Registers[_operatorBaseAddress + Ksl2Tl6Offset]; - - // Key Scale Level. Sets the attenuation in accordance with the octave. - _ksl = (ksl2Tl6 & 0xC0) >> 6; - // Total Level. Sets the overall damping for the envelope. - _tl = ksl2Tl6 & 0x3F; - - EnvelopeGenerator.SetAtennuation(_fNumber, _block, _ksl); - EnvelopeGenerator.TotalLevel = _tl; - } - - public void Update_AR4_DR4() - { - int ar4Dr4 = Opl.Registers[_operatorBaseAddress + Ar4Dr4Offset]; - - // Attack Rate. - Ar = (ar4Dr4 & 0xF0) >> 4; - // Decay Rate. - _dr = ar4Dr4 & 0x0F; - - EnvelopeGenerator.SetActualAttackRate(Ar, _ksr, _keyScaleNumber); - EnvelopeGenerator.SetActualDecayRate(_dr, _ksr, _keyScaleNumber); - } - - public void Update_SL4_RR4() - { - int sl4Rr4 = Opl.Registers[_operatorBaseAddress + Sl4Rr4Offset]; - - // Sustain Level. - _sl = (sl4Rr4 & 0xF0) >> 4; - // Release Rate. - _rr = sl4Rr4 & 0x0F; - - EnvelopeGenerator.SustainLevel = _sl; - EnvelopeGenerator.SetActualReleaseRate(_rr, _ksr, _keyScaleNumber); - } - - public void Update_5_WS3() - { - int _5_ws3 = Opl.Registers[_operatorBaseAddress + _5_WS3_Offset]; - Ws = _5_ws3 & 0x07; - } - - /// - /// Returns the current output value of the operator. - /// - /// Modulation factor to apply to the output. - /// Current output value of the operator. - public virtual double GetOperatorOutput(double modulator) - { - if (EnvelopeGenerator.State == AdsrState.Off) { - return 0; - } - - double envelopeInDb = EnvelopeGenerator.GetEnvelope(Egt, Am); - Envelope = Math.Pow(10, envelopeInDb / 10.0); - - // If it is in OPL2 mode, use first four waveforms only: - Ws &= (Opl.IsOpl3Mode << 2) + 3; - - UpdatePhase(); - - double operatorOutput = GetOutput(modulator, Phase, Ws); - return operatorOutput; - } - - public virtual double GetOutput(double modulator, double outputPhase, int waveform) - { - outputPhase = (outputPhase + modulator) % 1; - if (outputPhase < 0) - { - outputPhase++; - // If the double could not afford to be less than 1: - outputPhase %= 1; - } - - int sampleIndex = (int)(outputPhase * Wavelength); - return GetWaveformValue(waveform, sampleIndex) * Envelope; - } - - public virtual void KeyOn() - { - if (Ar > 0) - { - EnvelopeGenerator.KeyOn(); - Phase = 0; - } - else - { - EnvelopeGenerator.State = AdsrState.Off; - } - } - - public virtual void KeyOff() - { - EnvelopeGenerator.KeyOff(); - } - - public virtual void UpdateOperator(int ksn, int fNum, int blk) - { - _keyScaleNumber = ksn; - _fNumber = fNum; - _block = blk; - Update_AM1_VIB1_EGT1_KSR1_MULT4(); - Update_KSL2_TL6(); - Update_AR4_DR4(); - Update_SL4_RR4(); - Update_5_WS3(); - } - - /// - /// Calculates and stores the current phase of the operator. - /// - protected void UpdatePhase() - { - if (_vib == 1) { - Phase += _phaseIncrement * VibratoGenerator.GetValue(Opl.Dvb, Opl.VibratoIndex); - } else { - Phase += _phaseIncrement; - } - - Phase %= 1; - } - - /// - /// Calculates and stores the current frequency of the operator. - /// - private void UpdateFrequency() - { - double baseFrequency = _fNumber * Math.Pow(2, _block - 1) * Opl.SampleRate / Math.Pow(2, 19); - double operatorFrequency = baseFrequency * PhaseMultiplierTable[Mult]; - - _phaseIncrement = operatorFrequency / Opl.SampleRate; - } - - private static double GetWaveformValue(int w, int i) - { - const double thetaIncrement = 2 * Math.PI / 1024; - const double xFactor = 1 * 16d / 256d; - - switch (w) - { - case 0: - return Math.Sin(i * thetaIncrement); - - case 1: - return i < 512 ? Math.Sin(i * thetaIncrement) : 0; - - case 2: - return Math.Sin((i & 511) * thetaIncrement); - - case 3: - if (i < 256) { - return Math.Sin(i * thetaIncrement); - } else if (i is >= 512 and < 768) { - return Math.Sin((i - 512) * thetaIncrement); - } else { - return 0; - } - - case 4: - return i < 512 ? Math.Sin(i * 2 * thetaIncrement) : 0; - - case 5: - return i < 512 ? Math.Sin(((i * 2) & 511) * thetaIncrement) : 0; - - case 6: - return i < 512 ? 1 : -1; - - default: - if (i < 512) { - return Math.Pow(2, -(i * xFactor)); - } else { - return -Math.Pow(2, -(((i - 512) * xFactor) + (1 / 16d))); - } - } - } -} diff --git a/src/Spice86.Core/Emulator/Devices/Sound/Ymf262Emu/Operators/SnareDrum.cs b/src/Spice86.Core/Emulator/Devices/Sound/Ymf262Emu/Operators/SnareDrum.cs deleted file mode 100644 index 57c8eb347d..0000000000 --- a/src/Spice86.Core/Emulator/Devices/Sound/Ymf262Emu/Operators/SnareDrum.cs +++ /dev/null @@ -1,52 +0,0 @@ -namespace Spice86.Core.Emulator.Devices.Sound.Ymf262Emu.Operators; -using System; - -/// -/// Emulates the snare drum OPL operator. -/// -internal sealed class SnareDrum : Operator { - /// - /// Initializes a new instance of the SnareDrum operator. - /// - /// FmSynthesizer instance which owns the operator. - public SnareDrum(FmSynthesizer opl) - : base(0x14, opl) { - } - - /// - /// Returns the current output value of the operator. - /// - /// Modulation factor to apply to the output. - /// Current output value of the operator. - public override double GetOperatorOutput(double modulator) { - if (EnvelopeGenerator.State == AdsrState.Off) { - return 0; - } - - double envelopeInDb = EnvelopeGenerator.GetEnvelope(Egt, Am); - Envelope = Math.Pow(10, envelopeInDb / 10.0); - - // If it is in OPL2 mode, use first four waveforms only: - int waveIndex = Ws & ((Opl.IsOpl3Mode << 2) + 3); - - if (Opl.HighHatOperator != null) { - Phase = Opl.HighHatOperator.Phase * 2; - } - - double operatorOutput = GetOutput(modulator, Phase, waveIndex); - double noise = Random.Shared.NextDouble() * Envelope; - - if (operatorOutput / Envelope is not 1 and not (-1)) - { - if (operatorOutput > 0) { - operatorOutput = noise; - } else if (operatorOutput < 0) { - operatorOutput = -noise; - } else { - operatorOutput = 0; - } - } - - return operatorOutput * 2; - } -} diff --git a/src/Spice86.Core/Emulator/Devices/Sound/Ymf262Emu/Operators/TopCymbal.cs b/src/Spice86.Core/Emulator/Devices/Sound/Ymf262Emu/Operators/TopCymbal.cs deleted file mode 100644 index c7b6839c39..0000000000 --- a/src/Spice86.Core/Emulator/Devices/Sound/Ymf262Emu/Operators/TopCymbal.cs +++ /dev/null @@ -1,63 +0,0 @@ -namespace Spice86.Core.Emulator.Devices.Sound.Ymf262Emu.Operators; -using System; - -/// -/// Emulates the top cymbal OPL operator. -/// -internal class TopCymbal : Operator { - /// - /// Initializes a new instance of the TopCymbal class. - /// - /// Base operator register address. - /// FmSynthesizer instance which owns the operator. - protected TopCymbal(int baseAddress, FmSynthesizer opl) - : base(baseAddress, opl) - { - } - - /// - /// Initializes a new instance of the TopCymbal class. - /// - /// FmSynthesizer instance which owns the operator. - public TopCymbal(FmSynthesizer opl) - : base(0x15, opl) - { - } - - /// - /// Returns the current output value of the operator. - /// - /// Modulation factor to apply to the output. - /// Current output value of the operator. - public override double GetOperatorOutput(double modulator) { - if (Opl.HighHatOperator == null) { - return 0d; - } - double highHatOperatorPhase = Opl.HighHatOperator.Phase * PhaseMultiplierTable[Opl.HighHatOperator.Mult]; - // The Top Cymbal operator uses his own phase together with the High Hat phase. - return GetOperatorOutput(modulator, highHatOperatorPhase); - } - - public double GetOperatorOutput(double modulator, double externalPhase) - { - double envelopeInDb = EnvelopeGenerator.GetEnvelope(Egt, Am); - Envelope = Math.Pow(10, envelopeInDb / 10.0); - - UpdatePhase(); - - int waveIndex = Ws & ((Opl.IsOpl3Mode << 2) + 3); - - // Empirically tested multiplied phase for the Top Cymbal: - double carrierPhase = 8 * Phase % 1; - double modulatorPhase = externalPhase; - double modulatorOutput = GetOutput(NoModulator, modulatorPhase, waveIndex); - double carrierOutput = GetOutput(modulatorOutput, carrierPhase, waveIndex); - - const int cycles = 4; - if ((carrierPhase * cycles) % cycles > 0.1) { - carrierOutput = 0; - } - - return carrierOutput * 2; - } -} diff --git a/src/Spice86.Core/Emulator/Devices/Sound/Ymf262Emu/VibratoGenerator.cs b/src/Spice86.Core/Emulator/Devices/Sound/Ymf262Emu/VibratoGenerator.cs deleted file mode 100644 index 21e97e17ef..0000000000 --- a/src/Spice86.Core/Emulator/Devices/Sound/Ymf262Emu/VibratoGenerator.cs +++ /dev/null @@ -1,46 +0,0 @@ -namespace Spice86.Core.Emulator.Devices.Sound.Ymf262Emu; - -/// -/// Generates a vibrato waveform for use in the YMF262Emu sound emulator. -/// -internal static class VibratoGenerator { - /// - /// The length of the generated vibrato waveform. - /// - public const int Length = 8192; - - private static readonly double Cent = Math.Pow(Math.Pow(2, 1 / 12.0), 1 / 100.0); - private static readonly double Dvb0 = Math.Pow(Cent, 7); - private static readonly double Dvb1 = Math.Pow(Cent, 14); - - /// - /// The values of the generated vibrato waveform. - /// - private static readonly double[] Values = { - 1, - Math.Sqrt(Dvb0), - Dvb0, - Math.Sqrt(Dvb0), - 1, - 1 / Math.Sqrt(Dvb0), - 1 / Dvb0, - 1 / Math.Sqrt(Dvb0), - - 1, - Math.Sqrt(Dvb1), - Dvb1, - Math.Sqrt(Dvb1), - 1, - 1 / Math.Sqrt(Dvb1), - 1 / Dvb1, - 1 / Math.Sqrt(Dvb1) - }; - - /// - /// Returns the value of the generated vibrato waveform at the specified index and depth. - /// - /// The depth of the vibrato waveform. - /// The index of the vibrato waveform to retrieve. - /// The value of the generated vibrato waveform at the specified index and depth. - public static double GetValue(int dvb, int i) => Values[(dvb * 8) + Intrinsics.ExtractBits((uint)i, 10, 3, 0x1C00)]; -} \ No newline at end of file From 659e7546cca8bebdee199fff5ece5716da303c7f Mon Sep 17 00:00:00 2001 From: Maximilien Noal Date: Thu, 12 Sep 2024 20:32:13 +0200 Subject: [PATCH 2/5] feature: Adlib Gold, NukedUpl3 classes Signed-off-by: Maximilien Noal --- .../Backend/Audio/Iir/BandPassTransform.cs | 120 ++ .../Backend/Audio/Iir/BandStopTransform.cs | 122 ++ src/Spice86.Core/Backend/Audio/Iir/Biquad.cs | 148 ++ .../Backend/Audio/Iir/BiquadPoleState.cs | 37 + .../Backend/Audio/Iir/Butterworth.cs | 252 +++ src/Spice86.Core/Backend/Audio/Iir/Cascade.cs | 153 ++ .../Backend/Audio/Iir/ChebyshevI.cs | 288 ++++ .../Backend/Audio/Iir/ChebyshevII.cs | 265 ++++ .../Backend/Audio/Iir/ComplexExtensions.cs | 11 + .../Backend/Audio/Iir/ComplexPair.cs | 68 + .../Backend/Audio/Iir/ComplexUtils.cs | 23 + .../Backend/Audio/Iir/DirectFormAbstract.cs | 39 + .../Backend/Audio/Iir/DirectFormI.cs | 56 + .../Backend/Audio/Iir/DirectFormII.cs | 57 + .../Backend/Audio/Iir/HighPassTransform.cs | 75 + .../Backend/Audio/Iir/LayoutBase.cs | 86 ++ .../Backend/Audio/Iir/LowPassTransform.cs | 75 + .../Backend/Audio/Iir/MathSupplement.cs | 72 + .../Backend/Audio/Iir/PoleZeroPair.cs | 54 + src/Spice86.Core/Backend/Audio/Iir/RBJ.cs | 313 ++++ .../Backend/Audio/Iir/SOSCascade.cs | 63 + .../Emulator/Devices/Sound/AdlibGold.cs | 358 +++++ .../Emulator/Devices/Sound/AudioFrame.cs | 52 + .../Emulator/Devices/Sound/MathUtils.cs | 6 + .../Emulator/Devices/Sound/OPL.cs | 292 ++++ .../Emulator/Devices/Sound/OPL3Nuked.cs | 1372 +++++++++++++++++ .../Emulator/Devices/Sound/OPLConsts.cs | 5 + .../Emulator/Devices/Sound/OplMode.cs | 9 + .../Sound/StereoProcessorControlReg.cs | 8 + 29 files changed, 4479 insertions(+) create mode 100644 src/Spice86.Core/Backend/Audio/Iir/BandPassTransform.cs create mode 100644 src/Spice86.Core/Backend/Audio/Iir/BandStopTransform.cs create mode 100644 src/Spice86.Core/Backend/Audio/Iir/Biquad.cs create mode 100644 src/Spice86.Core/Backend/Audio/Iir/BiquadPoleState.cs create mode 100644 src/Spice86.Core/Backend/Audio/Iir/Butterworth.cs create mode 100644 src/Spice86.Core/Backend/Audio/Iir/Cascade.cs create mode 100644 src/Spice86.Core/Backend/Audio/Iir/ChebyshevI.cs create mode 100644 src/Spice86.Core/Backend/Audio/Iir/ChebyshevII.cs create mode 100644 src/Spice86.Core/Backend/Audio/Iir/ComplexExtensions.cs create mode 100644 src/Spice86.Core/Backend/Audio/Iir/ComplexPair.cs create mode 100644 src/Spice86.Core/Backend/Audio/Iir/ComplexUtils.cs create mode 100644 src/Spice86.Core/Backend/Audio/Iir/DirectFormAbstract.cs create mode 100644 src/Spice86.Core/Backend/Audio/Iir/DirectFormI.cs create mode 100644 src/Spice86.Core/Backend/Audio/Iir/DirectFormII.cs create mode 100644 src/Spice86.Core/Backend/Audio/Iir/HighPassTransform.cs create mode 100644 src/Spice86.Core/Backend/Audio/Iir/LayoutBase.cs create mode 100644 src/Spice86.Core/Backend/Audio/Iir/LowPassTransform.cs create mode 100644 src/Spice86.Core/Backend/Audio/Iir/MathSupplement.cs create mode 100644 src/Spice86.Core/Backend/Audio/Iir/PoleZeroPair.cs create mode 100644 src/Spice86.Core/Backend/Audio/Iir/RBJ.cs create mode 100644 src/Spice86.Core/Backend/Audio/Iir/SOSCascade.cs create mode 100644 src/Spice86.Core/Emulator/Devices/Sound/AdlibGold.cs create mode 100644 src/Spice86.Core/Emulator/Devices/Sound/AudioFrame.cs create mode 100644 src/Spice86.Core/Emulator/Devices/Sound/MathUtils.cs create mode 100644 src/Spice86.Core/Emulator/Devices/Sound/OPL.cs create mode 100644 src/Spice86.Core/Emulator/Devices/Sound/OPL3Nuked.cs create mode 100644 src/Spice86.Core/Emulator/Devices/Sound/OPLConsts.cs create mode 100644 src/Spice86.Core/Emulator/Devices/Sound/OplMode.cs create mode 100644 src/Spice86.Core/Emulator/Devices/Sound/StereoProcessorControlReg.cs diff --git a/src/Spice86.Core/Backend/Audio/Iir/BandPassTransform.cs b/src/Spice86.Core/Backend/Audio/Iir/BandPassTransform.cs new file mode 100644 index 0000000000..7521e25734 --- /dev/null +++ b/src/Spice86.Core/Backend/Audio/Iir/BandPassTransform.cs @@ -0,0 +1,120 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Copyright (c) 2009 by Vinnie Falco + * Copyright (c) 2016 by Bernd Porr + */ + +using System.Numerics; + +namespace Spice86.Core.Backend.Audio.Iir; + +/** + * Transforms from an analogue bandpass filter to a digital bandstop filter + */ +public class BandPassTransform { + private readonly double _wc2; + private readonly double _wc; + private readonly double _a, _b; + private readonly double _a2, _b2; + private readonly double _ab, _ab2; + + public BandPassTransform(double fc, double fw, LayoutBase digital, + LayoutBase analog) { + + digital.Reset(); + + if (fc < 0) { + throw new ArithmeticException("Cutoff frequency cannot be negative."); + } + + if (!(fc < 0.5)) { + throw new ArithmeticException("Cutoff frequency must be less than the Nyquist frequency."); + } + + double ww = 2 * Math.PI * fw; + + // pre-calcs + _wc2 = (2 * Math.PI * fc) - ww / 2; + _wc = _wc2 + ww; + + // what is this crap? + if (_wc2 < 1e-8) { + _wc2 = 1e-8; + } + + if (_wc > Math.PI - 1e-8) { + _wc = Math.PI - 1e-8; + } + + _a = Math.Cos((_wc + _wc2) * 0.5) / Math.Cos((_wc - _wc2) * 0.5); + _b = 1 / Math.Tan((_wc - _wc2) * 0.5); + _a2 = _a * _a; + _b2 = _b * _b; + _ab = _a * _b; + _ab2 = 2 * _ab; + + int numPoles = analog.NumPoles; + int pairs = numPoles / 2; + for (int i = 0; i < pairs; ++i) { + PoleZeroPair pair = analog.GetPair(i); + ComplexPair p1 = Transform(pair.poles.First); + ComplexPair z1 = Transform(pair.zeros.First); + + digital.AddPoleZeroConjugatePairs(p1.First, z1.First); + digital.AddPoleZeroConjugatePairs(p1.Second, z1.Second); + } + + if ((numPoles & 1) == 1) { + ComplexPair poles = Transform(analog.GetPair(pairs).poles.First); + ComplexPair zeros = Transform(analog.GetPair(pairs).zeros.First); + + digital.Add(poles, zeros); + } + + double wn = analog.NormalW; + digital.SetNormal( + 2 * Math.Atan(Math.Sqrt(Math.Tan((_wc + wn) * 0.5) + * Math.Tan((_wc2 + wn) * 0.5))), analog.NormalGain); + } + + private ComplexPair Transform(Complex c) { + if (Complex.IsInfinity(c)) { + return new ComplexPair(new Complex(-1, 0), new Complex(1, 0)); + } + + c = new Complex(1, 0).Add(c).Divide(new Complex(1, 0).Subtract(c)); // bilinear + + var v = new Complex(0, 0); + v = MathSupplement.AddMul(v, 4 * (_b2 * (_a2 - 1) + 1), c); + v = v.Add(8 * (_b2 * (_a2 - 1) - 1)); + v = v.Multiply(c); + v = v.Add(4 * (_b2 * (_a2 - 1) + 1)); + v = v.Sqrt(); + + Complex u = v.Multiply(-1); + u = MathSupplement.AddMul(u, _ab2, c); + u = u.Add(_ab2); + + v = MathSupplement.AddMul(v, _ab2, c); + v = v.Add(_ab2); + + var d = new Complex(0, 0); + d = MathSupplement.AddMul(d, 2 * (_b - 1), c).Add(2 * (1 + _b)); + + return new ComplexPair(u.Divide(d), v.Divide(d)); + } +} diff --git a/src/Spice86.Core/Backend/Audio/Iir/BandStopTransform.cs b/src/Spice86.Core/Backend/Audio/Iir/BandStopTransform.cs new file mode 100644 index 0000000000..ea486217aa --- /dev/null +++ b/src/Spice86.Core/Backend/Audio/Iir/BandStopTransform.cs @@ -0,0 +1,122 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Copyright (c) 2009 by Vinnie Falco + * Copyright (c) 2016 by Bernd Porr + */ + +using System.Numerics; + +namespace Spice86.Core.Backend.Audio.Iir; + +/** + * Transforms from an analogue lowpass filter to a digital bandstop filter + */ +public class BandStopTransform { + private readonly double _wc; + private readonly double _wc2; + private readonly double _a; + private readonly double _b; + private readonly double _a2; + private readonly double _b2; + + + public BandStopTransform(double fc, + double fw, + LayoutBase digital, + LayoutBase analog) { + digital.Reset(); + + if (fc < 0) { + throw new ArithmeticException("Cutoff frequency cannot be negative."); + } + + if (!(fc < 0.5)) { + throw new ArithmeticException("Cutoff frequency must be less than the Nyquist frequency."); + } + + double ww = 2 * Math.PI * fw; + + _wc2 = 2 * Math.PI * fc - ww / 2; + _wc = _wc2 + ww; + + // this is crap + if (_wc2 < 1e-8) { + _wc2 = 1e-8; + } + + if (_wc > Math.PI - 1e-8) { + _wc = Math.PI - 1e-8; + } + + _a = Math.Cos((_wc + _wc2) * .5) / + Math.Cos((_wc - _wc2) * .5); + _b = Math.Tan((_wc - _wc2) * .5); + _a2 = _a * _a; + _b2 = _b * _b; + + int numPoles = analog.NumPoles; + int pairs = numPoles / 2; + for (int i = 0; i < pairs; i++) { + PoleZeroPair pair = analog.GetPair(i); + ComplexPair p = Transform(pair.poles.First); + ComplexPair z = Transform(pair.zeros.First); + digital.AddPoleZeroConjugatePairs(p.First, z.First); + digital.AddPoleZeroConjugatePairs(p.Second, z.Second); + } + + if ((numPoles & 1) == 1) { + ComplexPair poles = Transform(analog.GetPair(pairs).poles.First); + ComplexPair zeros = Transform(analog.GetPair(pairs).zeros.First); + + digital.Add(poles, zeros); + } + + if (fc < 0.25) { + digital.SetNormal(Math.PI, analog.NormalGain); + } else { + digital.SetNormal(0, analog.NormalGain); + } + } + + private ComplexPair Transform(Complex c) { + if (c == Complex.Infinity) { + c = new Complex(-1, 0); + } else { + c = new Complex(1, 0).Add(c).Divide(new Complex(1, 0).Subtract(c)); // bilinear + } + + var u = new Complex(0, 0); + u = MathSupplement.AddMul(u, 4 * (_b2 + _a2 - 1), c); + u = u.Add(8 * (_b2 - _a2 + 1)); + u = u.Multiply(c); + u = u.Add(4 * (_a2 + _b2 - 1)); + u = u.Sqrt(); + + Complex v = u.Multiply(-.5); + v = v.Add(_a); + v = MathSupplement.AddMul(v, -_a, c); + + u = u.Multiply(.5); + u = u.Add(_a); + u = MathSupplement.AddMul(u, -_a, c); + + var d = new Complex(_b + 1, 0); + d = MathSupplement.AddMul(d, _b - 1, c); + + return new ComplexPair(u.Divide(d), v.Divide(d)); + } +} diff --git a/src/Spice86.Core/Backend/Audio/Iir/Biquad.cs b/src/Spice86.Core/Backend/Audio/Iir/Biquad.cs new file mode 100644 index 0000000000..f9ad474e1c --- /dev/null +++ b/src/Spice86.Core/Backend/Audio/Iir/Biquad.cs @@ -0,0 +1,148 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Copyright (c) 2009 by Vinnie Falco + * Copyright (c) 2016 by Bernd Porr + */ + +using System.Numerics; + +namespace Spice86.Core.Backend.Audio.Iir; + +/** + * Contains the coefficients of a 2nd order digital filter with two poles and two zeros + */ +public class Biquad { + public double A0 { get; set; } + public double A1 { get; set; } + public double A2 { get; set; } + public double B1 { get; set; } + public double B2 { get; set; } + public double B0 { get; set; } + + public double GetA0 => A0; + + public double GetA1 => A1 * A0; + + public double GetA2 => A2 * A0; + + public double GetB0 => B0 * A0; + + public double GetB1 => B1 * A0; + + public double GetB2 => B2 * A0; + + public Complex Response(double normalizedFrequency) { + double a0 = GetA0; + double a1 = GetA1; + double a2 = GetA2; + double b0 = GetB0; + double b1 = GetB1; + double b2 = GetB2; + + double w = 2 * Math.PI * normalizedFrequency; + Complex czn1 = ComplexUtils.PolarToComplex(1.0, -w); + Complex czn2 = ComplexUtils.PolarToComplex(1.0, -2 * w); + var ch = new Complex(1, 0); + var cbot = new Complex(1, 0); + + var ct = new Complex(b0 / a0, 0); + var cb = new Complex(1, 0); + ct = MathSupplement.AddMul(ct, b1 / a0, czn1); + ct = MathSupplement.AddMul(ct, b2 / a0, czn2); + cb = MathSupplement.AddMul(cb, a1 / a0, czn1); + cb = MathSupplement.AddMul(cb, a2 / a0, czn2); + ch = Complex.Multiply(ch, ct); + cbot = Complex.Multiply(cbot, cb); + + return Complex.Divide(ch, cbot); + } + + public void SetCoefficients(double a0, double a1, double a2, + double b0, double b1, double b2) { + A0 = a0; + A1 = a1 / a0; + A2 = a2 / a0; + B0 = b0 / a0; + B1 = b1 / a0; + B2 = b2 / a0; + } + + public void SetOnePole(Complex pole, Complex zero) { + const double a0 = 1; + double a1 = -pole.Real; + const double a2 = 0; + double b0 = -zero.Real; + const double b1 = 1; + const double b2 = 0; + SetCoefficients(a0, a1, a2, b0, b1, b2); + } + + public void SetTwoPole( + Complex pole1, Complex zero1, + Complex pole2, Complex zero2) { + const double a0 = 1; + double a1; + double a2; + + if (pole1.Imaginary != 0) { + a1 = -2 * pole1.Real; + a2 = Complex.Abs(pole1) * Complex.Abs(pole1); + } else { + a1 = -(pole1.Real + pole2.Real); + a2 = pole1.Real * pole2.Real; + } + + const double b0 = 1; + double b1; + double b2; + + if (zero1.Imaginary != 0) { + b1 = -2 * zero1.Real; + b2 = Complex.Abs(zero1) * Complex.Abs(zero1); + } else { + b1 = -(zero1.Real + zero2.Real); + b2 = zero1.Real * zero2.Real; + } + + SetCoefficients(a0, a1, a2, b0, b1, b2); + } + + public void SetPoleZeroForm(BiquadPoleState bps) { + SetPoleZeroPair(bps); + ApplyScale(bps.Gain); + } + + public void SetIdentity() { + SetCoefficients(1, 0, 0, 1, 0, 0); + } + + public void ApplyScale(double scale) { + B0 *= scale; + B1 *= scale; + B2 *= scale; + } + + + public void SetPoleZeroPair(PoleZeroPair pair) { + if (pair.IsSinglePole()) { + SetOnePole(pair.poles.First, pair.zeros.First); + } else { + SetTwoPole(pair.poles.First, pair.zeros.First, + pair.poles.Second, pair.zeros.Second); + } + } +} diff --git a/src/Spice86.Core/Backend/Audio/Iir/BiquadPoleState.cs b/src/Spice86.Core/Backend/Audio/Iir/BiquadPoleState.cs new file mode 100644 index 0000000000..535eb02186 --- /dev/null +++ b/src/Spice86.Core/Backend/Audio/Iir/BiquadPoleState.cs @@ -0,0 +1,37 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Copyright (c) 2009 by Vinnie Falco + * Copyright (c) 2016 by Bernd Porr + */ + +using System.Numerics; + +namespace Spice86.Core.Backend.Audio.Iir; + +/** + * PoleZeroPair with gain factor + */ +public class BiquadPoleState : PoleZeroPair { + public BiquadPoleState(Complex p, Complex z) : base(p, z) { + } + + public BiquadPoleState(Complex p1, Complex z1, + Complex p2, Complex z2) : base(p1, z1, p2, z2) { + } + + public double Gain { get; set; } +} diff --git a/src/Spice86.Core/Backend/Audio/Iir/Butterworth.cs b/src/Spice86.Core/Backend/Audio/Iir/Butterworth.cs new file mode 100644 index 0000000000..b3f45501f9 --- /dev/null +++ b/src/Spice86.Core/Backend/Audio/Iir/Butterworth.cs @@ -0,0 +1,252 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Copyright (c) 2009 by Vinnie Falco + * Copyright (c) 2016 by Bernd Porr + */ + +using System.Numerics; + +namespace Spice86.Core.Backend.Audio.Iir; + +/** + * User facing class which contains all the methods the user uses + * to create Butterworth filters. This done in this way: + * Butterworth butterworth = new Butterworth(); + * Then call one of the methods below to create + * low-,high-,band-, or stopband filters. For example: + * butterworth.bandPass(2,250,50,5); + */ +public class Butterworth : Cascade { + class AnalogLowPass : LayoutBase { + private readonly int _nPoles; + public AnalogLowPass(int nPoles) : base(nPoles) { + _nPoles = nPoles; + SetNormal(0, 1); + } + + public void Design() { + Reset(); + double n2 = 2 * _nPoles; + int pairs = _nPoles / 2; + for (int i = 0; i < pairs; ++i) { + Complex c = ComplexUtils.PolarToComplex(1F, Math.PI / 2.0 + + (2 * i + 1) * Math.PI / n2); + AddPoleZeroConjugatePairs(c, Complex.Infinity); + } + + if ((_nPoles & 1) == 1) { + Add(new Complex(-1, 0), Complex.Infinity); + } + } + } + + private void SetupLowPass(int order, double sampleRate, + double cutoffFrequency, int directFormType) { + + var analogProto = new AnalogLowPass(order); + analogProto.Design(); + + var digitalProto = new LayoutBase(order); + + new LowPassTransform(cutoffFrequency / sampleRate, digitalProto, + analogProto); + + SetLayout(digitalProto, directFormType); + } + + /** + * Butterworth Lowpass filter with default topology + * + * @param order + * The order of the filter + * @param sampleRate + * The sampling rate of the system + * @param cutoffFrequency + * the cutoff frequency + */ + public void LowPass(int order, double sampleRate, double cutoffFrequency) { + SetupLowPass(order, sampleRate, cutoffFrequency, + DirectFormAbstract.DirectFormII); + } + + /** + * Butterworth Lowpass filter with custom topology + * + * @param order + * The order of the filter + * @param sampleRate + * The sampling rate of the system + * @param cutoffFrequency + * The cutoff frequency + * @param directFormType + * The filter topology. This is either + * DirectFormAbstract.DIRECT_FORM_I or DIRECT_FORM_II + */ + public void LowPass(int order, double sampleRate, double cutoffFrequency, + int directFormType) { + SetupLowPass(order, sampleRate, cutoffFrequency, directFormType); + } + + private void SetupHighPass(int order, double sampleRate, + double cutoffFrequency, int directFormType) { + + var analogProto = new AnalogLowPass(order); + analogProto.Design(); + + var digitalProto = new LayoutBase(order); + + new HighPassTransform(cutoffFrequency / sampleRate, digitalProto, + analogProto); + + SetLayout(digitalProto, directFormType); + } + + /** + * Highpass filter with custom topology + * + * @param order + * Filter order (ideally only even orders) + * @param sampleRate + * Sampling rate of the system + * @param cutoffFrequency + * Cutoff of the system + * @param directFormType + * The filter topology. See DirectFormAbstract. + */ + public void HighPass(int order, double sampleRate, double cutoffFrequency, + int directFormType) { + SetupHighPass(order, sampleRate, cutoffFrequency, directFormType); + } + + /** + * Highpass filter with default filter topology + * + * @param order + * Filter order (ideally only even orders) + * @param sampleRate + * Sampling rate of the system + * @param cutoffFrequency + * Cutoff of the system + */ + public void HighPass(int order, double sampleRate, double cutoffFrequency) { + SetupHighPass(order, sampleRate, cutoffFrequency, + DirectFormAbstract.DirectFormII); + } + + private void SetupBandStop(int order, double sampleRate, + double centerFrequency, double widthFrequency, int directFormType) { + + var analogProto = new AnalogLowPass(order); + analogProto.Design(); + + var m_digitalProto = new LayoutBase(order * 2); + + new BandStopTransform(centerFrequency / sampleRate, widthFrequency + / sampleRate, m_digitalProto, analogProto); + + SetLayout(m_digitalProto, directFormType); + } + + /** + * Bandstop filter with default topology + * + * @param order + * Filter order (actual order is twice) + * @param sampleRate + * Samping rate of the system + * @param centerFrequency + * Center frequency + * @param widthFrequency + * Width of the notch + */ + public void BandStop(int order, double sampleRate, double centerFrequency, + double widthFrequency) { + SetupBandStop(order, sampleRate, centerFrequency, widthFrequency, + DirectFormAbstract.DirectFormII); + } + + /** + * Bandstop filter with custom topology + * + * @param order + * Filter order (actual order is twice) + * @param sampleRate + * Samping rate of the system + * @param centerFrequency + * Center frequency + * @param widthFrequency + * Width of the notch + * @param directFormType + * The filter topology + */ + public void BandStop(int order, double sampleRate, double centerFrequency, + double widthFrequency, int directFormType) { + SetupBandStop(order, sampleRate, centerFrequency, widthFrequency, + directFormType); + } + + private void SetupBandPass(int order, double sampleRate, + double centerFrequency, double widthFrequency, int directFormType) { + var m_analogProto = new AnalogLowPass(order); + m_analogProto.Design(); + + var m_digitalProto = new LayoutBase(order * 2); + + new BandPassTransform(centerFrequency / sampleRate, widthFrequency + / sampleRate, m_digitalProto, m_analogProto); + + SetLayout(m_digitalProto, directFormType); + } + + /** + * Bandpass filter with default topology + * + * @param order + * Filter order + * @param sampleRate + * Sampling rate + * @param centerFrequency + * Center frequency + * @param widthFrequency + * Width of the notch + */ + public void BandPass(int order, double sampleRate, double centerFrequency, + double widthFrequency) { + SetupBandPass(order, sampleRate, centerFrequency, widthFrequency, + DirectFormAbstract.DirectFormII); + } + + /** + * Bandpass filter with custom topology + * + * @param order + * Filter order + * @param sampleRate + * Sampling rate + * @param centerFrequency + * Center frequency + * @param widthFrequency + * Width of the notch + * @param directFormType + * The filter topology (see DirectFormAbstract) + */ + public void BandPass(int order, double sampleRate, double centerFrequency, + double widthFrequency, int directFormType) { + SetupBandPass(order, sampleRate, centerFrequency, widthFrequency, + directFormType); + } +} diff --git a/src/Spice86.Core/Backend/Audio/Iir/Cascade.cs b/src/Spice86.Core/Backend/Audio/Iir/Cascade.cs new file mode 100644 index 0000000000..1f98369801 --- /dev/null +++ b/src/Spice86.Core/Backend/Audio/Iir/Cascade.cs @@ -0,0 +1,153 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Copyright (c) 2009 by Vinnie Falco + * Copyright (c) 2016 by Bernd Porr + */ + + +using System.Numerics; + +namespace Spice86.Core.Backend.Audio.Iir; + +/** + * + * The mother of all filters. It contains the coefficients of all + * filter stages as a sequence of 2nd order filters and the states + * of the 2nd order filters which also imply if it's direct form I or II + * + */ +public class Cascade { + // coefficients + private Biquad[] _biquads; + + // the states of the filters + private DirectFormAbstract[] _states; + + // number of biquads in the system + private int _numBiquads; + + private int _numPoles; + + public int GetNumBiquads() { + return _numBiquads; + } + + public Biquad GetBiquad(int index) { + return _biquads[index]; + } + + public Cascade() { + _numBiquads = 0; + _biquads = Array.Empty(); + _states = Array.Empty(); + } + + public void Reset() { + for (int i = 0; i < _numBiquads; i++) + _states[i].Reset(); + } + + public double Filter(double x) { + double res = x; + for (int i = 0; i < _numBiquads; i++) { + if (_states[i] != null) { + res = _states[i].Process1(res, _biquads[i]); + } + } + return res; + } + + public Complex Response(double normalizedFrequency) { + double w = 2 * Math.PI * normalizedFrequency; + Complex czn1 = ComplexUtils.PolarToComplex(1.0, -w); + Complex czn2 = ComplexUtils.PolarToComplex(1.0, -2 * w); + var ch = new Complex(1, 0); + var cbot = new Complex(1, 0); + + for (int i = 0; i < _numBiquads; i++) { + Biquad stage = _biquads[i]; + var cb = new Complex(1, 0); + var ct = new Complex(stage.GetB0 / stage.GetA0, 0); + ct = MathSupplement.AddMul(ct, stage.GetB1 / stage.GetA0, czn1); + ct = MathSupplement.AddMul(ct, stage.GetB2 / stage.GetA0, czn2); + cb = MathSupplement.AddMul(cb, stage.GetA1 / stage.GetA0, czn1); + cb = MathSupplement.AddMul(cb, stage.GetA2 / stage.GetA0, czn2); + ch = Complex.Multiply(ch, ct); + cbot = Complex.Multiply(cbot, cb); + } + + return Complex.Divide(ch, cbot); + } + + public void ApplyScale(double scale) { + // For higher order filters it might be helpful + // to spread this factor between all the stages. + if (_biquads.Length > 0) { + _biquads[0].ApplyScale(scale); + } + } + + private void CreateStates(int filterTypes) { + switch (filterTypes) { + case DirectFormAbstract.DirectFormI: + _states = new DirectFormI[_numBiquads]; + for (int i = 0; i < _numBiquads; i++) { + _states[i] = new DirectFormI(); + } + break; + case DirectFormAbstract.DirectFormII: + default: + _states = new DirectFormII[_numBiquads]; + for (int i = 0; i < _numBiquads; i++) { + _states[i] = new DirectFormII(); + } + break; + } + } + + public void SetLayout(LayoutBase proto, int filterTypes) { + _numPoles = proto.NumPoles; + _numBiquads = (_numPoles + 1) / 2; + _biquads = new Biquad[_numBiquads]; + CreateStates(filterTypes); + for (int i = 0; i < _numBiquads; ++i) { + PoleZeroPair p = proto.GetPair(i); + _biquads[i] = new Biquad(); + _biquads[i].SetPoleZeroPair(p); + } + ApplyScale(proto.NormalGain + / Complex.Abs(Response(proto.NormalW / (2 * Math.PI)))); + } + + public void SetSOScoeff(double[][] sosCoefficients, int stateTypes) { + _numBiquads = sosCoefficients.Length; + _biquads = new Biquad[_numBiquads]; + CreateStates(stateTypes); + for (int i = 0; i < _numBiquads; ++i) { + _biquads[i] = new Biquad(); + _biquads[i].SetCoefficients( + sosCoefficients[i][3], + sosCoefficients[i][4], + sosCoefficients[i][5], + sosCoefficients[i][0], + sosCoefficients[i][1], + sosCoefficients[i][2] + ); + } + ApplyScale(1); + } +}; \ No newline at end of file diff --git a/src/Spice86.Core/Backend/Audio/Iir/ChebyshevI.cs b/src/Spice86.Core/Backend/Audio/Iir/ChebyshevI.cs new file mode 100644 index 0000000000..67c39b087f --- /dev/null +++ b/src/Spice86.Core/Backend/Audio/Iir/ChebyshevI.cs @@ -0,0 +1,288 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Copyright (c) 2009 by Vinnie Falco + * Copyright (c) 2016 by Bernd Porr + */ + +using System.Numerics; + +namespace Spice86.Core.Backend.Audio.Iir; + +/** + * User facing class which contains all the methods the user uses to create + * ChebyshevI filters. This done in this way: ChebyshevI chebyshevI = new + * ChebyshevI(); Then call one of the methods below to create low-,high-,band-, + * or stopband filters. For example: chebyshevI.bandPass(2,250,50,5,0.5); + */ +public class ChebyshevI : Cascade { + class AnalogLowPass : LayoutBase { + private readonly int _nPoles; + + public AnalogLowPass(int nPoles) : base(nPoles) { + _nPoles = nPoles; + } + + public void Design(double rippleDb) { + Reset(); + + double eps = Math.Sqrt(1.0 / Math.Exp(-rippleDb * 0.1 * MathSupplement.DoubleLn10) - 1); + double v0 = MathSupplement.Asinh(1 / eps) / _nPoles; + double sinh_v0 = -Math.Sinh(v0); + double cosh_v0 = Math.Cosh(v0); + + double n2 = 2 * _nPoles; + int pairs = _nPoles / 2; + for (int i = 0; i < pairs; ++i) { + int k = 2 * i + 1 - _nPoles; + double a = sinh_v0 * Math.Cos(k * Math.PI / n2); + double b = cosh_v0 * Math.Sin(k * Math.PI / n2); + + AddPoleZeroConjugatePairs(new Complex(a, b), new Complex( + double.PositiveInfinity, 0)); + } + + if ((_nPoles & 1) == 1) { + Add(new Complex(sinh_v0, 0), new Complex( + double.PositiveInfinity, 0)); + SetNormal(0, 1); + } else { + SetNormal(0, Math.Pow(10, -rippleDb / 20.0)); + } + } + } + + private void SetupLowPass(int order, double sampleRate, + double cutoffFrequency, double rippleDb, int directFormType) { + + var m_analogProto = new AnalogLowPass(order); + m_analogProto.Design(rippleDb); + + var m_digitalProto = new LayoutBase(order); + + new LowPassTransform(cutoffFrequency / sampleRate, m_digitalProto, + m_analogProto); + + SetLayout(m_digitalProto, directFormType); + } + + /** + * ChebyshevI Lowpass filter with default toplogy + * + * @param order + * The order of the filter + * @param sampleRate + * The sampling rate of the system + * @param cutoffFrequency + * the cutoff frequency + * @param rippleDb + * passband ripple in decibel sensible value: 1dB + */ + public void LowPass(int order, double sampleRate, double cutoffFrequency, + double rippleDb) { + SetupLowPass(order, sampleRate, cutoffFrequency, rippleDb, + DirectFormAbstract.DirectFormII); + } + + /** + * ChebyshevI Lowpass filter with custom topology + * + * @param order + * The order of the filter + * @param sampleRate + * The sampling rate of the system + * @param cutoffFrequency + * The cutoff frequency + * @param rippleDb + * passband ripple in decibel sensible value: 1dB + * @param directFormType + * The filter topology. This is either + * DirectFormAbstract.DIRECT_FORM_I or DIRECT_FORM_II + */ + public void LowPass(int order, double sampleRate, double cutoffFrequency, + double rippleDb, int directFormType) { + SetupLowPass(order, sampleRate, cutoffFrequency, rippleDb, + directFormType); + } + + private void SetupHighPass(int order, double sampleRate, + double cutoffFrequency, double rippleDb, int directFormType) { + var analogProto = new AnalogLowPass(order); + analogProto.Design(rippleDb); + + var digitalProto = new LayoutBase(order); + + new HighPassTransform(cutoffFrequency / sampleRate, digitalProto, + analogProto); + + SetLayout(digitalProto, directFormType); + } + + /** + * ChebyshevI Highpass filter with default topology + * + * @param order + * The order of the filter + * @param sampleRate + * The sampling rate of the system + * @param cutoffFrequency + * the cutoff frequency + * @param rippleDb + * passband ripple in decibel sensible value: 1dB + */ + public void HighPass(int order, double sampleRate, double cutoffFrequency, + double rippleDb) { + SetupHighPass(order, sampleRate, cutoffFrequency, rippleDb, + DirectFormAbstract.DirectFormII); + } + + /** + * ChebyshevI Lowpass filter and custom filter topology + * + * @param order + * The order of the filter + * @param sampleRate + * The sampling rate of the system + * @param cutoffFrequency + * The cutoff frequency + * @param rippleDb + * passband ripple in decibel sensible value: 1dB + * @param directFormType + * The filter topology. This is either + * DirectFormAbstract.DIRECT_FORM_I or DIRECT_FORM_II + */ + public void HighPass(int order, double sampleRate, double cutoffFrequency, + double rippleDb, int directFormType) { + SetupHighPass(order, sampleRate, cutoffFrequency, rippleDb, + directFormType); + } + + private void SetupBandStop(int order, double sampleRate, + double centerFrequency, double widthFrequency, double rippleDb, + int directFormType) { + + var analogProto = new AnalogLowPass(order); + analogProto.Design(rippleDb); + + var digitalProto = new LayoutBase(order * 2); + + new BandStopTransform(centerFrequency / sampleRate, widthFrequency + / sampleRate, digitalProto, analogProto); + + SetLayout(digitalProto, directFormType); + } + + /** + * Bandstop filter with default topology + * + * @param order + * Filter order (actual order is twice) + * @param sampleRate + * Samping rate of the system + * @param centerFrequency + * Center frequency + * @param widthFrequency + * Width of the notch + * @param rippleDb + * passband ripple in decibel sensible value: 1dB + */ + public void BandStop(int order, double sampleRate, double centerFrequency, + double widthFrequency, double rippleDb) { + SetupBandStop(order, sampleRate, centerFrequency, widthFrequency, + rippleDb, DirectFormAbstract.DirectFormII); + } + + /** + * Bandstop filter with custom topology + * + * @param order + * Filter order (actual order is twice) + * @param sampleRate + * Samping rate of the system + * @param centerFrequency + * Center frequency + * @param widthFrequency + * Width of the notch + * @param rippleDb + * passband ripple in decibel sensible value: 1dB + * @param directFormType + * The filter topology + */ + public void BandStop(int order, double sampleRate, double centerFrequency, + double widthFrequency, double rippleDb, int directFormType) { + SetupBandStop(order, sampleRate, centerFrequency, widthFrequency, + rippleDb, directFormType); + } + + private void SetupBandPass(int order, double sampleRate, + double centerFrequency, double widthFrequency, double rippleDb, + int directFormType) { + + var analogProto = new AnalogLowPass(order); + analogProto.Design(rippleDb); + + var digitalProto = new LayoutBase(order * 2); + + new BandPassTransform(centerFrequency / sampleRate, widthFrequency + / sampleRate, digitalProto, analogProto); + + SetLayout(digitalProto, directFormType); + + } + + /** + * Bandpass filter with default topology + * + * @param order + * Filter order + * @param sampleRate + * Sampling rate + * @param centerFrequency + * Center frequency + * @param widthFrequency + * Width of the notch + * @param rippleDb + * passband ripple in decibel sensible value: 1dB + */ + public void BandPass(int order, double sampleRate, double centerFrequency, + double widthFrequency, double rippleDb) { + SetupBandPass(order, sampleRate, centerFrequency, widthFrequency, + rippleDb, DirectFormAbstract.DirectFormII); + } + + /** + * Bandpass filter with custom topology + * + * @param order + * Filter order + * @param sampleRate + * Sampling rate + * @param centerFrequency + * Center frequency + * @param widthFrequency + * Width of the notch + * @param rippleDb + * passband ripple in decibel sensible value: 1dB + * @param directFormType + * The filter topology (see DirectFormAbstract) + */ + public void BandPass(int order, double sampleRate, double centerFrequency, + double widthFrequency, double rippleDb, int directFormType) { + SetupBandPass(order, sampleRate, centerFrequency, widthFrequency, + rippleDb, directFormType); + } + +} diff --git a/src/Spice86.Core/Backend/Audio/Iir/ChebyshevII.cs b/src/Spice86.Core/Backend/Audio/Iir/ChebyshevII.cs new file mode 100644 index 0000000000..41b6bfa3a5 --- /dev/null +++ b/src/Spice86.Core/Backend/Audio/Iir/ChebyshevII.cs @@ -0,0 +1,265 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Copyright (c) 2009 by Vinnie Falco + * Copyright (c) 2016 by Bernd Porr + */ + +using System.Numerics; + +namespace Spice86.Core.Backend.Audio.Iir; + +/** + * User facing class which contains all the methods the user uses to create + * ChebyshevI filters. This done in this way: ChebyshevII chebyshevII = new + * ChebyshevII(); Then call one of the methods below to create low-,high-,band-, + * or stopband filters. For example: chebyshevII.bandPass(2,250,50,5,0.5); + */ +public class ChebyshevII : Cascade { + class AnalogLowPass : LayoutBase { + private readonly int _nPoles; + + public AnalogLowPass(int nPoles) : base(nPoles) { + _nPoles = nPoles; + } + + public void Design(double stopBandDb) { + Reset(); + + double eps = Math.Sqrt(1.0 / (Math.Exp(stopBandDb * 0.1 * MathSupplement.DoubleLn10) - 1)); + double v0 = MathSupplement.Asinh(1 / eps) / _nPoles; + double sinhV0 = -Math.Sinh(v0); + double coshV0 = Math.Cosh(v0); + double fn = Math.PI / (2 * _nPoles); + + int k = 1; + for (int i = _nPoles / 2; --i >= 0; k += 2) { + double a = sinhV0 * Math.Cos((k - _nPoles) * fn); + double b = coshV0 * Math.Sin((k - _nPoles) * fn); + double d2 = a * a + b * b; + double im = 1 / Math.Cos(k * fn); + var pole = new Complex(a / d2, b / d2); + var zero = new Complex(0.0, im); + AddPoleZeroConjugatePairs(pole, zero); + } + + if ((_nPoles & 1) == 1) { + Add(new Complex(1 / sinhV0, 0), new Complex(double.PositiveInfinity, 0)); + } + SetNormal(0, 1); + } + } + + private void SetupLowPass(int order, double sampleRate, double cutoffFrequency, double rippleDb, + int directFormType) { + + var analogProto = new AnalogLowPass(order); + analogProto.Design(rippleDb); + + var digitalProto = new LayoutBase(order); + + new LowPassTransform(cutoffFrequency / sampleRate, digitalProto, analogProto); + + SetLayout(digitalProto, directFormType); + } + + /** + * ChebyshevI Lowpass filter with default toplogy + * + * @param order + * The order of the filter + * @param sampleRate + * The sampling rate of the system + * @param cutoffFrequency + * the cutoff frequency + * @param rippleDb + * passband ripple in decibel sensible value: 1dB + */ + public void LowPass(int order, double sampleRate, double cutoffFrequency, double rippleDb) { + SetupLowPass(order, sampleRate, cutoffFrequency, rippleDb, DirectFormAbstract.DirectFormII); + } + + /** + * ChebyshevI Lowpass filter with custom topology + * + * @param order + * The order of the filter + * @param sampleRate + * The sampling rate of the system + * @param cutoffFrequency + * The cutoff frequency + * @param rippleDb + * passband ripple in decibel sensible value: 1dB + * @param directFormType + * The filter topology. This is either + * DirectFormAbstract.DIRECT_FORM_I or DIRECT_FORM_II + */ + public void LowPass(int order, double sampleRate, double cutoffFrequency, double rippleDb, int directFormType) { + SetupLowPass(order, sampleRate, cutoffFrequency, rippleDb, directFormType); + } + + private void SetupHighPass(int order, double sampleRate, double cutoffFrequency, double rippleDb, + int directFormType) { + + var analogProto = new AnalogLowPass(order); + analogProto.Design(rippleDb); + + var digitalProto = new LayoutBase(order); + + new HighPassTransform(cutoffFrequency / sampleRate, digitalProto, analogProto); + + SetLayout(digitalProto, directFormType); + } + + /** + * ChebyshevI Highpass filter with default topology + * + * @param order + * The order of the filter + * @param sampleRate + * The sampling rate of the system + * @param cutoffFrequency + * the cutoff frequency + * @param rippleDb + * passband ripple in decibel sensible value: 1dB + */ + public void HighPass(int order, double sampleRate, double cutoffFrequency, double rippleDb) { + SetupHighPass(order, sampleRate, cutoffFrequency, rippleDb, DirectFormAbstract.DirectFormII); + } + + /** + * ChebyshevI Lowpass filter and custom filter topology + * + * @param order + * The order of the filter + * @param sampleRate + * The sampling rate of the system + * @param cutoffFrequency + * The cutoff frequency + * @param rippleDb + * passband ripple in decibel sensible value: 1dB + * @param directFormType + * The filter topology. This is either + * DirectFormAbstract.DIRECT_FORM_I or DIRECT_FORM_II + */ + public void HighPass(int order, double sampleRate, double cutoffFrequency, double rippleDb, int directFormType) { + SetupHighPass(order, sampleRate, cutoffFrequency, rippleDb, directFormType); + } + + private void SetupBandStop(int order, double sampleRate, double centerFrequency, double widthFrequency, + double rippleDb, int directFormType) { + + var analogProto = new AnalogLowPass(order); + analogProto.Design(rippleDb); + + var digitalProto = new LayoutBase(order * 2); + + new BandStopTransform(centerFrequency / sampleRate, widthFrequency / sampleRate, digitalProto, analogProto); + + SetLayout(digitalProto, directFormType); + } + + /** + * Bandstop filter with default topology + * + * @param order + * Filter order (actual order is twice) + * @param sampleRate + * Samping rate of the system + * @param centerFrequency + * Center frequency + * @param widthFrequency + * Width of the notch + * @param rippleDb + * passband ripple in decibel sensible value: 1dB + */ + public void BandStop(int order, double sampleRate, double centerFrequency, double widthFrequency, double rippleDb) { + SetupBandStop(order, sampleRate, centerFrequency, widthFrequency, rippleDb, DirectFormAbstract.DirectFormII); + } + + /** + * Bandstop filter with custom topology + * + * @param order + * Filter order (actual order is twice) + * @param sampleRate + * Samping rate of the system + * @param centerFrequency + * Center frequency + * @param widthFrequency + * Width of the notch + * @param rippleDb + * passband ripple in decibel sensible value: 1dB + * @param directFormType + * The filter topology + */ + public void BandStop(int order, double sampleRate, double centerFrequency, double widthFrequency, double rippleDb, + int directFormType) { + SetupBandStop(order, sampleRate, centerFrequency, widthFrequency, rippleDb, directFormType); + } + + private void SetupBandPass(int order, double sampleRate, double centerFrequency, double widthFrequency, + double rippleDb, int directFormType) { + + var analogProto = new AnalogLowPass(order); + analogProto.Design(rippleDb); + + var digitalProto = new LayoutBase(order * 2); + + new BandPassTransform(centerFrequency / sampleRate, widthFrequency / sampleRate, digitalProto, analogProto); + + SetLayout(digitalProto, directFormType); + } + + /** + * Bandpass filter with default topology + * + * @param order + * Filter order + * @param sampleRate + * Sampling rate + * @param centerFrequency + * Center frequency + * @param widthFrequency + * Width of the notch + * @param rippleDb + * passband ripple in decibel sensible value: 1dB + */ + public void BandPass(int order, double sampleRate, double centerFrequency, double widthFrequency, double rippleDb) { + SetupBandPass(order, sampleRate, centerFrequency, widthFrequency, rippleDb, DirectFormAbstract.DirectFormII); + } + + /** + * Bandpass filter with custom topology + * + * @param order + * Filter order + * @param sampleRate + * Sampling rate + * @param centerFrequency + * Center frequency + * @param widthFrequency + * Width of the notch + * @param rippleDb + * passband ripple in decibel sensible value: 1dB + * @param directFormType + * The filter topology (see DirectFormAbstract) + */ + public void BandPass(int order, double sampleRate, double centerFrequency, double widthFrequency, double rippleDb, + int directFormType) { + SetupBandPass(order, sampleRate, centerFrequency, widthFrequency, rippleDb, directFormType); + } +} diff --git a/src/Spice86.Core/Backend/Audio/Iir/ComplexExtensions.cs b/src/Spice86.Core/Backend/Audio/Iir/ComplexExtensions.cs new file mode 100644 index 0000000000..577b04117a --- /dev/null +++ b/src/Spice86.Core/Backend/Audio/Iir/ComplexExtensions.cs @@ -0,0 +1,11 @@ +using System.Numerics; + +namespace Spice86.Core.Backend.Audio.Iir; + +internal static class ComplexExtensions { + public static Complex Add(this Complex a, Complex b) => Complex.Add(a, b); + public static Complex Multiply(this Complex a, Complex b) => Complex.Multiply(a, b); + public static Complex Divide(this Complex a, Complex b) => Complex.Divide(a, b); + public static Complex Subtract(this Complex a, Complex b) => Complex.Subtract(a, b); + public static Complex Sqrt(this Complex a) => Complex.Sqrt(a); +} \ No newline at end of file diff --git a/src/Spice86.Core/Backend/Audio/Iir/ComplexPair.cs b/src/Spice86.Core/Backend/Audio/Iir/ComplexPair.cs new file mode 100644 index 0000000000..9802d8c0de --- /dev/null +++ b/src/Spice86.Core/Backend/Audio/Iir/ComplexPair.cs @@ -0,0 +1,68 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Copyright (c) 2009 by Vinnie Falco + * Copyright (c) 2016 by Bernd Porr + */ + + +using System.Numerics; + +namespace Spice86.Core.Backend.Audio.Iir; + +/** + * + * A complex pair + * + */ +public class ComplexPair { + public Complex First { get; } + public Complex Second { get; } + + public ComplexPair(Complex c1, Complex c2) { + First = c1; + Second = c2; + } + + public ComplexPair(Complex c1) { + First = c1; + Second = new Complex(0, 0); + } + + public bool IsConjugate() { + return Second.Equals(Complex.Conjugate(First)); + } + + public bool IsReal() { + return First.Imaginary == 0 && Second.Imaginary == 0; + } + + // Returns true if this is either a conjugate pair, + // or a pair of reals where neither is zero. + public bool IsMatchedPair() { + if (First.Imaginary != 0) { + return Second.Equals(Complex.Conjugate(First)); + } else { + return Second.Imaginary == 0 && + Second.Real != 0 && + First.Real != 0; + } + } + + public bool IsNaN() { + return Complex.IsNaN(First) || Complex.IsNaN(Second); + } +}; diff --git a/src/Spice86.Core/Backend/Audio/Iir/ComplexUtils.cs b/src/Spice86.Core/Backend/Audio/Iir/ComplexUtils.cs new file mode 100644 index 0000000000..4948a29af4 --- /dev/null +++ b/src/Spice86.Core/Backend/Audio/Iir/ComplexUtils.cs @@ -0,0 +1,23 @@ +using System.Numerics; + +namespace Spice86.Core.Backend.Audio.Iir; + +public static class ComplexUtils { + public static Complex PolarToComplex(double r, double theta) { + if (r < 0.0) { + throw new ArgumentException(r.ToString(), nameof(r)); + } else { + return new Complex(r * Math.Cos(theta), r * Math.Sin(theta)); + } + } + + public static Complex[] ConvertToComplex(double[] real) { + var c = new Complex[real.Length]; + + for (int i = 0; i < real.Length; ++i) { + c[i] = new Complex(real[i], 0.0); + } + + return c; + } +} diff --git a/src/Spice86.Core/Backend/Audio/Iir/DirectFormAbstract.cs b/src/Spice86.Core/Backend/Audio/Iir/DirectFormAbstract.cs new file mode 100644 index 0000000000..be56908aeb --- /dev/null +++ b/src/Spice86.Core/Backend/Audio/Iir/DirectFormAbstract.cs @@ -0,0 +1,39 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Copyright (c) 2009 by Vinnie Falco + * Copyright (c) 2016 by Bernd Porr + */ + +namespace Spice86.Core.Backend.Audio.Iir; + +/** + * Abstract form of the a filter which can have different state variables + * + * Direct form I or II is derived from it + */ +public abstract class DirectFormAbstract { + protected DirectFormAbstract() { + Reset(); + } + + public abstract void Reset(); + + public abstract double Process1(double x, Biquad s); + + public const int DirectFormI = 0; + public const int DirectFormII = 1; +} diff --git a/src/Spice86.Core/Backend/Audio/Iir/DirectFormI.cs b/src/Spice86.Core/Backend/Audio/Iir/DirectFormI.cs new file mode 100644 index 0000000000..cdacb8a612 --- /dev/null +++ b/src/Spice86.Core/Backend/Audio/Iir/DirectFormI.cs @@ -0,0 +1,56 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Copyright (c) 2009 by Vinnie Falco + * Copyright (c) 2016 by Bernd Porr + */ + +namespace Spice86.Core.Backend.Audio.Iir; + +/** + * + * Implementation of a Direct Form I filter with its states. The coefficients + * are supplied from the outside. + * + */ +public class DirectFormI : DirectFormAbstract { + public DirectFormI() { + Reset(); + } + + public override void Reset() { + _x1 = 0; + _x2 = 0; + _y1 = 0; + _y2 = 0; + } + + public override double Process1(double x, Biquad s) { + double res = s.B0 * x + s.B1 * _x1 + s.B2 * _x2 + - s.A1 * _y1 - s.A2 * _y2; + _x2 = _x1; + _y2 = _y1; + _x1 = x; + _y1 = res; + + return res; + } + + double _x2; // x[n-2] + double _y2; // y[n-2] + double _x1; // x[n-1] + double _y1; // y[n-1] +}; diff --git a/src/Spice86.Core/Backend/Audio/Iir/DirectFormII.cs b/src/Spice86.Core/Backend/Audio/Iir/DirectFormII.cs new file mode 100644 index 0000000000..678e6ab83e --- /dev/null +++ b/src/Spice86.Core/Backend/Audio/Iir/DirectFormII.cs @@ -0,0 +1,57 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Copyright (c) 2009 by Vinnie Falco + * Copyright (c) 2016 by Bernd Porr + */ + +namespace Spice86.Core.Backend.Audio.Iir; + +/** + * + * Implementation of a Direct Form II filter with its states. The coefficients + * are supplied from the outside. + * + */ + +public class DirectFormII : DirectFormAbstract { + public DirectFormII() { + Reset(); + } + + public override void Reset() { + _v1 = 0; + _v2 = 0; + } + + public override double Process1(double x, + Biquad s) { + if (s != null) { + double w = x - s.A1 * _v1 - s.A2 * _v2; + double res = s.B0 * w + s.B1 * _v1 + s.B2 * _v2; + + _v2 = _v1; + _v1 = w; + + return res; + } else { + return x; + } + } + + double _v1; // v[-1] + double _v2; // v[-2] +} diff --git a/src/Spice86.Core/Backend/Audio/Iir/HighPassTransform.cs b/src/Spice86.Core/Backend/Audio/Iir/HighPassTransform.cs new file mode 100644 index 0000000000..69981e6666 --- /dev/null +++ b/src/Spice86.Core/Backend/Audio/Iir/HighPassTransform.cs @@ -0,0 +1,75 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Copyright (c) 2009 by Vinnie Falco + * Copyright (c) 2016 by Bernd Porr + */ + +using System.Numerics; + +namespace Spice86.Core.Backend.Audio.Iir; + +/** + * Transforms from an analogue lowpass filter to a digital highpass filter + */ +public class HighPassTransform { + + private readonly double f; + + public HighPassTransform(double fc, LayoutBase digital, LayoutBase analog) { + digital.Reset(); + + if (fc < 0) { + throw new ArithmeticException("Cutoff frequency cannot be negative."); + } + + if (!(fc < 0.5)) { + throw new ArithmeticException("Cutoff frequency must be less than the Nyquist frequency."); + } + + // prewarp + f = 1.0d / Math.Tan(Math.PI * fc); + + int numPoles = analog.NumPoles; + int pairs = numPoles / 2; + for (int i = 0; i < pairs; ++i) { + PoleZeroPair pair = analog.GetPair(i); + digital.AddPoleZeroConjugatePairs(Transform(pair.poles.First), + Transform(pair.zeros.First)); + } + + if ((numPoles & 1) == 1) { + PoleZeroPair pair = analog.GetPair(pairs); + digital.Add(Transform(pair.poles.First), + Transform(pair.zeros.First)); + } + + digital.SetNormal(Math.PI - analog.NormalW, analog.NormalGain); + } + + private Complex Transform(Complex c) { + if (Complex.IsInfinity(c)) { + return new Complex(1, 0); + } + + // frequency transform + c = Complex.Multiply(c, f); + + // bilinear high pass transform + return new Complex(-1, 0).Multiply(new Complex(1, 0).Add(c)).Divide( + new Complex(1, 0).Subtract(c)); + } +} diff --git a/src/Spice86.Core/Backend/Audio/Iir/LayoutBase.cs b/src/Spice86.Core/Backend/Audio/Iir/LayoutBase.cs new file mode 100644 index 0000000000..81c193caba --- /dev/null +++ b/src/Spice86.Core/Backend/Audio/Iir/LayoutBase.cs @@ -0,0 +1,86 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Copyright (c) 2009 by Vinnie Falco + * Copyright (c) 2016 by Bernd Porr + */ + +using System.Numerics; + +namespace Spice86.Core.Backend.Audio.Iir; + +/** + * + * Digital/analogue filter coefficient storage space organising the + * storage as PoleZeroPairs so that we have as always a 2nd order filter + * + */ +public class LayoutBase { + private int _numPoles; + private readonly PoleZeroPair[] _pair; + private double _normalW; + private double _normalGain; + + public LayoutBase(PoleZeroPair[] pairs) { + _numPoles = pairs.Length * 2; + _pair = pairs; + } + + public LayoutBase(int numPoles) { + _numPoles = 0; + if (numPoles % 2 == 1) { + _pair = new PoleZeroPair[numPoles / 2 + 1]; + } else { + _pair = new PoleZeroPair[numPoles / 2]; + } + } + + public void Reset() { + _numPoles = 0; + } + + public int NumPoles => _numPoles; + + public void Add(Complex pole, Complex zero) { + _pair[_numPoles / 2] = new PoleZeroPair(pole, zero); + ++_numPoles; + } + + public void AddPoleZeroConjugatePairs(Complex pole, Complex zero) { + _pair[_numPoles / 2] = new PoleZeroPair(pole, zero, Complex.Conjugate(pole), + Complex.Conjugate(zero)); + _numPoles += 2; + } + + public void Add(ComplexPair poles, ComplexPair zeros) { + _pair[_numPoles / 2] = new PoleZeroPair(poles.First, zeros.First, + poles.Second, zeros.Second); + _numPoles += 2; + } + + public PoleZeroPair GetPair(int pairIndex) { + return _pair[pairIndex]; + } + + public double NormalW => _normalW; + + public double NormalGain => _normalGain; + + public void SetNormal(double w, double g) { + _normalW = w; + _normalGain = g; + } +}; diff --git a/src/Spice86.Core/Backend/Audio/Iir/LowPassTransform.cs b/src/Spice86.Core/Backend/Audio/Iir/LowPassTransform.cs new file mode 100644 index 0000000000..ba52b41cd4 --- /dev/null +++ b/src/Spice86.Core/Backend/Audio/Iir/LowPassTransform.cs @@ -0,0 +1,75 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Copyright (c) 2009 by Vinnie Falco + * Copyright (c) 2016 by Bernd Porr + */ + +using System.Numerics; + +namespace Spice86.Core.Backend.Audio.Iir; + +/** + * Transforms from an analogue lowpass filter to a digital lowpass filter + */ +public class LowPassTransform { + private readonly double _f; + + private Complex Transform(Complex c) { + if (Complex.IsInfinity(c)) { + return new Complex(-1, 0); + } + + // frequency transform + c = Complex.Multiply(c, _f); + + var one = new Complex(1, 0); + + // bilinear low pass transform + return Complex.Divide(Complex.Add(c, one), Complex.Subtract(c, one)); + } + + public LowPassTransform(double fc, LayoutBase digital, LayoutBase analog) { + digital.Reset(); + + if (fc < 0) { + throw new ArithmeticException("Cutoff frequency cannot be negative."); + } + + if (!(fc < 0.5)) { + throw new ArithmeticException("Cutoff frequency must be less than the Nyquist frequency."); + } + + // prewarp + _f = Math.Tan(Math.PI * fc); + + int numPoles = analog.NumPoles; + int pairs = numPoles / 2; + for (int i = 0; i < pairs; ++i) { + PoleZeroPair pair = analog.GetPair(i); + digital.AddPoleZeroConjugatePairs(Transform(pair.poles.First), + Transform(pair.zeros.First)); + } + + if ((numPoles & 1) == 1) { + PoleZeroPair pair = analog.GetPair(pairs); + digital.Add(Transform(pair.poles.First), + Transform(pair.zeros.First)); + } + + digital.SetNormal(analog.NormalW, analog.NormalGain); + } +} diff --git a/src/Spice86.Core/Backend/Audio/Iir/MathSupplement.cs b/src/Spice86.Core/Backend/Audio/Iir/MathSupplement.cs new file mode 100644 index 0000000000..d0e93ed25d --- /dev/null +++ b/src/Spice86.Core/Backend/Audio/Iir/MathSupplement.cs @@ -0,0 +1,72 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Copyright (c) 2009 by Vinnie Falco + * Copyright (c) 2016 by Bernd Porr + */ + +using System.Numerics; + +namespace Spice86.Core.Backend.Audio.Iir; + +/** + * + * Useful math functions which come back over and over again + * + */ +public static class MathSupplement { + public const double DoublePi = 3.1415926535897932384626433832795028841971; + public const double DoublePi2 = 1.5707963267948966192313216916397514420986; + public const double DoubleLn2 = 0.69314718055994530941723212145818; + public const double DoubleLn10 = 2.3025850929940456840179914546844; + + public static Complex SolveQuadratic1(double a, double b, double c) { + return new Complex(-b, 0).Add(new Complex(b * b - 4 * a * c, 0)).Sqrt() + .Divide(2.0 * a); + } + + public static Complex SolveQuadratic2(double a, double b, double c) { + return new Complex(-b, 0).Subtract(new Complex(b * b - 4 * a * c, 0)) + .Sqrt().Divide(2.0 * a); + } + + public static Complex AdjustImage(Complex c) { + if (Math.Abs(c.Imaginary) < 1e-30) { + return new Complex(c.Real, 0); + } else { + return c; + } + } + + public static Complex AddMul(Complex c, double v, Complex c1) { + return new Complex(c.Real + v * c1.Real, c.Imaginary + v + * c1.Imaginary); + } + + public static Complex Recip(Complex c) { + double n = 1.0 / (Complex.Abs(c) * Complex.Abs(c)); + + return new Complex(n * c.Real, n * c.Imaginary); + } + + public static double Asinh(double x) { + return Math.Log(x + Math.Sqrt(x * x + 1)); + } + + public static double Acosh(double x) { + return Math.Log(x + Math.Sqrt(x * x - 1)); + } +} diff --git a/src/Spice86.Core/Backend/Audio/Iir/PoleZeroPair.cs b/src/Spice86.Core/Backend/Audio/Iir/PoleZeroPair.cs new file mode 100644 index 0000000000..90cb7e36b1 --- /dev/null +++ b/src/Spice86.Core/Backend/Audio/Iir/PoleZeroPair.cs @@ -0,0 +1,54 @@ +namespace Spice86.Core.Backend.Audio.Iir; + +using System.Numerics; + +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Copyright (c) 2009 by Vinnie Falco + * Copyright (c) 2016 by Bernd Porr + */ + +/** + * + * It's written on the tin. + * + */ +public class PoleZeroPair { + public ComplexPair poles; + public ComplexPair zeros; + + // single pole/zero + public PoleZeroPair(Complex p, Complex z) { + poles = new ComplexPair(p); + zeros = new ComplexPair(z); + } + + // pole/zero pair + public PoleZeroPair(Complex p1, Complex z1, Complex p2, Complex z2) { + poles = new ComplexPair(p1, p2); + zeros = new ComplexPair(z1, z2); + } + + public bool IsSinglePole() { + return poles.Second.Equals(new Complex(0, 0)) + && zeros.Second.Equals(new Complex(0, 0)); + } + + public bool IsNaN() { + return poles.IsNaN() || zeros.IsNaN(); + } +}; diff --git a/src/Spice86.Core/Backend/Audio/Iir/RBJ.cs b/src/Spice86.Core/Backend/Audio/Iir/RBJ.cs new file mode 100644 index 0000000000..b0a7d4b2dd --- /dev/null +++ b/src/Spice86.Core/Backend/Audio/Iir/RBJ.cs @@ -0,0 +1,313 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Copyright (c) 2009 by Vinnie Falco + * Copyright (c) 2016 by Bernd Porr + */ +namespace Spice86.Core.Backend.Audio.Iir; + +/** + * Filter realizations based on Robert Bristol-Johnson formulae: + * + * http://www.musicdsp.org/files/Audio-EQ-Cookbook.txt + * + * These are all 2nd order filters which are tuned with the Q (or Quality factor). + * The Q factor causes a resonance at the cutoff frequency. The higher the Q + * factor the higher the responance. If 0.5 < Q < 1/sqrt(2) then there is no resonance peak. + * Above 1/sqrt(2) the peak becomes more and more pronounced. For bandpass and stopband + * the Q factor is replaced by the width of the filter. The higher Q the more narrow + * the bandwidth of the notch or bandpass. + * + **/ + +public class RBJBase : Biquad { + private readonly DirectFormI _state = new(); + + public double Filter(double s) { + return _state.Process1(s, this); + } + + public DirectFormI GetState() { + return _state; + } +} + +public class LowPass : RBJBase { + private const double ONESQRT2 = 0.707106781; + + public void SetupN( + double cutoffFrequency, + double q = ONESQRT2) { + double w0 = 2 * MathSupplement.DoublePi * cutoffFrequency; + double cs = Math.Cos(w0); + double sn = Math.Sin(w0); + double AL = sn / (2 * q); + double b0 = (1 - cs) / 2; + double b1 = 1 - cs; + double b2 = (1 - cs) / 2; + double a0 = 1 + AL; + double a1 = -2 * cs; + double a2 = 1 - AL; + SetCoefficients(a0, a1, a2, b0, b1, b2); + } + + public void Setup( + double sampleRate, + double cutoffFrequency, + double q = ONESQRT2) { + SetupN(cutoffFrequency / sampleRate, q); + } +} + +public class HighPass : RBJBase { + private const double ONESQRT2 = 0.707106781; + + public void SetupN( + double cutoffFrequency, + double q = ONESQRT2) { + double w0 = 2 * MathSupplement.DoublePi * cutoffFrequency; + double cs = Math.Cos(w0); + double sn = Math.Sin(w0); + double AL = sn / (2 * q); + double b0 = (1 + cs) / 2; + double b1 = -(1 + cs); + double b2 = (1 + cs) / 2; + double a0 = 1 + AL; + double a1 = -2 * cs; + double a2 = 1 - AL; + SetCoefficients(a0, a1, a2, b0, b1, b2); + } + + public void Setup( + double sampleRate, + double cutoffFrequency, + double q = ONESQRT2) { + SetupN(cutoffFrequency / sampleRate, q); + } +} + +public class BandPass1 : RBJBase { + public void SetupN( + double centerFrequency, + double bandWidth) { + double w0 = 2 * MathSupplement.DoublePi * centerFrequency; + double cs = Math.Cos(w0); + double sn = Math.Sin(w0); + double AL = sn / (2 * bandWidth); + double b0 = bandWidth * AL;// sn / 2; + double b1 = 0; + double b2 = -bandWidth * AL;//-sn / 2; + double a0 = 1 + AL; + double a1 = -2 * cs; + double a2 = 1 - AL; + SetCoefficients(a0, a1, a2, b0, b1, b2); + } + + public void Setup( + double sampleRate, + double centerFrequency, + double bandWidth) { + SetupN(centerFrequency / sampleRate, bandWidth); + } +} + +public class BandPass2 : RBJBase { + public void SetupN( + double centerFrequency, + double bandWidth) { + double w0 = 2 * MathSupplement.DoublePi * centerFrequency; + double cs = Math.Cos(w0); + double sn = Math.Sin(w0); + double AL = sn / (2 * bandWidth); + double b0 = AL; + double b1 = 0; + double b2 = -AL; + double a0 = 1 + AL; + double a1 = -2 * cs; + double a2 = 1 - AL; + SetCoefficients(a0, a1, a2, b0, b1, b2); + } + + public void Setup( + double sampleRate, + double centerFrequency, + double bandWidth) { + SetupN(centerFrequency / sampleRate, bandWidth); + } +} + +public class BandStop : RBJBase { + public void SetupN( + double centerFrequency, + double bandWidth) { + double w0 = 2 * MathSupplement.DoublePi * centerFrequency; + double cs = Math.Cos(w0); + double sn = Math.Sin(w0); + double AL = sn / (2 * bandWidth); + double b0 = 1; + double b1 = -2 * cs; + double b2 = 1; + double a0 = 1 + AL; + double a1 = -2 * cs; + double a2 = 1 - AL; + SetCoefficients(a0, a1, a2, b0, b1, b2); + } + + public void Setup( + double sampleRate, + double centerFrequency, + double bandWidth) { + SetupN(centerFrequency / sampleRate, bandWidth); + } +} + +public class IIRNotch : RBJBase { + public void SetupN( + double centerFrequency, + double q_factor = 10) { + double w0 = 2 * MathSupplement.DoublePi * centerFrequency; + double cs = Math.Cos(w0); + double r = Math.Exp(-(w0 / 2) / q_factor); + const double b0 = 1; + double b1 = -2 * cs; + const double b2 = 1; + const double a0 = 1; + double a1 = -2 * r * cs; + double a2 = r * r; + SetCoefficients(a0, a1, a2, b0, b1, b2); + } + + public void Setup( + double sampleRate, + double centerFrequency, + double q_factor = 10) { + SetupN(centerFrequency / sampleRate, q_factor); + } +} + + +public class LowShelf : RBJBase { + public void SetupN( + double cutoffFrequency, + double gainDb, + double shelfSlope = 1) { + double A = Math.Pow(10, gainDb / 40); + double w0 = 2 * MathSupplement.DoublePi * cutoffFrequency; + double cs = Math.Cos(w0); + double sn = Math.Sin(w0); + double AL = sn / 2 * Math.Sqrt((A + 1 / A) * (1 / shelfSlope - 1) + 2); + double sq = 2 * Math.Sqrt(A) * AL; + double b0 = A * (A + 1 - (A - 1) * cs + sq); + double b1 = 2 * A * (A - 1 - (A + 1) * cs); + double b2 = A * (A + 1 - (A - 1) * cs - sq); + double a0 = A + 1 + (A - 1) * cs + sq; + double a1 = -2 * (A - 1 + (A + 1) * cs); + double a2 = A + 1 + (A - 1) * cs - sq; + SetCoefficients(a0, a1, a2, b0, b1, b2); + } + + public void Setup(double sampleRate, + double cutoffFrequency, + double gainDb, + double shelfSlope = 1) { + SetupN(cutoffFrequency / sampleRate, gainDb, shelfSlope); + } +} + + +public class HighShelf : RBJBase { + public void SetupN( + double cutoffFrequency, + double gainDb, + double shelfSlope = 1) { + double A = Math.Pow(10, gainDb / 40); + double w0 = 2 * MathSupplement.DoublePi * cutoffFrequency; + double cs = Math.Cos(w0); + double sn = Math.Sin(w0); + double AL = sn / 2 * Math.Sqrt((A + 1 / A) * (1 / shelfSlope - 1) + 2); + double sq = 2 * Math.Sqrt(A) * AL; + double b0 = A * (A + 1 + (A - 1) * cs + sq); + double b1 = -2 * A * (A - 1 + (A + 1) * cs); + double b2 = A * (A + 1 + (A - 1) * cs - sq); + double a0 = A + 1 - (A - 1) * cs + sq; + double a1 = 2 * (A - 1 - (A + 1) * cs); + double a2 = A + 1 - (A - 1) * cs - sq; + SetCoefficients(a0, a1, a2, b0, b1, b2); + } + + public void Setup(double sampleRate, + double cutoffFrequency, + double gainDb, + double shelfSlope = 1) { + SetupN(cutoffFrequency / sampleRate, gainDb, shelfSlope); + } +} + +public class BandShelf : RBJBase { + public void SetupN( + double centerFrequency, + double gainDb, + double bandWidth) { + double A = Math.Pow(10, gainDb / 40); + double w0 = 2 * MathSupplement.DoublePi * centerFrequency; + double cs = Math.Cos(w0); + double sn = Math.Sin(w0); + double AL = sn * Math.Sinh(MathSupplement.DoubleLn2 / 2 * bandWidth * w0 / sn); + if (double.IsNaN(AL)) { + throw new("No solution available for these parameters.\n"); + } + double b0 = 1 + AL * A; + double b1 = -2 * cs; + double b2 = 1 - AL * A; + double a0 = 1 + AL / A; + double a1 = -2 * cs; + double a2 = 1 - AL / A; + SetCoefficients(a0, a1, a2, b0, b1, b2); + } + + public void Setup(double sampleRate, + double centerFrequency, + double gainDb, + double bandWidth) { + SetupN(centerFrequency / sampleRate, gainDb, bandWidth); + } +} + +public class AllPass : RBJBase { + private const double OneSqrtTwo = 0.707106781; + + public void SetupN( + double phaseFrequency, + double q = OneSqrtTwo) { + double w0 = 2 * MathSupplement.DoublePi * phaseFrequency; + double cs = Math.Cos(w0); + double sn = Math.Sin(w0); + double AL = sn / (2 * q); + double b0 = 1 - AL; + double b1 = -2 * cs; + double b2 = 1 + AL; + double a0 = 1 + AL; + double a1 = -2 * cs; + double a2 = 1 - AL; + SetCoefficients(a0, a1, a2, b0, b1, b2); + } + + public void Setup(double sampleRate, + double phaseFrequency, + double q = OneSqrtTwo) { + SetupN(phaseFrequency / sampleRate, q); + } +} \ No newline at end of file diff --git a/src/Spice86.Core/Backend/Audio/Iir/SOSCascade.cs b/src/Spice86.Core/Backend/Audio/Iir/SOSCascade.cs new file mode 100644 index 0000000000..0dd77b65b3 --- /dev/null +++ b/src/Spice86.Core/Backend/Audio/Iir/SOSCascade.cs @@ -0,0 +1,63 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Copyright (c) 2009 by Vinnie Falco + * Copyright (c) 2021 by Bernd Porr + */ + +namespace Spice86.Core.Backend.Audio.Iir; + +/** + * User facing class which contains two methods + * to create Custom SOS filters, in particular where + * the filter coefficients have been generated by + * Python's scipy signal library for example: + * sos = signal.butter(4, 0.1, output='sos'). + * Call one of the setup() methods below to + * set the SOS coefficients. See the unit + * test for examples how to set the coefficients. + */ +public class SOSCascade : Cascade { + /** + * Sets directly the coefficients of the chain of + * 2nd order filters. The layout of the array is + * excatly how the scipy python design functions + * output the sos coeffcients: + * [b0,b1,b2,a0,a1,a2],[b0,b1,b2,a0,a1,a2],... + * The filter type can be either DirectFormAbstract.DIRECT_FORM_II + * or DirectFormAbstract.DIRECT_FORM_I. + * @param sosCoefficients SOS coefficients + * @param directFormType Direct form type (I or II). + **/ + public void Setup( + double[][] sosCoefficients, + int directFormType) { + SetSOScoeff(sosCoefficients, directFormType); + } + + /** + * Sets directly the coefficients of the chain of + * 2nd order filters. The layout of the array is + * excatly how the scipy python design functions + * output the coeffcients: + * [b0,b1,b2,a0,a1,a2],[b0,b1,b2,a0,a1,a2],... + * The filter type is DIRECT_FORM_II. + * @param sosCoefficients SOS coefficients + **/ + public void Setup(double[][] sosCoefficients) { + SetSOScoeff(sosCoefficients, DirectFormAbstract.DirectFormII); + } +} diff --git a/src/Spice86.Core/Emulator/Devices/Sound/AdlibGold.cs b/src/Spice86.Core/Emulator/Devices/Sound/AdlibGold.cs new file mode 100644 index 0000000000..c8bad19efb --- /dev/null +++ b/src/Spice86.Core/Emulator/Devices/Sound/AdlibGold.cs @@ -0,0 +1,358 @@ +namespace Spice86.Core.Emulator.Devices.Sound; + +using Spice86.Core.Backend.Audio.Iir; +using Spice86.Core.Emulator.Devices.Sound.Ym7128b; +using Spice86.Shared.Interfaces; + +using System.Runtime.InteropServices; + +using HighShelf = Spice86.Core.Backend.Audio.IirFilters.HighShelf; +using LowShelf = Spice86.Core.Backend.Audio.IirFilters.LowShelf; + +/// +/// Adlib Gold implementation, translated from DOSBox Staging code +/// +public sealed class AdlibGold { + private readonly StereoProcessor _stereoProcessor; + private readonly SurroundProcessor _surroundProcessor; + private readonly ushort _sampleRate; + + public AdlibGold(ILoggerService loggerService) { + _sampleRate = 48000; + _stereoProcessor = new(_sampleRate, loggerService); + _surroundProcessor = new(_sampleRate); + } + + public void StereoControlWrite(byte reg, byte data) => _stereoProcessor.ControlWrite((StereoProcessorControlReg)reg, data); + + public void SurroundControlWrite(byte data) => _surroundProcessor.ControlWrite(data); + + private void Process(short[] input, uint framesRemaining, AudioFrame output) { + for (var index = 0; framesRemaining-- > 0; index++) { + AudioFrame frame = new(output.AsSpan()); + AudioFrame wet = _surroundProcessor.Process(frame); + + // Additional wet signal level boost to make the emulated + // sound more closely resemble real hardware recordings. + const float wetBoost = 1.8f; + frame.Left = wet.Left * wetBoost; + frame.Right = wet.Right * wetBoost; + frame = _surroundProcessor.Process(frame); + + output[index] = frame.Left; + output[index + 1] = frame.Right; + } + } + + [StructLayout(LayoutKind.Explicit)] + private struct StereoProcessorSwitchFunctions { + public StereoProcessorSwitchFunctions(byte value) { + data = value; + sourceSelector = value; + stereoMode = value; + } + + public StereoProcessorSwitchFunctions() { } + + [FieldOffset(0)] + public byte data; + [FieldOffset(0)] + public byte sourceSelector; + [FieldOffset(0)] + public byte stereoMode; + } + + private enum StereoProcessorStereoMode { + ForcedMono, + LinearStereo, + PseudoStereo, + SpatialStereo + } + + private enum StereoProcessorSourceSelector { + SoundA1 = 2, + SoundA2 = 3, + SoundB1 = 4, + SoundB2 = 5, + Stereo1 = 6, + Stereo2 = 7, + } + + /// + /// Philips Semiconductors TDA8425 hi-fi stereo audio processor emulation + /// + private class StereoProcessor { + private readonly ushort _sampleRate = 0; + //Left and Right channel gain values. + private float[] _gain = new float[2]; + private StereoProcessorSourceSelector _sourceSelector = new(); + private StereoProcessorStereoMode _stereoMode = new(); + + // Stero low and high-shelf filters + private readonly LowShelf[] _lowShelf = new LowShelf[] { new(), new() }; + private readonly HighShelf[] _highShelf = new HighShelf[] { new(), new() }; + private readonly AllPass _allPass = new(); + + private const int Volume0DbValue = 60; + + private const int ShelfFilter0DbValue = 6; + + private ILoggerService _loggerService; + + public StereoProcessor(ushort sampleRate, ILoggerService loggerService) { + _loggerService = loggerService; + _sampleRate = sampleRate; + if (_sampleRate <= 0) { + throw new IndexOutOfRangeException(nameof(_sampleRate)); + } + + const double allPassFrequency = 400.0; + const double qFactor = 1.7; + _allPass.Setup(_sampleRate, allPassFrequency, qFactor); + Reset(); + } + + public void Reset() { + ControlWrite(StereoProcessorControlReg.VolumeLeft, Volume0DbValue); + ControlWrite(StereoProcessorControlReg.VolumeRight, Volume0DbValue); + ControlWrite(StereoProcessorControlReg.Bass, ShelfFilter0DbValue); + ControlWrite(StereoProcessorControlReg.Treble, ShelfFilter0DbValue); + StereoProcessorSwitchFunctions sf = new() { + sourceSelector = (byte)StereoProcessorSourceSelector.Stereo1, + stereoMode = (byte)StereoProcessorStereoMode.LinearStereo + }; + ControlWrite(StereoProcessorControlReg.SwitchFunctions, sf.data); + } + + public void ControlWrite( + StereoProcessorControlReg reg, + byte data) { + float CalcVolumeGain(int value) { + const float minGainDb = -128.0f; + const float maxGainDb = 6.0f; + const float stepDb = 2.0f; + + float val = value - Volume0DbValue; + float gainDb = Math.Clamp(val * stepDb, minGainDb, maxGainDb); + return MathUtils.DecibelToGain(gainDb); + } + + float CalcFilterGainDb(int value) { + const double mainGainDb = -12.0; + const double maxGainDb = 15.0; + const double stepDb = 3.0; + + int val = value - ShelfFilter0DbValue; + return (float)Math.Clamp(val * stepDb, mainGainDb, maxGainDb); + } + + const int volumeControlWidth = 6; + const int volumeControlMask = (1 << volumeControlWidth) - 1; + + const int filterControlWidth = 4; + const int filterControlMask = (1 << filterControlWidth) - 1; + + switch (reg) { + case StereoProcessorControlReg.VolumeLeft: { + var value = data & volumeControlMask; + _gain[0] = CalcVolumeGain(value); + _loggerService.Debug("ADLIBGOLD: Stereo: Final left volume set to {Left}.2fdB {Value}", + _gain[0], + value); + } + break; + + case StereoProcessorControlReg.VolumeRight: { + var value = data & volumeControlMask; + _gain[1] = CalcVolumeGain(value); + _loggerService.Debug("ADLIBGOLD: Stereo: Final right volume set to {Right}.2fdB {Value}", + _gain[1], + value); + } + break; + + case StereoProcessorControlReg.Bass: { + var value = data & filterControlMask; + var gainDb = CalcFilterGainDb(value); + SetLowShelfGain(gainDb); + + _loggerService.Debug("ADLIBGOLD: Stereo: Bass gain set to {GainDb}.2fdB {Value}", + gainDb, + value); + } + break; + + case StereoProcessorControlReg.Treble: { + var value = data & filterControlMask; + // Additional treble boost to make the emulated sound more + // closely resemble real hardware recordings. + const int extraTreble = 1; + var gainDb = CalcFilterGainDb(value + extraTreble); + SetHighShelfGain(gainDb); + + _loggerService.Debug("ADLIBGOLD: Stereo: Treble gain set to {GainDb}.2fdB {Value}", + gainDb, + value); + } + break; + + case StereoProcessorControlReg.SwitchFunctions: { + var sf = new StereoProcessorSwitchFunctions(data); + _sourceSelector = (StereoProcessorSourceSelector)sf.sourceSelector; + _stereoMode = (StereoProcessorStereoMode)sf.stereoMode; + _loggerService.Debug("ADLIBGOLD: Stereo: Source selector set to {SourceSelector}, stereo mode set to {StereoMode}", + (int)(_sourceSelector), + (int)(_stereoMode)); + } + break; + } + } + + public void SetHighShelfGain(double gainDb) { + const double cutOffFrequency = 2500.0; + const double slope = 0.5; + foreach (HighShelf f in _highShelf) { + f.Setup(_sampleRate, cutOffFrequency, gainDb, slope); + } + } + + public void SetLowShelfGain(double gainDb) { + const double cutoffFreq = 400.0; + const double slope = 0.5; + foreach (LowShelf f in _lowShelf) { + f.Setup(_sampleRate, cutoffFreq, gainDb, slope); + } + } + + public AudioFrame ProcessSourceSelection(AudioFrame frame) { + return _sourceSelector switch { + StereoProcessorSourceSelector.SoundA1 or StereoProcessorSourceSelector.SoundA2 => new(frame.AsSpan()[0..0]), + StereoProcessorSourceSelector.SoundB1 or StereoProcessorSourceSelector.SoundB2 => new(frame.AsSpan()[1..1]), + _ => frame,// Dune sends an invalid source selector value of 0 during the + // intro; we'll just revert to stereo operation + }; + } + + public AudioFrame ProcessShelvingFilters(AudioFrame frame) { + AudioFrame outFrame = new(); + + for (int i = 0; i < 2; ++i) { + outFrame[i] = (float)_lowShelf[i].Filter(frame[i]); + outFrame[i] = (float)_highShelf[i].Filter(outFrame[i]); + } + return outFrame; + } + + public AudioFrame ProcessStereoProcessing(AudioFrame frame) { + AudioFrame outFrame = new(); + + switch (_stereoMode) { + case StereoProcessorStereoMode.ForcedMono: { + float m = frame.Left + frame.Right; + outFrame.Left = m; + outFrame.Right = m; + } + break; + + case StereoProcessorStereoMode.PseudoStereo: + outFrame.Left = (float)_allPass.Filter(frame.Left); + outFrame.Right = frame.Right; + break; + + case StereoProcessorStereoMode.SpatialStereo: { + const float crosstalkPercentage = 52.0f; + const float k = crosstalkPercentage / 100.0f; + float l = frame.Left; + float r = frame.Right; + outFrame.Left = l + (l - r) * k; + outFrame.Right = r + (r - l) * k; + } + break; + + case StereoProcessorStereoMode.LinearStereo: + default: outFrame = frame; break; + } + return outFrame; + } + } + + [StructLayout(LayoutKind.Explicit)] + private struct SurroundControlReg { + [FieldOffset(0)] + public byte data; + [FieldOffset(0)] + public byte din; + [FieldOffset(0)] + public byte sci; + [FieldOffset(0)] + public byte a0; + } + + /// + /// Yamaha YM7128B Surround Processor emulation + /// + private class SurroundProcessor { + private ChipIdeal _chip = new(); + + private ControlState _ctrlState = new(); + + private struct ControlState { + public byte Sci { get; set; } + public byte A0 { get; set; } + public byte Addr { get; set; } + public byte Data { get; set; } + } + + public SurroundProcessor(ushort sampleRate) { + if (sampleRate < 10) { + throw new ArgumentOutOfRangeException(nameof(sampleRate)); + } + + Ym7128B.ChipIdealSetup(ref _chip, sampleRate); + Ym7128B.ChipIdealReset(ref _chip); + Ym7128B.ChipIdealStart(ref _chip); + } + + public AudioFrame Process(AudioFrame frame) { + ChipIdealProcessData data = new(); + data.Inputs[0] = frame.Left + frame.Right; + Ym7128B.ChipIdealProcess(ref _chip, ref data); + return new(data.Outputs); + } + + public void ControlWrite(byte val) { + SurroundControlReg reg = new() { + data = val, + a0 = val, + din = val, + sci = val + }; + + // Change register data at the falling edge of 'a0' word clock + if (_ctrlState.A0 == 1 && reg.a0 == 0) { + // _logger.Debug("ADLIBGOLD: Surround: Write + // control register %d, data: %d", + + Ym7128B.ChipIdealWrite(ref _chip, _ctrlState.Addr, _ctrlState.Data); + Ym7128B.ChipIdealWrite(ref _chip, _ctrlState.Addr, _ctrlState.Data); + } else { + // Data is sent in serially through 'din' in MSB->LSB order, + // synchronised by the 'sci' bit clock. Data should be read on + // the rising edge of 'sci'. + if (_ctrlState.Sci == 0 && reg.sci == 1) { + // The 'a0' word clock determines the type of the data. + if (reg.a0 == 1) { + // Data cycle + _ctrlState.Data = (byte)((_ctrlState.Data << 1) | reg.din); + } else { + // Address cycle + _ctrlState.Addr = (byte)((_ctrlState.Addr << 1) | reg.din); + } + } + } + + _ctrlState.Sci = reg.sci; + _ctrlState.A0 = reg.a0; + } + } +} \ No newline at end of file diff --git a/src/Spice86.Core/Emulator/Devices/Sound/AudioFrame.cs b/src/Spice86.Core/Emulator/Devices/Sound/AudioFrame.cs new file mode 100644 index 0000000000..0802829105 --- /dev/null +++ b/src/Spice86.Core/Emulator/Devices/Sound/AudioFrame.cs @@ -0,0 +1,52 @@ +namespace Spice86.Core.Emulator.Devices.Sound; + +/// +/// A frame of audio data containing two channels. +/// +public ref struct AudioFrame +{ + private Span _data; + + /// + /// Initializes a new instance of the struct. + /// + /// The source of audio values + public AudioFrame(Span data) + { + _data = data; + } + + /// + /// Gives the underlying Span + /// + /// The underlying Span of floats + public Span AsSpan() => _data; + + /// + /// The left channel value. + /// + public float Left + { + get => _data[0]; + set => _data[0] = value; + } + + /// + /// The right channel value. + /// + public float Right { + get => _data.Length > 0 ? _data[1] : _data[0]; + set { + int index = _data.Length > 0 ? 1 : 0; + _data[index] = value; + } + } + + /// + /// Provides access to the left and right channel values using an index. + /// + public float this[int i] { + get { return int.IsEvenInteger(i) ? Left : Right; } + set { if (int.IsEvenInteger(i)) { Left = value; } else { Right = value; } } + } +} diff --git a/src/Spice86.Core/Emulator/Devices/Sound/MathUtils.cs b/src/Spice86.Core/Emulator/Devices/Sound/MathUtils.cs new file mode 100644 index 0000000000..8477190f12 --- /dev/null +++ b/src/Spice86.Core/Emulator/Devices/Sound/MathUtils.cs @@ -0,0 +1,6 @@ +namespace Spice86.Core.Emulator.Devices.Sound; + +public static class MathUtils { + public static float DecibelToGain(float decibel) => + (float)Math.Pow(10.0f, decibel / 20.0f); +} \ No newline at end of file diff --git a/src/Spice86.Core/Emulator/Devices/Sound/OPL.cs b/src/Spice86.Core/Emulator/Devices/Sound/OPL.cs new file mode 100644 index 0000000000..2cb48f32bf --- /dev/null +++ b/src/Spice86.Core/Emulator/Devices/Sound/OPL.cs @@ -0,0 +1,292 @@ +namespace Spice86.Core.Emulator.Devices.Sound; + +using System.Runtime.InteropServices; + +using Spice86.Core.Emulator.Devices.Sound.Ym7128b; +using Spice86.Core.Emulator.VM; + +public enum Mode { + Opl2, DualOpl2, Opl3, Opl3Gold +} + +public struct Control { + public Control() { } + public byte Index { get; set; } + public byte LVol { get; set; } = Opl.DefaultVolumeValue; + public byte RVol { get; set; } + + public bool IsActive { get; set; } + public bool UseMixer { get; set; } +} + +public class OplTimer { + /// + /// Rounded down start time + /// + private double _start = 0.0; + + /// + /// Time when you overflow + /// + private double _trigger = 0.0; + + /// + /// Clock Interval in Milliseconds + /// + private readonly double _clockInterval = 0.0; + + /// + /// Cycle interval + /// + private double _counterInterval = 0.0; + + private byte _counter = 0; + + private bool _enabled = false; + private bool _overflow = false; + private bool _masked = false; + public OplTimer(short micros) { + _clockInterval = micros * 0.001; + SetCounter(0); + } + + public void SetCounter(byte val) { + _counter = val; + // Interval for next cycle + _counterInterval = (256 - _counter) * _clockInterval; + } + + public void Reset() { + // On a reset make sure the start is in sync with the next cycle + _overflow = false; + } + + public void SetMask(bool set) { + _masked = set; + if (_masked) { + _overflow = false; + } + } + + public void Stop() { + _enabled = false; + } + + public void Start(double time) { + // Only properly start when not running before + if (!_enabled) { + _enabled = true; + _overflow = false; + // Sync start to the last clock interval + double clockMod = Math.IEEERemainder(time, _clockInterval); + + _start = time - clockMod; + // Overflow trigger + _trigger = _start + _counterInterval; + } + } + + public bool Update(double time) { + if (_enabled && (time >= _trigger)) { + // How far into the next cycle + double deltaTime = time - _trigger; + // Sync start to last cycle + double counterMod = Math.IEEERemainder(deltaTime, _counterInterval); + + _start = time - counterMod; + _trigger = _start + _counterInterval; + // Only set the overflow flag when not masked + if (!_masked) { + _overflow = true; + } + } + return _overflow; + } +} + +public class Chip { + private Machine _machine; + public Chip(Machine machine) { + _machine = machine; + Timer0 = new(80); + Timer1 = new(320); + } + + /// + /// Last selected register + /// + public OplTimer Timer0 { get; private set; } + public OplTimer Timer1 { get; private set; } + + /// + /// Check for it being a write to the timer + /// + public bool Write(ushort reg, byte val) { + // if(reg == 0x02 || reg == 0x03 || reg == 0x04) + // LOG(LOG_MISC,LOG_ERROR)("write adlib timer %X %X",reg,val); + switch (reg) { + case 0x02: + Timer0.Update(TimeSpan.FromTicks(_machine.Timer.NumberOfTicks).TotalMilliseconds); + Timer0.SetCounter(val); + return true; + case 0x03: + Timer1.Update(TimeSpan.FromTicks(_machine.Timer.NumberOfTicks).TotalMilliseconds); + Timer1.SetCounter(val); + return true; + case 0x04: + // Reset overflow in both timers + if ((val & 0x80) > 0) { + Timer0.Reset(); + Timer1.Reset(); + } else { + double time = TimeSpan.FromTicks(_machine.Timer.NumberOfTicks).TotalMilliseconds; + if ((val & 0x1) > 0) { + Timer0.Start(time); + } else { + Timer0.Stop(); + } + + if ((val & 0x2) > 0) { + Timer1.Start(time); + } else { + Timer1.Stop(); + } + Timer0.SetMask((val & 0x40) > 0); + Timer1.SetMask((val & 0x20) > 0); + } + return true; + } + return false; + } + + /// + /// Read the current timer state, will use current double + /// + public byte Read() { + TimeSpan time = TimeSpan.FromTicks(_machine.Timer.NumberOfTicks); + byte ret = 0; + + // Overflow won't be set if a channel is masked + if (Timer0.Update(time.TotalMilliseconds)) { + ret |= 0x40; + ret |= 0x80; + } + if (Timer1.Update(time.TotalMilliseconds)) { + ret |= 0x20; + ret |= 0x80; + } + return ret; + } +} + +public class Opl { + public const byte DefaultVolumeValue = 0xff; + + //public MixerChannel Channel { get; private set; } = new(); + + /// + /// The cache for 2 chips or an OPL3 + /// + public byte[] Cache { get; private set; } = new byte[512]; + + private Queue> _fifo = new(); + + private Mode _mode; + + private Chip[] _chip = new Chip[2]; + + private Opl3Chip _oplChip = new(); + + private byte _mem; + + private AdlibGold _adlibGold; + + // Playback related + private double _lastRenderedMs = 0.0; + private double _msPerFrame = 0.0; + + // Last selected address in the chip for the different modes + + private const int DefaultVolume = 0xff; + + + [StructLayout(LayoutKind.Explicit)] + private struct Reg { + [FieldOffset(0)] + public byte normal; + [FieldOffset(0)] + public byte[] dual; + + public Reg() { + dual = new byte[2]; + } + } + + private Reg _reg = new(); + + private Control _ctrl = new(); + + public Opl(AdlibGold adlibGold, OplMode mode) { + _adlibGold = adlibGold; + } + + private void AdlibGoldControlWrite(byte val) { + switch (_ctrl.Index) { + case 0x04: + _adlibGold.StereoControlWrite((byte)StereoProcessorControlReg.VolumeLeft, + val); + break; + case 0x05: + _adlibGold.StereoControlWrite((byte)StereoProcessorControlReg.VolumeRight, + val); + break; + case 0x06: + _adlibGold.StereoControlWrite((byte)StereoProcessorControlReg.Bass, val); + break; + case 0x07: + _adlibGold.StereoControlWrite((byte)StereoProcessorControlReg.Treble, val); + break; + + case 0x08: + _adlibGold.StereoControlWrite((byte)StereoProcessorControlReg.SwitchFunctions, + val); + break; + + case 0x09: // Left FM Volume + _ctrl.LVol = val; + goto setvol; + case 0x0a: // Right FM Volume + _ctrl.RVol = val; + setvol: + if (_ctrl.UseMixer) { + // Dune CD version uses 32 volume steps in an apparent + // mistake, should be 128 + _ctrl.LVol &= (byte) (0x1f / 31.0f); + _ctrl.RVol &= (byte) (0x1f / 31.0f); + } + break; + + case 0x18: // Surround + _adlibGold.SurroundControlWrite(val); + break; + } + } + + private byte AdlibGoldControlRead() { + switch (_ctrl.Index) { + case 0x00: // Board Options + return 0x50; // 16-bit ISA, surround module, no + // telephone/CDROM + // return 0x70; // 16-bit ISA, no + // telephone/surround/CD-ROM + + case 0x09: // Left FM Volume + return _ctrl.LVol; + case 0x0a: // Right FM Volume + return _ctrl.RVol; + case 0x15: // Audio Relocation + return 0x388 >> 3; // Cryo installer detection + } + return 0xff; + } +} \ No newline at end of file diff --git a/src/Spice86.Core/Emulator/Devices/Sound/OPL3Nuked.cs b/src/Spice86.Core/Emulator/Devices/Sound/OPL3Nuked.cs new file mode 100644 index 0000000000..c54afc0af3 --- /dev/null +++ b/src/Spice86.Core/Emulator/Devices/Sound/OPL3Nuked.cs @@ -0,0 +1,1372 @@ +/* Nuked OPL3 + * Copyright (C) 2013-2020 Nuke.YKT + * + * This file is part of Nuked OPL3. + * + * Nuked OPL3 is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 2.1 + * of the License, or (at your option) any later version. + * + * Nuked OPL3 is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Nuked OPL3. If not, see . + + * Nuked OPL3 emulator. + * Thanks: + * MAME Development Team(Jarek Burczynski, Tatsuyuki Satoh): + * Feedback and Rhythm part calculation information. + * forums.submarine.org.uk(carbon14, opl3): + * Tremolo and phase generator calculation information. + * OPLx decapsulated(Matthew Gambrell, Olli Niemitalo): + * OPL2 ROMs. + * siliconpr0n.org(John McMaster, digshadow): + * YMF262 and VRC VII decaps and die shots. + * + * version: 1.8 + */ + +/* Quirk: Some FM channels are output one sample later on the left side than the right. */ +#define OPL_QUIRK_CHANNELSAMPLEDELAY +// Enables Stereo Extensions (see Opl3Channel struct for example) +//#undef OPL_QUIRK_CHANNELSAMPLEDELAY +//#define OPL_ENABLE_STEREOEXT + +using System.Collections.ObjectModel; +namespace Spice86.Core.Emulator.Devices.Sound; + +using Spice86.Shared.Emulator.Errors; + +using System.Collections.Frozen; + +public static class Opl3Nuked { + public const int OplWriteBufSize = 1024; + public const int OplWriteBufDelay = 2; + +#if OPL_ENABLE_STEREOEXT && OPL_SIN +#define _USE_MATH_DEFINES + private static double OplSin(double x) { + return ((int)(Math.Sin(x) * Math.Pi / 512.0)) * 65536.0; + } +#endif + public const int RsmFrac = 10; + + /// + /// Channel types + /// + private enum ChType { + Ch2Op, + Ch4Op, + Ch4Op2, + ChDrum + } + + /// + /// Envelope key types + /// + private enum EnvelopeKeyType { + EgkNorm = 0x01, + EgkDrum = 0x02 + } + + /// + /// logsin table + /// + private static readonly FrozenSet LogSinRom = new ushort[] { + 0x859, 0x6c3, 0x607, 0x58b, 0x52e, 0x4e4, 0x4a6, 0x471, + 0x443, 0x41a, 0x3f5, 0x3d3, 0x3b5, 0x398, 0x37e, 0x365, + 0x34e, 0x339, 0x324, 0x311, 0x2ff, 0x2ed, 0x2dc, 0x2cd, + 0x2bd, 0x2af, 0x2a0, 0x293, 0x286, 0x279, 0x26d, 0x261, + 0x256, 0x24b, 0x240, 0x236, 0x22c, 0x222, 0x218, 0x20f, + 0x206, 0x1fd, 0x1f5, 0x1ec, 0x1e4, 0x1dc, 0x1d4, 0x1cd, + 0x1c5, 0x1be, 0x1b7, 0x1b0, 0x1a9, 0x1a2, 0x19b, 0x195, + 0x18f, 0x188, 0x182, 0x17c, 0x177, 0x171, 0x16b, 0x166, + 0x160, 0x15b, 0x155, 0x150, 0x14b, 0x146, 0x141, 0x13c, + 0x137, 0x133, 0x12e, 0x129, 0x125, 0x121, 0x11c, 0x118, + 0x114, 0x10f, 0x10b, 0x107, 0x103, 0x0ff, 0x0fb, 0x0f8, + 0x0f4, 0x0f0, 0x0ec, 0x0e9, 0x0e5, 0x0e2, 0x0de, 0x0db, + 0x0d7, 0x0d4, 0x0d1, 0x0cd, 0x0ca, 0x0c7, 0x0c4, 0x0c1, + 0x0be, 0x0bb, 0x0b8, 0x0b5, 0x0b2, 0x0af, 0x0ac, 0x0a9, + 0x0a7, 0x0a4, 0x0a1, 0x09f, 0x09c, 0x099, 0x097, 0x094, + 0x092, 0x08f, 0x08d, 0x08a, 0x088, 0x086, 0x083, 0x081, + 0x07f, 0x07d, 0x07a, 0x078, 0x076, 0x074, 0x072, 0x070, + 0x06e, 0x06c, 0x06a, 0x068, 0x066, 0x064, 0x062, 0x060, + 0x05e, 0x05c, 0x05b, 0x059, 0x057, 0x055, 0x053, 0x052, + 0x050, 0x04e, 0x04d, 0x04b, 0x04a, 0x048, 0x046, 0x045, + 0x043, 0x042, 0x040, 0x03f, 0x03e, 0x03c, 0x03b, 0x039, + 0x038, 0x037, 0x035, 0x034, 0x033, 0x031, 0x030, 0x02f, + 0x02e, 0x02d, 0x02b, 0x02a, 0x029, 0x028, 0x027, 0x026, + 0x025, 0x024, 0x023, 0x022, 0x021, 0x020, 0x01f, 0x01e, + 0x01d, 0x01c, 0x01b, 0x01a, 0x019, 0x018, 0x017, 0x017, + 0x016, 0x015, 0x014, 0x014, 0x013, 0x012, 0x011, 0x011, + 0x010, 0x00f, 0x00f, 0x00e, 0x00d, 0x00d, 0x00c, 0x00c, + 0x00b, 0x00a, 0x00a, 0x009, 0x009, 0x008, 0x008, 0x007, + 0x007, 0x007, 0x006, 0x006, 0x005, 0x005, 0x005, 0x004, + 0x004, 0x004, 0x003, 0x003, 0x003, 0x002, 0x002, 0x002, + 0x002, 0x001, 0x001, 0x001, 0x001, 0x001, 0x001, 0x001, + 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000 + }.ToFrozenSet(); + + /// + /// Exp table + /// + private static readonly FrozenSet ExpRom = new uint[]{ + 0x7fa, 0x7f5, 0x7ef, 0x7ea, 0x7e4, 0x7df, 0x7da, 0x7d4, + 0x7cf, 0x7c9, 0x7c4, 0x7bf, 0x7b9, 0x7b4, 0x7ae, 0x7a9, + 0x7a4, 0x79f, 0x799, 0x794, 0x78f, 0x78a, 0x784, 0x77f, + 0x77a, 0x775, 0x770, 0x76a, 0x765, 0x760, 0x75b, 0x756, + 0x751, 0x74c, 0x747, 0x742, 0x73d, 0x738, 0x733, 0x72e, + 0x729, 0x724, 0x71f, 0x71a, 0x715, 0x710, 0x70b, 0x706, + 0x702, 0x6fd, 0x6f8, 0x6f3, 0x6ee, 0x6e9, 0x6e5, 0x6e0, + 0x6db, 0x6d6, 0x6d2, 0x6cd, 0x6c8, 0x6c4, 0x6bf, 0x6ba, + 0x6b5, 0x6b1, 0x6ac, 0x6a8, 0x6a3, 0x69e, 0x69a, 0x695, + 0x691, 0x68c, 0x688, 0x683, 0x67f, 0x67a, 0x676, 0x671, + 0x66d, 0x668, 0x664, 0x65f, 0x65b, 0x657, 0x652, 0x64e, + 0x649, 0x645, 0x641, 0x63c, 0x638, 0x634, 0x630, 0x62b, + 0x627, 0x623, 0x61e, 0x61a, 0x616, 0x612, 0x60e, 0x609, + 0x605, 0x601, 0x5fd, 0x5f9, 0x5f5, 0x5f0, 0x5ec, 0x5e8, + 0x5e4, 0x5e0, 0x5dc, 0x5d8, 0x5d4, 0x5d0, 0x5cc, 0x5c8, + 0x5c4, 0x5c0, 0x5bc, 0x5b8, 0x5b4, 0x5b0, 0x5ac, 0x5a8, + 0x5a4, 0x5a0, 0x59c, 0x599, 0x595, 0x591, 0x58d, 0x589, + 0x585, 0x581, 0x57e, 0x57a, 0x576, 0x572, 0x56f, 0x56b, + 0x567, 0x563, 0x560, 0x55c, 0x558, 0x554, 0x551, 0x54d, + 0x549, 0x546, 0x542, 0x53e, 0x53b, 0x537, 0x534, 0x530, + 0x52c, 0x529, 0x525, 0x522, 0x51e, 0x51b, 0x517, 0x514, + 0x510, 0x50c, 0x509, 0x506, 0x502, 0x4ff, 0x4fb, 0x4f8, + 0x4f4, 0x4f1, 0x4ed, 0x4ea, 0x4e7, 0x4e3, 0x4e0, 0x4dc, + 0x4d9, 0x4d6, 0x4d2, 0x4cf, 0x4cc, 0x4c8, 0x4c5, 0x4c2, + 0x4be, 0x4bb, 0x4b8, 0x4b5, 0x4b1, 0x4ae, 0x4ab, 0x4a8, + 0x4a4, 0x4a1, 0x49e, 0x49b, 0x498, 0x494, 0x491, 0x48e, + 0x48b, 0x488, 0x485, 0x482, 0x47e, 0x47b, 0x478, 0x475, + 0x472, 0x46f, 0x46c, 0x469, 0x466, 0x463, 0x460, 0x45d, + 0x45a, 0x457, 0x454, 0x451, 0x44e, 0x44b, 0x448, 0x445, + 0x442, 0x43f, 0x43c, 0x439, 0x436, 0x433, 0x430, 0x42d, + 0x42a, 0x428, 0x425, 0x422, 0x41f, 0x41c, 0x419, 0x416, + 0x414, 0x411, 0x40e, 0x40b, 0x408, 0x406, 0x403, 0x400 + }.ToFrozenSet(); + + /// + /// freq mult table multiplied by 2 + /// 1/2, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 10, 12, 12, 15, 15 + /// + private static readonly FrozenSet Mt = new byte[] { + 1, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 20, 24, 24, 30, 30 + }.ToFrozenSet(); + + /// + /// KSL Table + /// + private static readonly FrozenSet KslRom = new byte[] { + 0, 32, 40, 45, 48, 51, 53, 55, 56, 58, 59, 60, 61, 62, 63, 64 + }.ToFrozenSet(); + + private static readonly FrozenSet KslShift =new byte[] { + 8, 1, 2, 0 + }.ToFrozenSet(); + + /// + /// envelope generator constants + /// + private static readonly ReadOnlyCollection EgIncStep = Array.AsReadOnly(new[] { + new byte[]{ 0, 0, 0, 0 }, + new byte[]{ 1, 0, 0, 0 }, + new byte[]{ 1, 0, 1, 0 }, + new byte[]{ 1, 1, 1, 0 } + }); + + /// + /// address decoding + /// + private static readonly ReadOnlyCollection AdSlot = Array.AsReadOnly(new sbyte[] { + 0, 1, 2, 3, 4, 5, -1, -1, 6, 7, 8, 9, 10, 11, -1, -1, + 12, 13, 14, 15, 16, 17, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 + }); + + private static readonly FrozenSet ChSlot = new byte[] { + 0, 1, 2, 6, 7, 8, 12, 13, 14, 18, 19, 20, 24, 25, 26, 30, 31, 32 + }.ToFrozenSet(); + +#if OPL_ENABLE_STEREOEXT + /// + /// stereo extension panning table + /// + private static int[] PanpotLut = new int[256](); + private const byte PanpotLutBuild = 0; +#endif + + /* + * Envelope generator + */ + private static short Opl3EnvelopeCalcExp(int level) { + if (level > 0x1fff) { + level = 0x1fff; + } + return (short)((ExpRom.Items[level & 0xff] << 1) >> (level >> 8)); + } + + private static short Opl3EnvelopeCalcSin0(ushort phase, ushort envelope) { + ushort neg = 0; + phase &= 0x3ff; + if ((phase & 0x200) >= 1) { + neg = 0xffff; + } + ushort output; + if ((phase & 0x100) >= 1) { + output = LogSinRom.Items[(phase & 0xff) ^ 0xff]; + } else { + output = LogSinRom.Items[phase & 0xff]; + } + return (short)(Opl3EnvelopeCalcExp(output + (envelope << 3)) ^ neg); + } + + private static short Opl3EnvelopeCalcSin1(ushort phase, ushort envelope) { + phase &= 0x3f; + ushort output; + if ((phase & 0x200) >= 1) { + output = 0x1000; + } else if ((phase & 0x100) >= 1) { + output = LogSinRom.Items[(phase & 0xff) ^ 0xff]; + } else { + output = LogSinRom.Items[phase & 0xff]; + } + return Opl3EnvelopeCalcExp(output + (envelope << 3)); + } + + private static short Opl3EnvelopeCalcSin2(ushort phase, ushort envelope) { + phase &= 0x3ff; + ushort output; + if ((phase & 0x100) >= 1) { + output = LogSinRom.Items[(phase & 0xff) ^ 0xff]; + } else { + output = LogSinRom.Items[phase & 0xff]; + } + return Opl3EnvelopeCalcExp(output + (envelope << 3)); + } + + private static short Opl3EnvelopeCalcSin3(ushort phase, ushort envelope) { + phase &= 0x3ff; + ushort output; + if ((phase & 0x100) >= 1) { + output = 0x1000; + } else { + output = LogSinRom.Items[phase & 0xff]; + } + return Opl3EnvelopeCalcExp(output + (envelope << 3)); + } + + private static short Opl3EnvelopeCalcSin4(ushort phase, ushort envelope) { + ushort neg = 0; + phase &= 0x3ff; + if ((phase & 0x300) == 0x100) { + neg = 0xffff; + } + ushort output; + if ((phase & 0x200) >= 1) { + output = 0x1000; + } else if ((phase & 0x80) >= 1) { + output = LogSinRom.Items[((phase ^ 0xff) << 1) & 0xff]; + } else { + output = LogSinRom.Items[(phase << 1) & 0xff]; + } + return (short)(Opl3EnvelopeCalcExp(output + (envelope << 3)) ^ neg); + } + + private static short Opl3EnvelopeCalcSin5(ushort phase, ushort envelope) { + phase &= 0x3ff; + ushort output; + if ((phase & 0x200) >= 1) { + output = 0x1000; + } else if ((phase & 0x80) >= 1) { + output = LogSinRom.Items[((phase ^ 0xff) << 1) & 0xff]; + } else { + output = LogSinRom.Items[(phase << 1) & 0xff]; + } + return Opl3EnvelopeCalcExp(output + (envelope << 3)); + } + + private static short Opl3EnvelopeCalcSin6(ushort phase, ushort envelope) { + ushort neg = 0; + phase &= 0x3ff; + if ((phase & 0x200) >= 1) { + neg = 0xffff; + } + return (short)(Opl3EnvelopeCalcExp(envelope << 3) ^ neg); + } + + private static short Opl3EnvelopeCalcSin7(ushort phase, ushort envelope) { + ushort neg = 0; + phase &= 0x3ff; + if ((phase & 0x200) >= 1) { + neg = 0xffff; + phase = (ushort)((phase & 0x1ff) ^ 0x1ff); + } + ushort output = (ushort)(phase << 3); + return (short)(Opl3EnvelopeCalcExp(output + (envelope << 3)) ^ neg); + } + + private static readonly FrozenSet> EnvelopeSin = new Func[]{ + (x, y) => Opl3EnvelopeCalcSin0(x, y), + (x, y) => Opl3EnvelopeCalcSin1(x, y), + (x, y) => Opl3EnvelopeCalcSin2(x, y), + (x, y) => Opl3EnvelopeCalcSin3(x, y), + (x, y) => Opl3EnvelopeCalcSin4(x, y), + (x, y) => Opl3EnvelopeCalcSin5(x, y), + (x, y) => Opl3EnvelopeCalcSin6(x, y), + (x, y) => Opl3EnvelopeCalcSin7(x, y), + }.ToFrozenSet(); + + private enum EnvelopeGenNum { + Attack, + Decay, + Sustain, + Release, + } + + private static void Opl3EnvelopeUpdateKsl(ref Opl3Slot slot) { + short ksl = (short)((KslRom.Items[slot.Channel.FNum >> 6] << 2) + - ((0x08 - slot.Channel.Block) << 5)); + if (ksl < 0) { + ksl = 0; + } + slot.EgKsl = (byte)ksl; + } + + private static void Opl3EnvelopeCalc(ref Opl3Slot slot) { + byte nonzero; + byte rate; + byte rateHi; + byte rateLo; + byte regRate = 0; + byte ks; + byte egShift, shift; + ushort egRout; + short egInc; + byte egOff; + byte reset = 0; + slot.EgOut = (ushort)(slot.EgRout + (slot.RegTl << 2) + + (slot.EgKsl >> KslShift.Items[slot.RegKsl]) + slot.Trem); + if (slot.Key > 0 && slot.EgGen == (int)EnvelopeGenNum.Release) { + reset = 1; + regRate = slot.RegAr; + } else { + switch (slot.EgGen) { + case (byte)EnvelopeGenNum.Attack: + regRate = slot.RegAr; + break; + case (byte)EnvelopeGenNum.Decay: + regRate = slot.RegDr; + break; + case (byte)EnvelopeGenNum.Sustain: + if (slot.RegType <= 0) { + regRate = slot.RegRr; + } + break; + case (byte)EnvelopeGenNum.Release: + regRate = slot.RegRr; + break; + } + } + slot.PgReset = reset; + ks = (byte)(slot.Channel.Ksv >> ((slot.RegKsr ^ 1) << 1)); + nonzero = (byte)(regRate != 0 ? 1 : 0); + rate = (byte)(ks + (regRate << 2)); + rateHi = (byte)(rate >> 2); + rateLo = (byte)(rate & 0x03); + if ((rateHi & 0x10) > 0) { + rateHi = 0x0f; + } + egShift = (byte)(rateHi + slot.Chip.EgAdd); + shift = 0; + if (nonzero > 0) { + if (rateHi < 12) { + if (slot.Chip.EgState > 0) { + switch (egShift) { + case 12: + shift = 1; + break; + case 13: + shift = (byte)((rateLo >> 1) & 0x01); + break; + case 14: + shift = (byte)(rateLo & 0x01); + break; + } + } + } else { + shift = (byte)((rateHi & 0x03) + EgIncStep[rateLo][slot.Chip.Timer & 0x03]); + if ((shift & 0x04) > 0) { + shift = 0x03; + } + if (shift <= 0) { + shift = slot.Chip.EgState; + } + } + } + egRout = slot.EgRout; + egInc = 0; + egOff = 0; + /* Instant attack */ + if (reset > 0 && rateHi == 0x0f) { + egRout = 0x00; + } + /* Envelope off */ + if ((slot.EgRout & 0x1f8) == 0x1f8) { + egOff = 1; + } + if (slot.EgGen != (byte)EnvelopeGenNum.Attack && reset <= 0 && egOff > 0) { + egRout = 0x1ff; + } + switch (slot.EgGen) { + case (byte)EnvelopeGenNum.Attack: + if (slot.EgRout <= 0) { + slot.EgGen = (byte)EnvelopeGenNum.Decay; + } else if (slot.Key > 0 && shift > 0 && rateHi != 0x0f) { + egInc = (short)(~slot.EgRout >> (4 - shift)); + } + break; + case (byte)EnvelopeGenNum.Decay: + if ((slot.EgRout >> 4) == slot.RegSl) { + slot.EgGen = (byte)EnvelopeGenNum.Sustain; + } else if (egOff <= 0 && reset <= 0 && shift > 0) { + egInc = (short)(1 << (shift - 1)); + } + break; + case (byte)EnvelopeGenNum.Sustain: + case (byte)EnvelopeGenNum.Release: + if (egOff <= 0 && reset <= 0 && shift > 0) { + egInc = (short)(1 << (shift - 1)); + } + break; + } + slot.EgRout = (ushort)((egRout + egInc) & 0x1ff); + /* Key off */ + if (reset > 0) { + slot.EgGen = (byte)EnvelopeGenNum.Attack; + } + if (slot.Key <= 0) { + slot.EgGen = (byte)EnvelopeGenNum.Release; + } + } + + private static void OPL3EnvelopeKeyOn(ref Opl3Slot slot, byte type) { + slot.Key |= type; + } + + private static void Opl3EnvelopeKeyOff(ref Opl3Slot slot, byte type) { + slot.Key &= (byte)~type; + } + + /* + Phase Generator + */ + + private static void Opl3PhaseGenerate(ref Opl3Slot slot) { + Opl3Chip chip; + ushort fNum; + uint basefreq; + byte rmXor, nBit; + uint noise; + ushort phase; + + chip = slot.Chip; + fNum = slot.Channel.FNum; + if (slot.RegVib > 0) { + sbyte range; + byte vibpos; + + range = (sbyte)((fNum >> 7) & 7); + vibpos = slot.Chip.VibPos; + + if ((vibpos & 3) <= 0) { + range = 0; + } else if ((vibpos & 1) > 0) { + range >>= 1; + } + range >>= slot.Chip.VibShift; + + if ((vibpos & 4) > 0) { + range = (sbyte)-range; + } + fNum = (ushort)(fNum + range); + } + basefreq = (uint)((fNum << slot.Channel.Block) >> 1); + phase = (ushort)(slot.PgPhase >> 9); + if (slot.PgReset > 0) { + slot.PgPhase = 0; + } + slot.PgPhase = slot.PgPhase + (basefreq * Mt.Items[slot.RegMult]) >> 1; + /* Rhythm mode */ + noise = chip.Noise; + slot.PgPhaseOut = phase; + if (slot.SlotNum == 13) /* hh */ + { + chip.RmHhBits2 = (byte)((phase >> 2) & 1); + chip.RmHhBits3 = (byte)((phase >> 3) & 1); + chip.RmHhBits7 = (byte)((phase >> 7) & 1); + chip.RmHhBits8 = (byte)((phase >> 8) & 1); + } + if (slot.SlotNum == 17 && (chip.Rhy & 0x20) > 0) /* tc */ + { + chip.RmTcBits3 = (byte)((phase >> 3) & 1); + chip.RmTcBits5 = (byte)((phase >> 5) & 1); + } + if ((chip.Rhy & 0x20) > 0) { + rmXor = (byte)((chip.RmHhBits2 ^ chip.RmHhBits7) + | (chip.RmHhBits3 ^ chip.RmTcBits5) + | (chip.RmTcBits3 ^ chip.RmTcBits5)); + switch (slot.SlotNum) { + case 13: /* hh */ + slot.PgPhaseOut = (ushort)(rmXor << 9); + if ((rmXor ^ (noise & 1)) > 0) { + slot.PgPhaseOut |= 0xd0; + } else { + slot.PgPhaseOut |= 0x34; + } + break; + case 16: /* sd */ + slot.PgPhaseOut = (ushort)((chip.RmHhBits8 << 9) + | ((chip.RmHhBits8 ^ (ushort)(noise & 1)) << 8)); + break; + case 17: /* tc */ + slot.PgPhaseOut = (ushort)((rmXor << 9) | 0x80); + break; + } + } + nBit = (byte)(((noise >> 14) ^ noise) & 0x01); + chip.Noise = (uint)(((ushort)(noise >> 1)) | ((ushort)(nBit << 22))); + } + + /* + Slot + */ + + private static void Opl3SlotWrite20(ref Opl3Slot slot, byte data) { + if (((data >> 7) & 0x01) > 0) { + slot.Trem = slot.Chip.Tremolo; + } else { + slot.Trem = (byte)slot.Chip.ZeroMod; + } + slot.RegVib = (byte)((data >> 6) & 0x01); + slot.RegType = (byte)((data >> 5) & 0x01); + slot.RegKsr = (byte)((data >> 4) & 0x01); + slot.RegMult = (byte)(data & 0x0f); + } + + private static void Opl3SlotWrite40(ref Opl3Slot slot, byte data) { + slot.RegKsl = (byte)((data >> 6) & 0x03); + slot.RegTl = (byte)(data & 0x3f); + Opl3EnvelopeUpdateKsl(ref slot); + } + + private static void Opl3SlotWrite60(ref Opl3Slot slot, byte data) { + slot.RegSl = (byte)((data >> 4) & 0x0f); + if (slot.RegSl == 0x0f) { + slot.RegSl = 0x1f; + } + slot.RegRr = (byte)(data & 0x0f); + } + + private static void Opl3SlotWrite80(ref Opl3Slot slot, byte data) { + slot.RegSl = (byte)((data >> 4) & 0x0f); + if (slot.RegSl == 0x0f) { + slot.RegSl = 0x1f; + } + slot.RegRr = (byte)(data & 0x0f); + } + + private static void Opl3SlotWriteE0(ref Opl3Slot slot, byte data) { + slot.RegWf = (byte)(data & 0x07); + if (slot.Chip.NewM == 0x00) { + slot.RegWf &= 0x03; + } + } + + private static void Opl3SlotGenerate(ref Opl3Slot slot) { + slot.Out = EnvelopeSin.Items[slot.RegWf]((ushort)(slot.PgPhaseOut + slot.Mod), slot.EgOut); + } + + private static void Opl3SlotCalcFb(ref Opl3Slot slot) { + if (slot.Channel.Fb != 0x00) { + slot.FbMod = (short)((slot.PrOut + slot.Out) >> (0x09 - slot.Channel.Fb)); + } else { + slot.FbMod = 0; + } + slot.PrOut = slot.Out; + } + + /* + Channel + */ + + private static void Opl3ChannelSetupAlg(Opl3Channel channel) { + ArgumentNullException.ThrowIfNull(channel); + channel.Pair ??= new(); + ArgumentNullException.ThrowIfNull(channel.Pair); + if (channel.ChType == (byte)ChType.ChDrum) { + if (channel.ChNum is 7 or 8) { + channel.Slots[0].Mod = channel.Chip.ZeroMod; + channel.Slots[1].Mod = channel.Chip.ZeroMod; + return; + } + switch (channel.Alg & 0x01) { + case 0x00: + channel.Slots[0].Mod = channel.Slots[0].FbMod; + channel.Slots[1].Mod = channel.Slots[0].Out; + break; + case 0x01: + channel.Slots[0].Mod = channel.Slots[0].FbMod; + channel.Slots[1].Mod = channel.Chip.ZeroMod; + break; + } + return; + } + if ((channel.Alg & 0x08) > 0) { + return; + } + if ((channel.Alg & 0x04) > 0) { + channel.Pair.Out[0] = channel.Chip.ZeroMod; + channel.Pair.Out[1] = channel.Chip.ZeroMod; + channel.Pair.Out[2] = channel.Chip.ZeroMod; + channel.Pair.Out[3] = channel.Chip.ZeroMod; + switch (channel.Alg & 0x03) { + case 0x00: + channel.Pair.Slots[0].Mod = channel.Pair.Slots[0].FbMod; + channel.Pair.Slots[1].Mod = channel.Pair.Slots[0].Out; + channel.Slots[0].Mod = channel.Pair.Slots[1].Out; + channel.Slots[1].Mod = channel.Slots[0].Out; + channel.Out[0] = channel.Slots[1].Out; + channel.Out[1] = channel.Chip.ZeroMod; + channel.Out[2] = channel.Chip.ZeroMod; + channel.Out[3] = channel.Chip.ZeroMod; + break; + case 0x01: + channel.Pair.Slots[0].Mod = channel.Pair.Slots[0].FbMod; + channel.Pair.Slots[1].Mod = channel.Pair.Slots[0].Out; + channel.Slots[0].Mod = channel.Chip.ZeroMod; + channel.Slots[1].Mod = channel.Slots[0].Out; + channel.Out[0] = channel.Pair.Slots[1].Out; + channel.Out[1] = channel.Slots[1].Out; + channel.Out[2] = channel.Chip.ZeroMod; + channel.Out[3] = channel.Chip.ZeroMod; + break; + case 0x02: + channel.Pair.Slots[0].Mod = channel.Pair.Slots[0].FbMod; + channel.Pair.Slots[1].Mod = channel.Chip.ZeroMod; + channel.Slots[0].Mod = channel.Pair.Slots[1].Out; + channel.Slots[1].Mod = channel.Slots[0].Out; + channel.Out[0] = channel.Pair.Slots[0].Out; + channel.Out[1] = channel.Slots[1].Out; + channel.Out[2] = channel.Chip.ZeroMod; + channel.Out[3] = channel.Chip.ZeroMod; + break; + case 0x03: + channel.Pair.Slots[0].Mod = channel.Pair.Slots[0].FbMod; + channel.Pair.Slots[1].Mod = channel.Chip.ZeroMod; + channel.Slots[0].Mod = channel.Pair.Slots[1].Out; + channel.Slots[1].Mod = channel.Chip.ZeroMod; + channel.Out[0] = channel.Pair.Slots[0].Out; + channel.Out[1] = channel.Slots[0].Out; + channel.Out[2] = channel.Slots[1].Out; + channel.Out[3] = channel.Chip.ZeroMod; + break; + } + } else { + switch (channel.Alg & 0x01) { + case 0x00: + channel.Slots[0].Mod = channel.Slots[0].FbMod; + channel.Slots[1].Mod = channel.Slots[0].Out; + channel.Out[0] = channel.Slots[1].Out; + channel.Out[1] = channel.Chip.ZeroMod; + channel.Out[2] = channel.Chip.ZeroMod; + channel.Out[3] = channel.Chip.ZeroMod; + break; + case 0x01: + channel.Slots[0].Mod = channel.Slots[0].FbMod; + channel.Slots[1].Mod = channel.Chip.ZeroMod; + channel.Out[0] = channel.Slots[0].Out; + channel.Out[1] = channel.Slots[1].Out; + channel.Out[2] = channel.Chip.ZeroMod; + channel.Out[3] = channel.Chip.ZeroMod; + break; + } + } + } + + private static void Opl3ChannelUpdateRhytm(ref Opl3Chip chip, byte data) { + Opl3Channel channel6; + Opl3Channel channel7; + Opl3Channel channel8; + byte chNum; + + chip.Rhy = (byte)(data & 0x3f); + if ((chip.Rhy & 0x20) > 0) { + channel6 = chip.Channel[6]; + channel7 = chip.Channel[7]; + channel8 = chip.Channel[8]; + channel6.Out[0] = channel6.Slots[1].Out; + channel6.Out[1] = channel6.Slots[1].Out; + channel6.Out[2] = chip.ZeroMod; + channel6.Out[3] = chip.ZeroMod; + channel7.Out[0] = channel7.Slots[0].Out; + channel7.Out[1] = channel7.Slots[0].Out; + channel7.Out[2] = channel7.Slots[1].Out; + channel7.Out[3] = channel7.Slots[1].Out; + channel8.Out[0] = channel8.Slots[0].Out; + channel8.Out[1] = channel8.Slots[0].Out; + channel8.Out[2] = channel8.Slots[1].Out; + channel8.Out[3] = channel8.Slots[1].Out; + for (chNum = 6; chNum < 9; chNum++) { + chip.Channel[chNum].ChType = (byte)ChType.ChDrum; + } + Opl3ChannelSetupAlg(channel6); + Opl3ChannelSetupAlg(channel7); + Opl3ChannelSetupAlg(channel8); + /* hh */ + if ((chip.Rhy & 0x01) > 0) { + OPL3EnvelopeKeyOn(ref channel7.Slots[0], (byte)EnvelopeKeyType.EgkDrum); + } else { + Opl3EnvelopeKeyOff(ref channel7.Slots[0], (byte)EnvelopeKeyType.EgkDrum); + } + /* tc */ + if ((chip.Rhy & 0x02) > 0) { + OPL3EnvelopeKeyOn(ref channel8.Slots[1], (byte)EnvelopeKeyType.EgkDrum); + } else { + Opl3EnvelopeKeyOff(ref channel8.Slots[1], (byte)EnvelopeKeyType.EgkDrum); + } + /* tom */ + if ((chip.Rhy & 0x04) > 0) { + OPL3EnvelopeKeyOn(ref channel8.Slots[0], (byte)EnvelopeKeyType.EgkDrum); + } else { + Opl3EnvelopeKeyOff(ref channel8.Slots[0], (byte)EnvelopeKeyType.EgkDrum); + } + /* sd */ + if ((chip.Rhy & 0x08) > 0) { + OPL3EnvelopeKeyOn(ref channel7.Slots[1], (byte)EnvelopeKeyType.EgkDrum); + } else { + Opl3EnvelopeKeyOff(ref channel7.Slots[1], (byte)EnvelopeKeyType.EgkDrum); + } + /* bd */ + if ((chip.Rhy & 0x10) > 0) { + OPL3EnvelopeKeyOn(ref channel6.Slots[0], (byte)EnvelopeKeyType.EgkDrum); + OPL3EnvelopeKeyOn(ref channel6.Slots[1], (byte)EnvelopeKeyType.EgkDrum); + } else { + Opl3EnvelopeKeyOff(ref channel6.Slots[0], (byte)EnvelopeKeyType.EgkDrum); + Opl3EnvelopeKeyOff(ref channel6.Slots[1], (byte)EnvelopeKeyType.EgkDrum); + } + } else { + for (chNum = 6; chNum < 9; chNum++) { + chip.Channel[chNum].ChType = (byte)ChType.Ch2Op; + Opl3ChannelSetupAlg(chip.Channel[chNum]); + Opl3EnvelopeKeyOff(ref chip.Channel[chNum].Slots[0], (byte)EnvelopeKeyType.EgkDrum); + Opl3EnvelopeKeyOff(ref chip.Channel[chNum].Slots[1], (byte)EnvelopeKeyType.EgkDrum); + } + } + } + + private static void Opl3ChannelWriteA0(Opl3Channel channel, byte data) { + if (channel.Chip.NewM > 0 && channel.ChType == (byte)ChType.Ch4Op2) { + return; + } + channel.FNum = (ushort)((channel.FNum & 0x300) | data); + channel.Ksv = (byte)((channel.Block << 1) + | ((channel.FNum >> (0x09 - channel.Chip.Nts)) & 0x01)); + Opl3EnvelopeUpdateKsl(ref channel.Slots[0]); + Opl3EnvelopeUpdateKsl(ref channel.Slots[1]); + if (channel.Pair is not null && + channel.Chip.NewM > 0 && channel.ChType == (byte)ChType.Ch4Op) { + channel.Pair.FNum = channel.FNum; + channel.Pair.Ksv = channel.Ksv; + Opl3EnvelopeUpdateKsl(ref channel.Pair.Slots[0]); + Opl3EnvelopeUpdateKsl(ref channel.Pair.Slots[1]); + } + } + + private static void Opl3ChannelWriteB0(Opl3Channel channel, byte data) { + if (channel.Chip.NewM > 0 && channel.ChType == (byte)ChType.Ch4Op2) { + return; + } + channel.FNum = (ushort)((channel.FNum & 0xff) | ((data & 0x03) << 8)); + channel.Block = (byte)((data >> 2) & 0x07); + channel.Ksv = (byte)((channel.Block << 1) + | ((channel.FNum >> (0x09 - channel.Chip.Nts)) & 0x01)); + Opl3EnvelopeUpdateKsl(ref channel.Slots[0]); + Opl3EnvelopeUpdateKsl(ref channel.Slots[1]); + if (channel.Pair is not null && + channel.Chip.NewM > 0 && channel.ChType == (byte)ChType.Ch4Op) { + channel.Pair.FNum = channel.FNum; + channel.Pair.Block = channel.Block; + channel.Pair.Ksv = channel.Ksv; + Opl3EnvelopeUpdateKsl(ref channel.Pair.Slots[0]); + Opl3EnvelopeUpdateKsl(ref channel.Pair.Slots[1]); + } + } + + private static void Opl3ChannelWriteC0(Opl3Channel channel, byte data) { + channel.Fb = (byte)((data & 0x0e) >> 1); + channel.Con = (byte)(data & 0x01); + channel.Alg = channel.Con; + if (channel.Pair is not null && channel.Chip.NewM > 0) { + if (channel.ChType == (byte)ChType.Ch4Op) { + channel.Pair.Alg = (byte)(0x04 | (channel.Con << 1) | (channel.Pair.Con)); + channel.Alg = 0x08; + Opl3ChannelSetupAlg(channel.Pair); + } else if (channel.ChType == (byte)ChType.Ch4Op2) { + channel.Alg = (byte)(0x04 | (channel.Pair.Con << 1) | (channel.Con)); + channel.Pair.Alg = 0x08; + Opl3ChannelSetupAlg(channel); + } else { + Opl3ChannelSetupAlg(channel); + } + } else { + Opl3ChannelSetupAlg(channel); + } + if (channel.Chip.NewM > 0) { + channel.Cha = (ushort)(((data >> 4) & 0x01) > 0 ? ~0 : 0); + channel.Chb = (ushort)(((data >> 5) & 0x01) > 0 ? ~0 : 0); + } else { + channel.Cha = channel.Chb = unchecked((ushort)~0); + } +#if OPL_ENABLE_STEREOEXT + if (!channel.Chip.StereoExt > 0) + { + channel.LeftPan = channel.Cha << 16; + channel.RightPan = channel.Chb << 16; + } +#endif + } + +#if OPL_ENABLE_STEREOEXT + private static void OPL3ChannelWriteD0(Opl3Channel channel, byte data) + { + if (channel.Chip.StereoExt > 0) + { + channel.LeftPan = PanpotLut[data ^ 0xff]; + channel.RightPan = PanpotLut[data]; + } + } +#endif + + private static void OPL3ChannelKeyOn(Opl3Channel channel) { + if (channel.Chip.NewM > 0 && channel.Pair is not null) { + if (channel.ChType == (byte)ChType.Ch4Op) { + OPL3EnvelopeKeyOn(ref channel.Slots[0], (byte)EnvelopeKeyType.EgkNorm); + OPL3EnvelopeKeyOn(ref channel.Slots[1], (byte)EnvelopeKeyType.EgkNorm); + OPL3EnvelopeKeyOn(ref channel.Pair.Slots[0], (byte)EnvelopeKeyType.EgkNorm); + OPL3EnvelopeKeyOn(ref channel.Pair.Slots[1], (byte)EnvelopeKeyType.EgkNorm); + } else if (channel.ChType is ((byte)ChType.Ch2Op) or ((byte)ChType.ChDrum)) { + OPL3EnvelopeKeyOn(ref channel.Slots[0], (byte)EnvelopeKeyType.EgkNorm); + OPL3EnvelopeKeyOn(ref channel.Slots[1], (byte)EnvelopeKeyType.EgkNorm); + } + } else { + OPL3EnvelopeKeyOn(ref channel.Slots[0], (byte)EnvelopeKeyType.EgkNorm); + OPL3EnvelopeKeyOn(ref channel.Slots[1], (byte)EnvelopeKeyType.EgkNorm); + } + } + + private static void Opl3ChannelKeyOff(Opl3Channel channel) { + if (channel.Chip.NewM > 0 && channel.Pair is not null) { + if (channel.ChType == (byte)ChType.Ch4Op) { + Opl3EnvelopeKeyOff(ref channel.Slots[0], (byte)EnvelopeKeyType.EgkNorm); + Opl3EnvelopeKeyOff(ref channel.Slots[1], (byte)EnvelopeKeyType.EgkNorm); + Opl3EnvelopeKeyOff(ref channel.Pair.Slots[0], (byte)EnvelopeKeyType.EgkNorm); + Opl3EnvelopeKeyOff(ref channel.Pair.Slots[1], (byte)EnvelopeKeyType.EgkNorm); + } else if (channel.ChType is ((byte)ChType.Ch2Op) or ((byte)ChType.ChDrum)) { + Opl3EnvelopeKeyOff(ref channel.Slots[0], (byte)EnvelopeKeyType.EgkNorm); + Opl3EnvelopeKeyOff(ref channel.Slots[1], (byte)EnvelopeKeyType.EgkNorm); + } + } else { + Opl3EnvelopeKeyOff(ref channel.Slots[0], (byte)EnvelopeKeyType.EgkNorm); + Opl3EnvelopeKeyOff(ref channel.Slots[1], (byte)EnvelopeKeyType.EgkNorm); + } + } + + private static void Opl3ChannelSet4Op(ref Opl3Chip chip, byte data) { + byte bit; + byte chnum; + for (bit = 0; bit < 6; bit++) { + chnum = bit; + if (bit >= 3) { + chnum += 9 - 3; + } + if (((data >> bit) & 0x01) > 0) { + chip.Channel[chnum].ChType = (byte)ChType.Ch4Op; + chip.Channel[chnum + 3].ChType = (byte)ChType.Ch4Op2; + } else { + chip.Channel[chnum].ChType = (byte)ChType.Ch2Op; + chip.Channel[chnum + 3].ChType = (byte)ChType.Ch2Op; + } + } + } + + private static short Opl3ClipSample(int sample) { + if (sample > 32767) { + sample = 32767; + } else if (sample < -32768) { + sample = -32768; + } + return (short)sample; + } + + private static void Opl3ProcessSlot(ref Opl3Slot slot) { + Opl3SlotCalcFb(ref slot); + Opl3EnvelopeCalc(ref slot); + Opl3PhaseGenerate(ref slot); + Opl3SlotGenerate(ref slot); + } + + public static void Opl3Generate(ref Opl3Chip chip, short[] buf) { + Opl3Channel channel; + Opl3WriteBuf writebuf; + short[] output; + int mix; + byte ii; + short accm; + byte shift = 0; + + buf[1] = Opl3ClipSample(chip.MixBuff[1]); + +#if OPL_QUIRK_CHANNELSAMPLEDELAY + for (ii = 0; ii < 15; ii++) +#else + for (ii = 0; ii < 36; ii++) +#endif + { + Opl3ProcessSlot(ref chip.Slot[ii]); + } + + mix = 0; + for (ii = 0; ii < 18; ii++) { + channel = chip.Channel[ii]; + output = channel.Out; + accm = (short)(output[0] + output[1] + output[2] + output[3]); +#if OPL_ENABLE_STEREOEXT + mix += (short)((accm * channel.LeftPan) >> 16); +#else + mix += (short)(accm & channel.Cha); +#endif + } + chip.MixBuff[0] = mix; + +#if OPL_QUIRK_CHANNELSAMPLEDELAY + for (ii = 15; ii < 18; ii++) + { + Opl3ProcessSlot(ref chip.Slot[ii]); + } +#endif + + buf[0] = Opl3ClipSample(chip.MixBuff[0]); + +#if OPL_QUIRK_CHANNELSAMPLEDELAY + for (ii = 18; ii < 33; ii++) + { + Opl3ProcessSlot(ref chip.Slot[ii]); + } +#endif + + mix = 0; + for (ii = 0; ii < 18; ii++) { + channel = chip.Channel[ii]; + output = channel.Out; + accm = (short)(output[0] + output[1] + output[2] + output[3]); +#if OPL_ENABLE_STEREOEXT + mix += (short)((accm * channel.RightPan) >> 16); +#else + mix += (short)(accm & channel.Chb); +#endif + } + chip.MixBuff[1] = mix; + +#if OPL_QUIRK_CHANNELSAMPLEDELAY + for (ii = 33; ii < 36; ii++) + { + Opl3ProcessSlot(ref chip.Slot[ii]); + } +#endif + + if ((chip.Timer & 0x3f) == 0x3f) { + chip.TremoloPos = (byte)((chip.TremoloPos + 1) % 210); + } + if (chip.TremoloPos < 105) { + chip.Tremolo = (byte)(chip.TremoloPos >> chip.TremoloShift); + } else { + chip.Tremolo = (byte)((210 - chip.TremoloPos) >> chip.TremoloShift); + } + + if ((chip.Timer & 0x3ff) == 0x3ff) { + chip.VibPos = (byte)((chip.VibPos + 1) & 7); + } + + chip.Timer++; + + chip.EgAdd = 0; + if (chip.EgTimer > 0) { + while (shift < 36 && ((chip.EgTimer >> shift) & 1) == 0) { + shift++; + } + if (shift > 12) { + chip.EgAdd = 0; + } else { + chip.EgAdd = (byte)(shift + 1); + } + } + + if (chip.EgTimerRem > 0 || chip.EgState > 0) { + if (chip.EgTimer == 0xfffffffff) { + chip.EgTimer = 0; + chip.EgTimerRem = 1; + } else { + chip.EgTimer++; + chip.EgTimerRem = 0; + } + } + + chip.EgState ^= 1; + writebuf = chip.WriteBuf[chip.WriteBufCur]; + while (writebuf.Time <= chip.WritebufSampleCnt) { + if ((writebuf.Reg & 0x200) <= 0) { + break; + } + writebuf.Reg &= 0x1ff; + Opl3WriteReg(ref chip, writebuf.Reg, writebuf.Data); + chip.WriteBufCur = (chip.WriteBufCur + 1) % OplWriteBufSize; + } + chip.WritebufSampleCnt++; + } + + public static void Opl3GenerateResampled(ref Opl3Chip chip, short[] buf, uint bufOffset) { + while (chip.SampleCnt >= chip.RateRatio) { + chip.OldSamples[0] = chip.Samples[0]; + chip.OldSamples[1] = chip.Samples[1]; + Opl3Generate(ref chip, chip.Samples); + chip.SampleCnt -= chip.RateRatio; + } + buf[bufOffset + 0] = (short)((chip.OldSamples[0] * (chip.RateRatio - chip.SampleCnt) + + chip.Samples[0] * chip.SampleCnt) / chip.RateRatio); + buf[bufOffset + 1] = (short)((chip.OldSamples[1] * (chip.RateRatio - chip.SampleCnt) + + chip.Samples[1] * chip.SampleCnt) / chip.RateRatio); + chip.SampleCnt += 1 << RsmFrac; + } + + public static void Opl3Reset(ref Opl3Chip chip, uint samplerate) { + Opl3Slot slot; + Opl3Channel channel; + byte slotNum; + byte channelNum; + byte localChannelSlot; + + for (slotNum = 0; slotNum < 36; slotNum++) { + slot = chip.Slot[slotNum]; + slot.Chip = chip; + slot.Mod = chip.ZeroMod; + slot.EgRout = 0x1ff; + slot.EgOut = 0x1ff; + slot.EgGen = (byte)EnvelopeGenNum.Release; + slot.Trem = (byte)chip.ZeroMod; + slot.SlotNum = slotNum; + } + + /* (DOSBox Staging addition) + * The number of channels is not defined as a self-documenting constant + * variable and instead is represented by hardcoded literals (18) throughout + * the code. Therefore, we programmatically determine the total number of + * channels available and double check it against this magic literal. + */ + if(chip.Channel.Length != 18) { + throw new UnrecoverableException($"{nameof(chip.Channel)} must equal 18"); + } + + for (channelNum = 0; channelNum < 18; channelNum++) { + channel = chip.Channel[channelNum]; + localChannelSlot = ChSlot.Items[channelNum]; + channel.Slots[0] = chip.Slot[localChannelSlot]; + channel.Slots[1] = chip.Slot[localChannelSlot + 3]; + chip.Slot[localChannelSlot].Channel = channel; + chip.Slot[localChannelSlot + 3].Channel = channel; + if ((channelNum % 9) < 3) { + /* (DOSBox Staging addition) */ + int index = channelNum + 3; + // assert(index < channels); + channel.Pair = chip.Channel[index]; + } else if ((channelNum % 9) < 6) { + /* (DOSBox Staging addition) */ + int index = channelNum - 3; + // assert(index >= 0 && index < channels); + channel.Pair = chip.Channel[index]; + } + channel.Chip = chip; + channel.Out[0] = chip.ZeroMod; + channel.Out[1] = chip.ZeroMod; + channel.Out[2] = chip.ZeroMod; + channel.Out[3] = chip.ZeroMod; + channel.ChType = (byte)ChType.Ch2Op; + channel.Cha = 0xffff; + channel.Chb = 0xffff; +#if OPL_ENABLE_STEREOEXT + channel.leftpan = 0x10000; + channel.rightpan = 0x10000; +#endif + channel.ChNum = channelNum; + Opl3ChannelSetupAlg(channel); + } + chip.Noise = 1; + chip.RateRatio = (int)((samplerate << RsmFrac) / 49716); + chip.TremoloShift = 4; + chip.VibShift = 1; + +#if OPL_ENABLE_STEREOEXT + if (!PanpotLutBuild) + { + int i; + for (i = 0; i < 256; i++) + { + PanpotLut[i] = OplSin(i); + } + PanpotLutBuild = 1; + } +#endif + } + + public static void Opl3WriteReg(ref Opl3Chip chip, ushort reg, byte v) { + byte high = (byte)((reg >> 8) & 0x01); + byte regm = (byte)(reg & 0xff); + switch (regm & 0xf0) { + case 0x00: + if (high > 0) { + switch (regm & 0x0f) { + case 0x04: + Opl3ChannelSet4Op(ref chip, v); + break; + case 0x05: + chip.NewM = (byte)(v & 0x01); +#if OPL_ENABLE_STEREOEXT + chip.StereoExt = (v >> 1) & 0x01; +#endif + break; + } + } else { + switch (regm & 0x0f) { + case 0x08: + chip.Nts = (byte)((v >> 6) & 0x01); + break; + } + } + break; + case 0x20: + case 0x30: + if (AdSlot[regm & 0x1f] >= 0) { + Opl3SlotWrite20(ref chip.Slot[18 * high + AdSlot[regm & 0x1f]], v); + } + break; + case 0x40: + case 0x50: + if (AdSlot[regm & 0x1f] >= 0) { + Opl3SlotWrite40(ref chip.Slot[18 * high + AdSlot[regm & 0x1f]], v); + } + break; + case 0x60: + case 0x70: + if (AdSlot[regm & 0x1f] >= 0) { + Opl3SlotWrite60(ref chip.Slot[18 * high + AdSlot[regm & 0x1f]], v); + } + break; + case 0x80: + case 0x90: + if (AdSlot[regm & 0x1f] >= 0) { + Opl3SlotWrite80(ref chip.Slot[18 * high + AdSlot[regm & 0x1f]], v); + } + break; + case 0xe0: + case 0xf0: + if (AdSlot[regm & 0x1f] >= 0) { + Opl3SlotWriteE0(ref chip.Slot[18 * high + AdSlot[regm & 0x1f]], v); + } + break; + case 0xa0: + if ((regm & 0x0f) < 9) { + Opl3ChannelWriteA0(chip.Channel[9 * high + (regm & 0x0f)], v); + } + break; + case 0xb0: + if (regm == 0xbd && high <= 0) { + chip.TremoloShift = (byte)((((v >> 7) ^ 1) << 1) + 2); + chip.VibShift = (byte)(((v >> 6) & 0x01) ^ 1); + Opl3ChannelUpdateRhytm(ref chip, v); + } else if ((regm & 0x0f) < 9) { + Opl3ChannelWriteB0(chip.Channel[9 * high + (regm & 0x0f)], v); + if ((v & 0x20) > 0) { + OPL3ChannelKeyOn(chip.Channel[9 * high + (regm & 0x0f)]); + } else { + Opl3ChannelKeyOff(chip.Channel[9 * high + (regm & 0x0f)]); + } + } + break; + case 0xc0: + if ((regm & 0x0f) < 9) { + Opl3ChannelWriteC0(chip.Channel[9 * high + (regm & 0x0f)], v); + } + break; +#if OPL_ENABLE_STEREOEXT + case 0xd0: + if ((regm & 0x0f) < 9) + { + OPL3ChannelWriteD0(chip.Channel[9 * high + (regm & 0x0f)], v); + } + break; +#endif + } + } + + public static void Opl3WriteRegBuffered(ref Opl3Chip chip, ushort reg, byte v) { + ulong time1, time2; + Opl3WriteBuf writebuf; + uint writeBufLast = chip.WriteBufLast; + writebuf = chip.WriteBuf[writeBufLast]; + + if ((writebuf.Reg & 0x200) > 0) { + Opl3WriteReg(ref chip, (ushort)(writebuf.Reg & 0x1ff), writebuf.Data); + + chip.WriteBufCur = (writeBufLast + 1) % OplWriteBufSize; + chip.WritebufSampleCnt = writebuf.Time; + } + + writebuf.Reg = (ushort)(reg | 0x200); + writebuf.Data = v; + time1 = chip.WriteBufLastTime + OplWriteBufDelay; + time2 = chip.WritebufSampleCnt; + + if (time1 < time2) { + time1 = time2; + } + + writebuf.Time = time1; + chip.WriteBufLastTime = time1; + chip.WriteBufLast = (writeBufLast + 1) % OplWriteBufSize; + } + + public static void Opl3GenerateStream(ref Opl3Chip chip, short[] sndptr, uint numsamples) { + uint i; + uint sndOffset = 0; + for (i = 0; i < numsamples; i++) { + Opl3GenerateResampled(ref chip, sndptr, sndOffset); + sndOffset += 2; + } + } +} +public struct Opl3Chip { + public Opl3Chip() { + for(int i = 0; i < Channel.Length; i++) { + Channel[i] = new(); + } + } + public Opl3Channel[] Channel { get; set; } = new Opl3Channel[18]; + public Opl3Slot[] Slot { get; set; } = new Opl3Slot[36]; + public ushort Timer { get; set; } + public ulong EgTimer { get; set; } + public byte EgTimerRem { get; set; } + public byte EgState { get; set; } + public byte EgAdd { get; set; } + public byte NewM { get; set; } + public byte Nts { get; set; } + public byte Rhy { get; set; } + public byte VibPos { get; set; } + public byte VibShift { get; set; } + public byte Tremolo { get; set; } + public byte TremoloPos { get; set; } + public byte TremoloShift { get; set; } + public uint Noise { get; set; } + public short ZeroMod { get; init; } + public int[] MixBuff { get; init; } = new int[2]; + public byte RmHhBits2 { get; set; } + public byte RmHhBits3 { get; set; } + public byte RmHhBits7 { get; set; } + public byte RmHhBits8 { get; set; } + public byte RmTcBits3 { get; set; } + public byte RmTcBits5 { get; set; } + +#if OPL_ENABLE_STEREOEXT + public byte StereoExt { get; set; } +#endif + + /* OPL3L */ + public int RateRatio { get; set; } + public int SampleCnt { get; set; } + public short[] OldSamples { get; set; } = new short[2]; + + public short[] Samples { get; set; } = new short[2]; + + public ulong WritebufSampleCnt { get; set; } + + public uint WriteBufCur { get; set; } + + public uint WriteBufLast { get; set; } + + public ulong WriteBufLastTime { get; set; } + + public Opl3WriteBuf[] WriteBuf { get; set; } = new Opl3WriteBuf[Opl3Nuked.OplWriteBufSize]; +} + +public struct Opl3Slot { + public Opl3Channel Channel { get; set; } + + public Opl3Chip Chip { get; set; } + public short Out { get; set; } + public short FbMod { get; set; } + public short Mod { get; set; } + public short PrOut { get; set; } + public ushort EgRout { get; set; } + public ushort EgOut { get; set; } + public byte EgInc { get; set; } + public byte EgGen { get; set; } + public byte EgRate { get; set; } + public byte EgKsl { get; set; } + public byte Trem { get; set; } + public byte RegVib { get; set; } + public byte RegType { get; set; } + public byte RegKsr { get; set; } + public byte RegMult { get; set; } + public byte RegKsl { get; set; } + public byte RegTl { get; set; } + public byte RegAr { get; init; } + public byte RegDr { get; init; } + public byte RegSl { get; set; } + + public byte RegRr { get; set; } + public byte RegWf { get; set; } + public byte Key { get; set; } + public uint PgReset { get; set; } + public uint PgPhase { get; set; } + public ushort PgPhaseOut { get; set; } + public byte SlotNum { get; set; } +} + +public class Opl3Channel { + public Opl3Slot[] Slots { get; set; } = new Opl3Slot[2]; + public Opl3Channel? Pair { get; set; } + public Opl3Chip Chip { get; set; } + + public short[] Out { get; set; } = new short[4]; + public byte ChType { get; set; } + public ushort FNum { get; set; } + public byte Block { get; set; } + public byte Fb { get; set; } + public byte Con { get; set; } + public byte Alg { get; set; } + public byte Ksv { get; set; } + public ushort Cha { get; set; } + public ushort Chb { get; set; } + public byte ChNum { get; set; } +#if OPL_ENABLE_STEREOEXT + public int LeftPan { get; set; } + public int RightPan { get; set; } +#endif +} + +public struct Opl3WriteBuf { + public ulong Time { get; set; } + public ushort Reg { get; set; } + public byte Data { get; set; } +} diff --git a/src/Spice86.Core/Emulator/Devices/Sound/OPLConsts.cs b/src/Spice86.Core/Emulator/Devices/Sound/OPLConsts.cs new file mode 100644 index 0000000000..4bdb419eb9 --- /dev/null +++ b/src/Spice86.Core/Emulator/Devices/Sound/OPLConsts.cs @@ -0,0 +1,5 @@ +namespace Spice86.Core.Emulator.Devices.Sound; +internal static class OplConsts { + public const int FmMusicStatusPortNumber2 = 0x388; + public const int FmMusicDataPortNumber2 = 0x389; +} diff --git a/src/Spice86.Core/Emulator/Devices/Sound/OplMode.cs b/src/Spice86.Core/Emulator/Devices/Sound/OplMode.cs new file mode 100644 index 0000000000..0970f998a2 --- /dev/null +++ b/src/Spice86.Core/Emulator/Devices/Sound/OplMode.cs @@ -0,0 +1,9 @@ +namespace Spice86.Core.Emulator.Devices.Sound; + +public enum OplMode { + None, + Cms, + Opl2, + Opl3, + Opl3Gold +} \ No newline at end of file diff --git a/src/Spice86.Core/Emulator/Devices/Sound/StereoProcessorControlReg.cs b/src/Spice86.Core/Emulator/Devices/Sound/StereoProcessorControlReg.cs new file mode 100644 index 0000000000..34a48a74d2 --- /dev/null +++ b/src/Spice86.Core/Emulator/Devices/Sound/StereoProcessorControlReg.cs @@ -0,0 +1,8 @@ +namespace Spice86.Core.Emulator.Devices.Sound; +public enum StereoProcessorControlReg { + VolumeLeft, + VolumeRight, + Bass, + Treble, + SwitchFunctions, +} From 18039f5db320bd329d00bd245cd2df1b1cab2275 Mon Sep 17 00:00:00 2001 From: Maximilien Noal Date: Thu, 12 Sep 2024 21:18:07 +0200 Subject: [PATCH 3/5] fix: AdlibGlod.Process translation Signed-off-by: Maximilien Noal --- src/Spice86.Core/Emulator/Devices/Sound/AdlibGold.cs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/Spice86.Core/Emulator/Devices/Sound/AdlibGold.cs b/src/Spice86.Core/Emulator/Devices/Sound/AdlibGold.cs index c8bad19efb..926ec30cbf 100644 --- a/src/Spice86.Core/Emulator/Devices/Sound/AdlibGold.cs +++ b/src/Spice86.Core/Emulator/Devices/Sound/AdlibGold.cs @@ -27,9 +27,12 @@ public AdlibGold(ILoggerService loggerService) { public void SurroundControlWrite(byte data) => _surroundProcessor.ControlWrite(data); - private void Process(short[] input, uint framesRemaining, AudioFrame output) { - for (var index = 0; framesRemaining-- > 0; index++) { - AudioFrame frame = new(output.AsSpan()); + private void Process(Span input, uint frames, Span output) { + uint framesRemaining = frames; + int index = 0; + while(framesRemaining-- > 0) { + Span inputPart = input.Slice(index, 2); + AudioFrame frame = new AudioFrame(inputPart.Cast()); AudioFrame wet = _surroundProcessor.Process(frame); // Additional wet signal level boost to make the emulated @@ -41,6 +44,7 @@ private void Process(short[] input, uint framesRemaining, AudioFrame output) { output[index] = frame.Left; output[index + 1] = frame.Right; + index += 2; } } From 8844859d4dd192074d9777855744faf951b2dd62 Mon Sep 17 00:00:00 2001 From: Maximilien Noal Date: Mon, 16 Sep 2024 21:03:09 +0200 Subject: [PATCH 4/5] refactor: wire up the new OPL Here be dragons. Signed-off-by: Maximilien Noal --- .../Devices/Sound/Blaster/SoundBlaster.cs | 10 +-- .../Emulator/Devices/Sound/OPL.cs | 72 ++++++++++++++----- src/Spice86.Core/Emulator/VM/Machine.cs | 10 ++- src/Spice86/Spice86DependencyInjection.cs | 6 +- 4 files changed, 65 insertions(+), 33 deletions(-) diff --git a/src/Spice86.Core/Emulator/Devices/Sound/Blaster/SoundBlaster.cs b/src/Spice86.Core/Emulator/Devices/Sound/Blaster/SoundBlaster.cs index 4c74a408f4..a036e86891 100644 --- a/src/Spice86.Core/Emulator/Devices/Sound/Blaster/SoundBlaster.cs +++ b/src/Spice86.Core/Emulator/Devices/Sound/Blaster/SoundBlaster.cs @@ -155,11 +155,6 @@ public class SoundBlaster : DefaultIOPortHandler, IDmaDevice8, IDmaDevice16, IRe /// The SoundBlaster's OPL3 FM sound channel. /// public SoundChannel FMSynthSoundChannel { get; } - - /// - /// The internal FM synth chip for music. - /// - public OPL3FM Opl3Fm { get; } /// /// The type of SoundBlaster card currently emulated. @@ -196,7 +191,6 @@ public SoundBlaster(IOPortDispatcher ioPortDispatcher, SoftwareMixer softwareMix }; PCMSoundChannel = softwareMixer.CreateChannel(nameof(SoundBlaster)); FMSynthSoundChannel = softwareMixer.CreateChannel(nameof(OPL3FM)); - Opl3Fm = new OPL3FM(FMSynthSoundChannel, state, ioPortDispatcher, failOnUnhandledPort, loggerService, pauseHandler); _ctMixer = new HardwareMixer(soundBlasterHardwareConfig, PCMSoundChannel, FMSynthSoundChannel, loggerService); InitPortHandlers(ioPortDispatcher); } @@ -370,8 +364,8 @@ private void InitPortHandlers(IOPortDispatcher ioPortDispatcher) { ioPortDispatcher.AddIOPortHandler(LEFT_SPEAKER_DATA_PORT_NUMBER, this); ioPortDispatcher.AddIOPortHandler(RIGHT_SPEAKER_STATUS_PORT_NUMBER, this); ioPortDispatcher.AddIOPortHandler(RIGHT_SPEAKER_DATA_PORT_NUMBER, this); - ioPortDispatcher.AddIOPortHandler(FM_MUSIC_STATUS_PORT_NUMBER, this); - ioPortDispatcher.AddIOPortHandler(FM_MUSIC_DATA_PORT_NUMBER, this); + //ioPortDispatcher.AddIOPortHandler(FM_MUSIC_STATUS_PORT_NUMBER, this); + //ioPortDispatcher.AddIOPortHandler(FM_MUSIC_DATA_PORT_NUMBER, this); ioPortDispatcher.AddIOPortHandler(IGNORE_PORT, this); // Those are managed by OPL3FM class. //ioPortDispatcher.AddIOPortHandler(FM_MUSIC_STATUS_PORT_NUMBER_2, this); diff --git a/src/Spice86.Core/Emulator/Devices/Sound/OPL.cs b/src/Spice86.Core/Emulator/Devices/Sound/OPL.cs index 2cb48f32bf..bee20ca2a0 100644 --- a/src/Spice86.Core/Emulator/Devices/Sound/OPL.cs +++ b/src/Spice86.Core/Emulator/Devices/Sound/OPL.cs @@ -1,9 +1,11 @@ namespace Spice86.Core.Emulator.Devices.Sound; -using System.Runtime.InteropServices; +using Spice86.Core.Emulator.CPU; +using Spice86.Core.Emulator.Devices.Timer; +using Spice86.Core.Emulator.IOPorts; +using Spice86.Shared.Interfaces; -using Spice86.Core.Emulator.Devices.Sound.Ym7128b; -using Spice86.Core.Emulator.VM; +using System.Runtime.InteropServices; public enum Mode { Opl2, DualOpl2, Opl3, Opl3Gold @@ -105,9 +107,9 @@ public bool Update(double time) { } public class Chip { - private Machine _machine; - public Chip(Machine machine) { - _machine = machine; + private Timer _timer; + public Chip(Timer timer) { + _timer = timer; Timer0 = new(80); Timer1 = new(320); } @@ -126,11 +128,11 @@ public bool Write(ushort reg, byte val) { // LOG(LOG_MISC,LOG_ERROR)("write adlib timer %X %X",reg,val); switch (reg) { case 0x02: - Timer0.Update(TimeSpan.FromTicks(_machine.Timer.NumberOfTicks).TotalMilliseconds); + Timer0.Update(TimeSpan.FromTicks(_timer.NumberOfTicks).TotalMilliseconds); Timer0.SetCounter(val); return true; case 0x03: - Timer1.Update(TimeSpan.FromTicks(_machine.Timer.NumberOfTicks).TotalMilliseconds); + Timer1.Update(TimeSpan.FromTicks(_timer.NumberOfTicks).TotalMilliseconds); Timer1.SetCounter(val); return true; case 0x04: @@ -139,7 +141,7 @@ public bool Write(ushort reg, byte val) { Timer0.Reset(); Timer1.Reset(); } else { - double time = TimeSpan.FromTicks(_machine.Timer.NumberOfTicks).TotalMilliseconds; + double time = TimeSpan.FromTicks(_timer.NumberOfTicks).TotalMilliseconds; if ((val & 0x1) > 0) { Timer0.Start(time); } else { @@ -163,7 +165,7 @@ public bool Write(ushort reg, byte val) { /// Read the current timer state, will use current double /// public byte Read() { - TimeSpan time = TimeSpan.FromTicks(_machine.Timer.NumberOfTicks); + TimeSpan time = TimeSpan.FromTicks(_timer.NumberOfTicks); byte ret = 0; // Overflow won't be set if a channel is masked @@ -179,7 +181,10 @@ public byte Read() { } } -public class Opl { +/// +/// The OPL3 / OPL2 / Adlib Gold OPL chip emulation class. +/// +public class Opl : DefaultIOPortHandler { public const byte DefaultVolumeValue = 0xff; //public MixerChannel Channel { get; private set; } = new(); @@ -200,6 +205,8 @@ public class Opl { private byte _mem; private AdlibGold _adlibGold; + + private OplMode _oplMode; // Playback related private double _lastRenderedMs = 0.0; @@ -208,16 +215,13 @@ public class Opl { // Last selected address in the chip for the different modes private const int DefaultVolume = 0xff; - - - [StructLayout(LayoutKind.Explicit)] + private struct Reg { - [FieldOffset(0)] public byte normal; - [FieldOffset(0)] public byte[] dual; public Reg() { + normal = 0; dual = new byte[2]; } } @@ -226,11 +230,43 @@ public Reg() { private Control _ctrl = new(); - public Opl(AdlibGold adlibGold, OplMode mode) { + private bool _dualOpl = false; + + public Opl(State state, Timer timer, IOPortDispatcher ioPortDispatcher, + bool failOnUnhandledPort, ILoggerService loggerService, + AdlibGold adlibGold, OplMode oplMode) + : base(state, failOnUnhandledPort, loggerService) { _adlibGold = adlibGold; + _oplMode = oplMode; + _chip[0] = new Chip(timer); + _chip[1] = new Chip(timer); + InitPortHandlers(ioPortDispatcher); + } + + public override byte ReadByte(ushort port) { + return _chip[0].Read(); + } + + public override void WriteByte(ushort port, byte value) { + _chip[0].Write(port, value); } - private void AdlibGoldControlWrite(byte val) { + private void InitPortHandlers(IOPortDispatcher ioPortDispatcher) { + ioPortDispatcher.AddIOPortHandler(0x388, this); + ioPortDispatcher.AddIOPortHandler(0x38b, this); + if (_dualOpl) { + //Read/Write + ioPortDispatcher.AddIOPortHandler(0x220, this); + //Read/Write + ioPortDispatcher.AddIOPortHandler(0x223, this); + } + //Read/Write + ioPortDispatcher.AddIOPortHandler(0x228, this); + //Write + ioPortDispatcher.AddIOPortHandler(0x229, this); + } + + private void AdlibGoldControlWrite(byte val) { switch (_ctrl.Index) { case 0x04: _adlibGold.StereoControlWrite((byte)StereoProcessorControlReg.VolumeLeft, diff --git a/src/Spice86.Core/Emulator/VM/Machine.cs b/src/Spice86.Core/Emulator/VM/Machine.cs index d3838871db..d3f4143b2e 100644 --- a/src/Spice86.Core/Emulator/VM/Machine.cs +++ b/src/Spice86.Core/Emulator/VM/Machine.cs @@ -10,12 +10,10 @@ using Spice86.Core.Emulator.Devices.Sound.Blaster; using Spice86.Core.Emulator.Devices.Sound.Midi; using Spice86.Core.Emulator.Devices.Sound.PCSpeaker; -using Spice86.Core.Emulator.Devices.Sound.Ymf262Emu; using Spice86.Core.Emulator.Devices.Timer; using Spice86.Core.Emulator.Devices.Video; using Spice86.Core.Emulator.InterruptHandlers.Bios; using Spice86.Core.Emulator.InterruptHandlers.Common.Callback; -using Spice86.Core.Emulator.InterruptHandlers.Common.RoutineInstall; using Spice86.Core.Emulator.InterruptHandlers.Input.Keyboard; using Spice86.Core.Emulator.InterruptHandlers.Input.Mouse; using Spice86.Core.Emulator.InterruptHandlers.SystemClock; @@ -191,7 +189,7 @@ public sealed class Machine : IDisposable { /// /// The OPL3 FM Synth chip. /// - public OPL3FM OPL3FM { get; } + public Opl OPL { get; } /// /// The internal software mixer for all sound channels. @@ -226,7 +224,7 @@ public sealed class Machine : IDisposable { /// /// Initializes a new instance of the class. /// - public Machine(BiosDataArea biosDataArea, BiosEquipmentDeterminationInt11Handler biosEquipmentDeterminationInt11Handler, BiosKeyboardInt9Handler biosKeyboardInt9Handler, CallbackHandler callbackHandler, Cpu cpu, CfgCpu cfgCpu, State cpuState, Dos dos, GravisUltraSound gravisUltraSound, IOPortDispatcher ioPortDispatcher, Joystick joystick, Keyboard keyboard, KeyboardInt16Handler keyboardInt16Handler, EmulatorBreakpointsManager emulatorBreakpointsManager, IMemory memory, Midi midiDevice, PcSpeaker pcSpeaker, DualPic dualPic, SoundBlaster soundBlaster, SystemBiosInt12Handler systemBiosInt12Handler, SystemBiosInt15Handler systemBiosInt15Handler, SystemClockInt1AHandler systemClockInt1AHandler, Timer timer, TimerInt8Handler timerInt8Handler, VgaCard vgaCard, IVideoState vgaRegisters, IIOPortHandler vgaIoPortHandler, IVgaRenderer vgaRenderer, IVideoInt10Handler videoInt10Handler, VgaRom vgaRom, DmaController dmaController, OPL3FM opl3FM, SoftwareMixer softwareMixer, IMouseDevice mouseDevice, IMouseDriver mouseDriver, IVgaFunctionality vgaFunctions, IPauseHandler pauseHandler) { + public Machine(BiosDataArea biosDataArea, BiosEquipmentDeterminationInt11Handler biosEquipmentDeterminationInt11Handler, BiosKeyboardInt9Handler biosKeyboardInt9Handler, CallbackHandler callbackHandler, Cpu cpu, CfgCpu cfgCpu, State cpuState, Dos dos, GravisUltraSound gravisUltraSound, IOPortDispatcher ioPortDispatcher, Joystick joystick, Keyboard keyboard, KeyboardInt16Handler keyboardInt16Handler, EmulatorBreakpointsManager emulatorBreakpointsManager, IMemory memory, Midi midiDevice, PcSpeaker pcSpeaker, DualPic dualPic, SoundBlaster soundBlaster, SystemBiosInt12Handler systemBiosInt12Handler, SystemBiosInt15Handler systemBiosInt15Handler, SystemClockInt1AHandler systemClockInt1AHandler, Timer timer, TimerInt8Handler timerInt8Handler, VgaCard vgaCard, IVideoState vgaRegisters, IIOPortHandler vgaIoPortHandler, IVgaRenderer vgaRenderer, IVideoInt10Handler videoInt10Handler, VgaRom vgaRom, DmaController dmaController, Opl opl, SoftwareMixer softwareMixer, IMouseDevice mouseDevice, IMouseDriver mouseDriver, IVgaFunctionality vgaFunctions, IPauseHandler pauseHandler) { BiosDataArea = biosDataArea; BiosEquipmentDeterminationInt11Handler = biosEquipmentDeterminationInt11Handler; BiosKeyboardInt9Handler = biosKeyboardInt9Handler; @@ -258,7 +256,7 @@ public Machine(BiosDataArea biosDataArea, BiosEquipmentDeterminationInt11Handler VideoInt10Handler = videoInt10Handler; VgaRom = vgaRom; DmaController = dmaController; - OPL3FM = opl3FM; + OPL = opl; SoftwareMixer = softwareMixer; MouseDevice = mouseDevice; MouseDriver = mouseDriver; @@ -275,7 +273,7 @@ private void Dispose(bool disposing) { if (disposing) { MidiDevice.Dispose(); SoundBlaster.Dispose(); - OPL3FM.Dispose(); + //OPL3FM.Dispose(); PcSpeaker.Dispose(); SoftwareMixer.Dispose(); } diff --git a/src/Spice86/Spice86DependencyInjection.cs b/src/Spice86/Spice86DependencyInjection.cs index ab68ac52c3..da19694e0e 100644 --- a/src/Spice86/Spice86DependencyInjection.cs +++ b/src/Spice86/Spice86DependencyInjection.cs @@ -41,6 +41,7 @@ namespace Spice86; using Spice86.Core.Emulator.VM; using Spice86.Core.Emulator.VM.Breakpoint; using Spice86.Infrastructure; +using Spice86.Logging; using Spice86.Shared.Emulator.Memory; using Spice86.Shared.Interfaces; using Spice86.Shared.Utils; @@ -190,7 +191,10 @@ public Spice86DependencyInjection(ILoggerService loggerService, Configuration co timer, timerInt8Handler, vgaCard, videoState, videoInt10Handler, vgaRenderer, vgaBios, vgaRom, - dmaController, soundBlaster.Opl3Fm, softwareMixer, mouse, mouseDriver, + dmaController, new Opl( + state, timer, ioPortDispatcher, configuration.FailOnUnhandledPort, loggerService, + new AdlibGold(loggerService), OplMode.Opl3), + softwareMixer, mouse, mouseDriver, vgaFunctionality, pauseHandler); IDictionary functionsInformation = reader.ReadGhidraSymbolsFromFileOrCreate(); From 9a0a8844f5110e9217c7675430ad5187af36477f Mon Sep 17 00:00:00 2001 From: Maximilien Noal Date: Mon, 17 Feb 2025 07:54:37 +0100 Subject: [PATCH 5/5] refactor: OPL class as a front to NukedOPL3 --- .../Devices/Sound/Blaster/SoundBlaster.cs | 2 +- .../Devices/Sound/{OPL.cs => OplFmSynth.cs} | 36 +---- .../Ymf262Emu/{OPL3FM.cs => OPLFMChip.cs} | 131 +++++++++--------- src/Spice86.Core/Emulator/VM/Machine.cs | 5 +- src/Spice86/Spice86DependencyInjection.cs | 8 +- 5 files changed, 81 insertions(+), 101 deletions(-) rename src/Spice86.Core/Emulator/Devices/Sound/{OPL.cs => OplFmSynth.cs} (87%) rename src/Spice86.Core/Emulator/Devices/Sound/Ymf262Emu/{OPL3FM.cs => OPLFMChip.cs} (54%) diff --git a/src/Spice86.Core/Emulator/Devices/Sound/Blaster/SoundBlaster.cs b/src/Spice86.Core/Emulator/Devices/Sound/Blaster/SoundBlaster.cs index a036e86891..ed290eac64 100644 --- a/src/Spice86.Core/Emulator/Devices/Sound/Blaster/SoundBlaster.cs +++ b/src/Spice86.Core/Emulator/Devices/Sound/Blaster/SoundBlaster.cs @@ -190,7 +190,7 @@ public SoundBlaster(IOPortDispatcher ioPortDispatcher, SoftwareMixer softwareMix Name = nameof(SoundBlaster), }; PCMSoundChannel = softwareMixer.CreateChannel(nameof(SoundBlaster)); - FMSynthSoundChannel = softwareMixer.CreateChannel(nameof(OPL3FM)); + FMSynthSoundChannel = softwareMixer.CreateChannel(nameof(OPLFMChip)); _ctMixer = new HardwareMixer(soundBlasterHardwareConfig, PCMSoundChannel, FMSynthSoundChannel, loggerService); InitPortHandlers(ioPortDispatcher); } diff --git a/src/Spice86.Core/Emulator/Devices/Sound/OPL.cs b/src/Spice86.Core/Emulator/Devices/Sound/OplFmSynth.cs similarity index 87% rename from src/Spice86.Core/Emulator/Devices/Sound/OPL.cs rename to src/Spice86.Core/Emulator/Devices/Sound/OplFmSynth.cs index bee20ca2a0..cadad5c458 100644 --- a/src/Spice86.Core/Emulator/Devices/Sound/OPL.cs +++ b/src/Spice86.Core/Emulator/Devices/Sound/OplFmSynth.cs @@ -14,7 +14,7 @@ public enum Mode { public struct Control { public Control() { } public byte Index { get; set; } - public byte LVol { get; set; } = Opl.DefaultVolumeValue; + public byte LVol { get; set; } = OplFmSynth.DefaultVolumeValue; public byte RVol { get; set; } public bool IsActive { get; set; } @@ -184,11 +184,9 @@ public byte Read() { /// /// The OPL3 / OPL2 / Adlib Gold OPL chip emulation class. /// -public class Opl : DefaultIOPortHandler { +public class OplFmSynth { public const byte DefaultVolumeValue = 0xff; - //public MixerChannel Channel { get; private set; } = new(); - /// /// The cache for 2 chips or an OPL3 /// @@ -232,40 +230,14 @@ public Reg() { private bool _dualOpl = false; - public Opl(State state, Timer timer, IOPortDispatcher ioPortDispatcher, - bool failOnUnhandledPort, ILoggerService loggerService, - AdlibGold adlibGold, OplMode oplMode) - : base(state, failOnUnhandledPort, loggerService) { + public OplFmSynth(Timer timer, + AdlibGold adlibGold, OplMode oplMode) { _adlibGold = adlibGold; _oplMode = oplMode; _chip[0] = new Chip(timer); _chip[1] = new Chip(timer); - InitPortHandlers(ioPortDispatcher); - } - - public override byte ReadByte(ushort port) { - return _chip[0].Read(); } - public override void WriteByte(ushort port, byte value) { - _chip[0].Write(port, value); - } - - private void InitPortHandlers(IOPortDispatcher ioPortDispatcher) { - ioPortDispatcher.AddIOPortHandler(0x388, this); - ioPortDispatcher.AddIOPortHandler(0x38b, this); - if (_dualOpl) { - //Read/Write - ioPortDispatcher.AddIOPortHandler(0x220, this); - //Read/Write - ioPortDispatcher.AddIOPortHandler(0x223, this); - } - //Read/Write - ioPortDispatcher.AddIOPortHandler(0x228, this); - //Write - ioPortDispatcher.AddIOPortHandler(0x229, this); - } - private void AdlibGoldControlWrite(byte val) { switch (_ctrl.Index) { case 0x04: diff --git a/src/Spice86.Core/Emulator/Devices/Sound/Ymf262Emu/OPL3FM.cs b/src/Spice86.Core/Emulator/Devices/Sound/Ymf262Emu/OPLFMChip.cs similarity index 54% rename from src/Spice86.Core/Emulator/Devices/Sound/Ymf262Emu/OPL3FM.cs rename to src/Spice86.Core/Emulator/Devices/Sound/Ymf262Emu/OPLFMChip.cs index 549a3de22f..9665480386 100644 --- a/src/Spice86.Core/Emulator/Devices/Sound/Ymf262Emu/OPL3FM.cs +++ b/src/Spice86.Core/Emulator/Devices/Sound/Ymf262Emu/OPLFMChip.cs @@ -8,54 +8,62 @@ using System; /// -/// Virtual device which emulates OPL3 FM sound. +/// The class responsible for emulating the OPL FM music synth chip. /// -public class OPL3FM : DefaultIOPortHandler, IDisposable { +public class OPLFMChip : DefaultIOPortHandler, IDisposable { private const byte Timer1Mask = 0xC0; private const byte Timer2Mask = 0xA0; + private bool _dualOpl = false; + private readonly SoundChannel _soundChannel; //TODO: replace it with nukedOpl3. private readonly dynamic? _synth; private readonly IPauseHandler _pauseHandler; - private int _currentAddress; private volatile bool _endThread; private readonly Thread _playbackThread; - private bool _initialized; - private byte _statusByte; - private byte _timer1Data; - private byte _timer2Data; - private byte _timerControlByte; - private bool _disposed; /// - /// The sound channel used for the OPL3 FM synth. + /// The sound channel used for rendering audio. /// public SoundChannel SoundChannel => _soundChannel; /// - /// Initializes a new instance of the OPL3 FM synth chip. + /// Initializes a new instance of the OPL FM synth chip. /// - /// The software mixer's sound channel for the OPL3 FM Synth chip. + /// The software mixer's sound channel for the OPL FM Synth chip. /// The CPU registers and flags. /// The class that is responsible for dispatching ports reads and writes to classes that respond to them. /// Whether we throw an exception when an I/O port wasn't handled. /// The logger service implementation. /// Class for handling pausing the emulator. - public OPL3FM(SoundChannel fmSynthSoundChannel, State state, IOPortDispatcher ioPortDispatcher, bool failOnUnhandledPort, ILoggerService loggerService, IPauseHandler pauseHandler) : base(state, failOnUnhandledPort, loggerService) { + public OPLFMChip(SoundChannel fmSynthSoundChannel, State state, IOPortDispatcher ioPortDispatcher, bool failOnUnhandledPort, ILoggerService loggerService, IPauseHandler pauseHandler) : base(state, failOnUnhandledPort, loggerService) { _pauseHandler = pauseHandler; _soundChannel = fmSynthSoundChannel; //_synth = new(48000); _playbackThread = new Thread(GenerateWaveforms) { - Name = nameof(OPL3FM) + Name = nameof(OPLFMChip) }; InitPortHandlers(ioPortDispatcher); + StartPlaybackThread(); } + private void InitPortHandlers(IOPortDispatcher ioPortDispatcher) { ioPortDispatcher.AddIOPortHandler(0x388, this); ioPortDispatcher.AddIOPortHandler(0x389, this); + ioPortDispatcher.AddIOPortHandler(0x38b, this); + if (_dualOpl) { + //Read/Write + ioPortDispatcher.AddIOPortHandler(0x220, this); + //Read/Write + ioPortDispatcher.AddIOPortHandler(0x223, this); + } + //Read/Write + ioPortDispatcher.AddIOPortHandler(0x228, this); + //Write + ioPortDispatcher.AddIOPortHandler(0x229, this); } /// @@ -72,7 +80,6 @@ private void Dispose(bool disposing) { if (_playbackThread.IsAlive) { _playbackThread.Join(); } - _initialized = false; } _disposed = true; } @@ -80,58 +87,60 @@ private void Dispose(bool disposing) { /// public override byte ReadByte(ushort port) { - if ((_timerControlByte & 0x01) != 0x00 && (_statusByte & Timer1Mask) == 0) { - _timer1Data++; - if (_timer1Data == 0) { - _statusByte |= Timer1Mask; - } - } - - if ((_timerControlByte & 0x02) != 0x00 && (_statusByte & Timer2Mask) == 0) { - _timer2Data++; - if (_timer2Data == 0) { - _statusByte |= Timer2Mask; - } - } - - return _statusByte; + //if ((_timerControlByte & 0x01) != 0x00 && (_statusByte & Timer1Mask) == 0) { + // _timer1Data++; + // if (_timer1Data == 0) { + // _statusByte |= Timer1Mask; + // } + //} + + //if ((_timerControlByte & 0x02) != 0x00 && (_statusByte & Timer2Mask) == 0) { + // _timer2Data++; + // if (_timer2Data == 0) { + // _statusByte |= Timer2Mask; + // } + //} + + //return _statusByte; + throw new NotImplementedException(); } /// public override ushort ReadWord(ushort port) { - return _statusByte; + //return _statusByte; + throw new NotImplementedException(); } /// public override void WriteByte(ushort port, byte value) { - if (port == 0x388) { - _currentAddress = value; - } else if (port == 0x389) { - if (_currentAddress == 0x02) { - _timer1Data = value; - } else if (_currentAddress == 0x03) { - _timer2Data = value; - } else if (_currentAddress == 0x04) { - _timerControlByte = value; - if ((value & 0x80) == 0x80) { - _statusByte = 0; - } - } else { - if (!_initialized) { - StartPlaybackThread(); - } - - _synth?.SetRegisterValue(0, _currentAddress, value); - } - } + //if (port == 0x388) { + // _currentAddress = value; + //} else if (port == 0x389) { + // if (_currentAddress == 0x02) { + // _timer1Data = value; + // } else if (_currentAddress == 0x03) { + // _timer2Data = value; + // } else if (_currentAddress == 0x04) { + // _timerControlByte = value; + // if ((value & 0x80) == 0x80) { + // _statusByte = 0; + // } + // } else { + // if (!_initialized) { + // StartPlaybackThread(); + // } + + // _synth?.SetRegisterValue(0, _currentAddress, value); + // } + //} } /// public override void WriteWord(ushort port, ushort value) { - if (port == 0x388) { - WriteByte(0x388, (byte)value); - WriteByte(0x389, (byte)(value >> 8)); - } + //if (port == 0x388) { + // WriteByte(0x388, (byte)value); + // WriteByte(0x389, (byte)(value >> 8)); + //} } /// /// Generates and plays back output waveform data. @@ -140,25 +149,21 @@ private void GenerateWaveforms() { const int length = 1024; Span buffer = stackalloc float[length]; Span playBuffer = stackalloc float[length * 2]; - FillBuffer(buffer, playBuffer); + MonoToStereo(buffer, playBuffer); while (!_endThread) { _pauseHandler.WaitIfPaused(); _soundChannel.Render(playBuffer); - FillBuffer(buffer, playBuffer); + MonoToStereo(buffer, playBuffer); } } - private void FillBuffer(Span buffer, Span playBuffer) { + private void MonoToStereo(Span buffer, Span playBuffer) { //_synth?.GetData(buffer); ChannelAdapter.MonoToStereo(buffer, playBuffer); } private void StartPlaybackThread() { - if (_endThread) { - return; - } - _loggerService.Information("Starting thread '{ThreadName}'", _playbackThread.Name ?? nameof(OPL3FM)); - _initialized = true; + _loggerService.Information("Starting thread '{ThreadName}'", _playbackThread.Name ?? nameof(OPLFMChip)); _playbackThread.Start(); } } \ No newline at end of file diff --git a/src/Spice86.Core/Emulator/VM/Machine.cs b/src/Spice86.Core/Emulator/VM/Machine.cs index d3f4143b2e..a0ebdd9053 100644 --- a/src/Spice86.Core/Emulator/VM/Machine.cs +++ b/src/Spice86.Core/Emulator/VM/Machine.cs @@ -10,6 +10,7 @@ using Spice86.Core.Emulator.Devices.Sound.Blaster; using Spice86.Core.Emulator.Devices.Sound.Midi; using Spice86.Core.Emulator.Devices.Sound.PCSpeaker; +using Spice86.Core.Emulator.Devices.Sound.Ymf262Emu; using Spice86.Core.Emulator.Devices.Timer; using Spice86.Core.Emulator.Devices.Video; using Spice86.Core.Emulator.InterruptHandlers.Bios; @@ -189,7 +190,7 @@ public sealed class Machine : IDisposable { /// /// The OPL3 FM Synth chip. /// - public Opl OPL { get; } + public OPLFMChip OPL { get; } /// /// The internal software mixer for all sound channels. @@ -224,7 +225,7 @@ public sealed class Machine : IDisposable { /// /// Initializes a new instance of the class. /// - public Machine(BiosDataArea biosDataArea, BiosEquipmentDeterminationInt11Handler biosEquipmentDeterminationInt11Handler, BiosKeyboardInt9Handler biosKeyboardInt9Handler, CallbackHandler callbackHandler, Cpu cpu, CfgCpu cfgCpu, State cpuState, Dos dos, GravisUltraSound gravisUltraSound, IOPortDispatcher ioPortDispatcher, Joystick joystick, Keyboard keyboard, KeyboardInt16Handler keyboardInt16Handler, EmulatorBreakpointsManager emulatorBreakpointsManager, IMemory memory, Midi midiDevice, PcSpeaker pcSpeaker, DualPic dualPic, SoundBlaster soundBlaster, SystemBiosInt12Handler systemBiosInt12Handler, SystemBiosInt15Handler systemBiosInt15Handler, SystemClockInt1AHandler systemClockInt1AHandler, Timer timer, TimerInt8Handler timerInt8Handler, VgaCard vgaCard, IVideoState vgaRegisters, IIOPortHandler vgaIoPortHandler, IVgaRenderer vgaRenderer, IVideoInt10Handler videoInt10Handler, VgaRom vgaRom, DmaController dmaController, Opl opl, SoftwareMixer softwareMixer, IMouseDevice mouseDevice, IMouseDriver mouseDriver, IVgaFunctionality vgaFunctions, IPauseHandler pauseHandler) { + public Machine(BiosDataArea biosDataArea, BiosEquipmentDeterminationInt11Handler biosEquipmentDeterminationInt11Handler, BiosKeyboardInt9Handler biosKeyboardInt9Handler, CallbackHandler callbackHandler, Cpu cpu, CfgCpu cfgCpu, State cpuState, Dos dos, GravisUltraSound gravisUltraSound, IOPortDispatcher ioPortDispatcher, Joystick joystick, Keyboard keyboard, KeyboardInt16Handler keyboardInt16Handler, EmulatorBreakpointsManager emulatorBreakpointsManager, IMemory memory, Midi midiDevice, PcSpeaker pcSpeaker, DualPic dualPic, SoundBlaster soundBlaster, SystemBiosInt12Handler systemBiosInt12Handler, SystemBiosInt15Handler systemBiosInt15Handler, SystemClockInt1AHandler systemClockInt1AHandler, Timer timer, TimerInt8Handler timerInt8Handler, VgaCard vgaCard, IVideoState vgaRegisters, IIOPortHandler vgaIoPortHandler, IVgaRenderer vgaRenderer, IVideoInt10Handler videoInt10Handler, VgaRom vgaRom, DmaController dmaController, OPLFMChip opl, SoftwareMixer softwareMixer, IMouseDevice mouseDevice, IMouseDriver mouseDriver, IVgaFunctionality vgaFunctions, IPauseHandler pauseHandler) { BiosDataArea = biosDataArea; BiosEquipmentDeterminationInt11Handler = biosEquipmentDeterminationInt11Handler; BiosKeyboardInt9Handler = biosKeyboardInt9Handler; diff --git a/src/Spice86/Spice86DependencyInjection.cs b/src/Spice86/Spice86DependencyInjection.cs index da19694e0e..be7992cd07 100644 --- a/src/Spice86/Spice86DependencyInjection.cs +++ b/src/Spice86/Spice86DependencyInjection.cs @@ -22,6 +22,7 @@ namespace Spice86; using Spice86.Core.Emulator.Devices.Sound.Blaster; using Spice86.Core.Emulator.Devices.Sound.Midi; using Spice86.Core.Emulator.Devices.Sound.PCSpeaker; +using Spice86.Core.Emulator.Devices.Sound.Ymf262Emu; using Spice86.Core.Emulator.Devices.Timer; using Spice86.Core.Emulator.Devices.Video; using Spice86.Core.Emulator.Function; @@ -191,9 +192,10 @@ public Spice86DependencyInjection(ILoggerService loggerService, Configuration co timer, timerInt8Handler, vgaCard, videoState, videoInt10Handler, vgaRenderer, vgaBios, vgaRom, - dmaController, new Opl( - state, timer, ioPortDispatcher, configuration.FailOnUnhandledPort, loggerService, - new AdlibGold(loggerService), OplMode.Opl3), + dmaController, new OPLFMChip( + soundBlaster.FMSynthSoundChannel, state, + ioPortDispatcher, configuration.FailOnUnhandledPort, + loggerService, pauseHandler), softwareMixer, mouse, mouseDriver, vgaFunctionality, pauseHandler);