Essgee.Unity/Assets/Plugins/Essgee/Emulation/Audio/DMGAudio.Square.cs
2025-01-02 17:55:16 +08:00

274 lines
5.9 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Essgee.Emulation.Audio
{
public partial class DMGAudio
{
public class Square : IDMGAudioChannel
{
static readonly bool[,] dutyCycleTable = new bool[,]
{
{ false, false, false, false, false, false, false, true, }, // 00000001 12.5%
{ true, false, false, false, false, false, false, true, }, // 10000001 25%
{ true, false, false, false, false, true, true, true, }, // 10000111 50%
{ false, true, true, true, true, true, true, false, } // 01111110 75%
};
// NR10/20
byte sweepPeriodReload, sweepShift;
bool sweepNegate;
// NR11/21
byte dutyCycle, lengthLoad;
// NR12/22
byte envelopeStartingVolume, envelopePeriodReload;
bool envelopeAddMode;
// NR13/23
byte frequencyLSB;
// NR14/24
bool trigger, lengthEnable;
byte frequencyMSB;
//
readonly bool channelSupportsSweep;
// Sweep
bool isSweepEnabled;
int sweepCounter, sweepFreqShadow;
// Frequency
int frequencyCounter;
// Envelope
int volume, envelopeCounter;
bool isEnvelopeUpdateEnabled;
// Misc
bool isChannelEnabled, isDacEnabled;
int lengthCounter, dutyCounter;
public int OutputVolume { get; private set; }
public bool IsActive { get { return lengthCounter != 0; } }
public Square(bool hasSweep)
{
channelSupportsSweep = hasSweep;
}
public void Reset()
{
isSweepEnabled = false;
sweepCounter = sweepFreqShadow = 0;
frequencyCounter = 0;
volume = 15;
envelopeCounter = 0;
isEnvelopeUpdateEnabled = false;
isChannelEnabled = isDacEnabled = false;
lengthCounter = dutyCounter = 0;
OutputVolume = volume;
}
public void LengthCounterClock()
{
if (lengthCounter > 0 && lengthEnable)
{
lengthCounter--;
if (lengthCounter == 0)
isChannelEnabled = false;
}
}
public void SweepClock()
{
if (!channelSupportsSweep) return;
sweepCounter--;
if (sweepCounter == 0)
{
sweepCounter = sweepPeriodReload;
if (isSweepEnabled && sweepPeriodReload != 0)
{
var newFrequency = PerformSweepCalculations();
if (newFrequency <= 2047 && sweepShift != 0)
{
sweepFreqShadow = newFrequency;
frequencyMSB = (byte)((newFrequency >> 8) & 0b111);
frequencyLSB = (byte)(newFrequency & 0xFF);
PerformSweepCalculations();
}
}
}
}
public void VolumeEnvelopeClock()
{
envelopeCounter--;
if (envelopeCounter == 0)
{
envelopeCounter = envelopePeriodReload;
if (isEnvelopeUpdateEnabled)
{
var newVolume = volume;
if (envelopeAddMode) newVolume++;
else newVolume--;
if (newVolume >= 0 && newVolume <= 15)
volume = newVolume;
else
isEnvelopeUpdateEnabled = false;
}
}
}
public void Step()
{
if (!isChannelEnabled) return;
frequencyCounter--;
if (frequencyCounter == 0)
{
frequencyCounter = (2048 - ((frequencyMSB << 8) | frequencyLSB)) * 4;
dutyCounter++;
dutyCounter %= 8;
}
OutputVolume = isDacEnabled && dutyCycleTable[dutyCycle, dutyCounter] ? volume : 0;
}
private void Trigger()
{
isChannelEnabled = true;
if (lengthCounter == 0) lengthCounter = 64;
frequencyCounter = (2048 - ((frequencyMSB << 8) | frequencyLSB)) * 4;
volume = envelopeStartingVolume;
envelopeCounter = envelopePeriodReload;
isEnvelopeUpdateEnabled = true;
if (channelSupportsSweep)
{
sweepFreqShadow = (frequencyMSB << 8) | frequencyLSB;
sweepCounter = sweepPeriodReload;
isSweepEnabled = sweepPeriodReload != 0 || sweepShift != 0;
if (sweepShift != 0)
PerformSweepCalculations();
}
}
private int PerformSweepCalculations()
{
var newFrequency = sweepFreqShadow >> sweepShift;
if (sweepNegate) newFrequency = -newFrequency;
newFrequency += sweepFreqShadow;
if (newFrequency > 2047) isChannelEnabled = false;
return newFrequency;
}
public void WritePort(byte port, byte value)
{
switch (port)
{
case 0:
if (channelSupportsSweep)
{
sweepPeriodReload = (byte)((value >> 4) & 0b111);
sweepNegate = ((value >> 3) & 0b1) == 0b1;
sweepShift = (byte)((value >> 0) & 0b111);
}
break;
case 1:
dutyCycle = (byte)((value >> 6) & 0b11);
lengthLoad = (byte)((value >> 0) & 0b111111);
lengthCounter = 64 - lengthLoad;
break;
case 2:
envelopeStartingVolume = (byte)((value >> 4) & 0b1111);
envelopeAddMode = ((value >> 3) & 0b1) == 0b1;
envelopePeriodReload = (byte)((value >> 0) & 0b111);
isDacEnabled = ((value >> 3) & 0b11111) != 0;
break;
case 3:
frequencyLSB = value;
break;
case 4:
trigger = ((value >> 7) & 0b1) == 0b1;
lengthEnable = ((value >> 6) & 0b1) == 0b1;
frequencyMSB = (byte)((value >> 0) & 0b111);
if (trigger) Trigger();
break;
}
}
public byte ReadPort(byte port)
{
switch (port)
{
case 0:
if (channelSupportsSweep)
{
return (byte)(
0x80 |
(sweepPeriodReload << 4) |
(sweepNegate ? (1 << 3) : 0) |
(sweepShift << 0));
}
else
return 0xFF;
case 1:
return (byte)(
0x3F |
(dutyCycle << 6));
case 2:
return (byte)(
(envelopeStartingVolume << 4) |
(envelopeAddMode ? (1 << 3) : 0) |
(envelopePeriodReload << 0));
case 4:
return (byte)(
0xBF |
(lengthEnable ? (1 << 6) : 0));
default:
return 0xFF;
}
}
public void WriteWaveRam(byte offset, byte value)
{
throw new Exception("Channel type does have Wave RAM");
}
public byte ReadWaveRam(byte offset)
{
throw new Exception("Channel type does have Wave RAM");
}
}
}
}