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