GBA.Unity/Assets/emulator/NdsAudio.cs

525 lines
20 KiB
C#

using static OptimeGBA.Bits;
using static OptimeGBA.MemoryUtil;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Numerics;
namespace OptimeGBA
{
public class CustomSample
{
public short[] Data; // In PCM16
public uint LoopPoint; // In samples
public uint RepeatMode;
public CustomSample(short[] data, uint loopPoint, uint repeatMode)
{
Data = data;
LoopPoint = loopPoint;
RepeatMode = repeatMode;
}
}
public class AudioChannelNds
{
// SOUNDxCNT
public uint Volume;
public byte VolumeDiv;
public bool Hold;
public byte Pan;
public byte PulseDuty;
public byte RepeatMode;
public byte Format;
public bool Playing;
public uint SOUNDSAD;
public uint SOUNDTMR;
public uint SOUNDPNT;
public uint SOUNDLEN;
public uint SamplePos;
public uint Timer;
public uint Interval;
public int CurrentValue;
public int AdpcmIndex;
public int AdpcmLoopValue;
public int AdpcmLoopIndex;
public uint AdpcmLoopCurrentData;
public uint CurrentData;
public bool DebugEnable = true;
public uint DebugAdpcmSaved;
public uint DebugAdpcmRestored;
public long DebugStartTicks;
}
public class NdsAudio
{
Nds Nds;
public NdsAudio(Nds nds)
{
Nds = nds;
for (uint i = 0; i < 16; i++)
{
Channels[i] = new AudioChannelNds();
}
Sample(0);
}
public AudioChannelNds[] Channels = new AudioChannelNds[16];
public const int SampleRate = 32768;
public bool EnableBlipBufResampling = true;
public BlipBuf BlipBuf = new BlipBuf(32, true, 16);
public bool Record = false;
public WavWriter WavWriter = new WavWriter(SampleRate);
public WavWriter WavWriterSinc = new WavWriter(SampleRate);
public int SampleTimer = 0;
public const uint SampleBufferMax = 256;
public short[] SampleBuffer = new short[SampleBufferMax];
public uint SampleBufferPos = 0;
public static sbyte[] IndexTable = { -1, -1, -1, -1, 2, 4, 6, 8 };
public static short[] AdpcmTable = {
0x0007, 0x0008, 0x0009, 0x000A, 0x000B, 0x000C, 0x000D, 0x000E, 0x0010, 0x0011, 0x0013, 0x0015,
0x0017, 0x0019, 0x001C, 0x001F, 0x0022, 0x0025, 0x0029, 0x002D, 0x0032, 0x0037, 0x003C, 0x0042,
0x0049, 0x0050, 0x0058, 0x0061, 0x006B, 0x0076, 0x0082, 0x008F, 0x009D, 0x00AD, 0x00BE, 0x00D1,
0x00E6, 0x00FD, 0x0117, 0x0133, 0x0151, 0x0173, 0x0198, 0x01C1, 0x01EE, 0x0220, 0x0256, 0x0292,
0x02D4, 0x031C, 0x036C, 0x03C3, 0x0424, 0x048E, 0x0502, 0x0583, 0x0610, 0x06AB, 0x0756, 0x0812,
0x08E0, 0x09C3, 0x0ABD, 0x0BD0, 0x0CFF, 0x0E4C, 0x0FBA, 0x114C, 0x1307, 0x14EE, 0x1706, 0x1954,
0x1BDC, 0x1EA5, 0x21B6, 0x2515, 0x28CA, 0x2CDF, 0x315B, 0x364B, 0x3BB9, 0x41B2, 0x4844, 0x4F7E,
0x5771, 0x602F, 0x69CE, 0x7462, 0x7FFF
};
// SOUNDCNT
public byte MasterVolume;
public uint LeftOutputFrom;
public uint RightOutputFrom;
public bool Ch1ToMixer;
public bool Ch3ToMixer;
public bool MasterEnable;
// SOUNDBIAS
public ushort SOUNDBIAS;
// SNDCAPCNT
byte SNDCAP0CNT;
byte SNDCAP1CNT;
public byte ReadHwio8(uint addr)
{
byte val = 0;
switch (addr)
{
case 0x4000500: // SOUNDCNT B0
val |= (byte)(MasterVolume & 0x7FU);
break;
case 0x4000501: // SOUNDCNT B1
val |= (byte)((LeftOutputFrom & 0b11) << 0);
val |= (byte)((RightOutputFrom & 0b11) << 2);
if (Ch1ToMixer) val = BitSet(val, 4);
if (Ch3ToMixer) val = BitSet(val, 5);
if (MasterEnable) val = BitSet(val, 7);
break;
case 0x4000504:
return (byte)(SOUNDBIAS >> 0);
case 0x4000505:
return (byte)(SOUNDBIAS >> 8);
case 0x4000508:
return SNDCAP0CNT;
case 0x4000509:
return SNDCAP1CNT;
}
return val;
}
public void WriteHwio8(uint addr, byte val)
{
switch (addr)
{
case 0x4000500: // SOUNDCNT B0
MasterVolume = (byte)(val & 0x7FU);
break;
case 0x4000501: // SOUNDCNT B1
LeftOutputFrom = (byte)((val >> 0) & 0b11);
RightOutputFrom = (byte)((val >> 2) & 0b11);
Ch1ToMixer = BitTest(val, 4);
Ch3ToMixer = BitTest(val, 5);
MasterEnable = BitTest(val, 7);
break;
case 0x4000504:
SOUNDBIAS &= 0xFF00;
SOUNDBIAS |= (ushort)(val << 0);
break;
case 0x4000505:
SOUNDBIAS &= 0x00FF;
SOUNDBIAS |= (ushort)(val << 8);
break;
case 0x4000508:
SNDCAP0CNT = val;
break;
case 0x4000509:
SNDCAP1CNT = val;
break;
}
}
public byte ReadHwio8Channels(uint addr)
{
var c = Channels[(addr >> 4) & 0xF];
byte val = 0;
switch (addr & 0xF)
{
case 0x0:
val |= (byte)(c.Volume & 0x7F);
break;
case 0x1:
val |= c.VolumeDiv;
if (c.Hold) val = BitSet(val, 7);
break;
case 0x2:
val |= c.Pan;
break;
case 0x3:
val |= c.PulseDuty;
val |= (byte)(c.RepeatMode << 3);
val |= (byte)(c.Format << 5);
if (c.Playing) val = BitSet(val, 7);
break;
}
return val;
}
public void WriteHwio8Channels(uint addr, byte val)
{
var c = Channels[(addr >> 4) & 0xF];
switch (addr & 0xF)
{
case 0x0:
c.Volume = (byte)(val & 0x7F);
break;
case 0x1:
c.VolumeDiv = (byte)(val & 3);
c.Hold = BitTest(val, 7);
break;
case 0x2:
c.Pan = (byte)(val & 0x7F);
break;
case 0x3:
c.PulseDuty = (byte)(val & 7);
c.RepeatMode = (byte)((val >> 3) & 3);
c.Format = (byte)((val >> 5) & 3);
if (!c.Playing && BitTest(val, 7))
{
StartChannel(c);
}
c.Playing = BitTest(val, 7);
break;
case 0x4:
case 0x5:
case 0x6:
case 0x7:
Console.WriteLine(Util.Hex(Nds.Cpu7.GetCurrentInstrAddr(), 8));
// Console.WriteLine(Nds.MemoryControl.SharedRamControl);
c.SOUNDSAD = SetByteIn(c.SOUNDSAD, val, addr & 3) & 0x7FFFFFC;
if (c.Playing)
{
StartChannel(c);
}
break;
case 0x8:
case 0x9:
c.SOUNDTMR = SetByteIn(c.SOUNDTMR, val, addr & 1);
c.Interval = 2 * (0x10000 - c.SOUNDTMR);
break;
case 0xA:
case 0xB:
c.SOUNDPNT = SetByteIn(c.SOUNDPNT, val, addr & 1);
break;
case 0xC:
case 0xD:
case 0xE:
case 0xF:
c.SOUNDLEN = SetByteIn(c.SOUNDLEN, val, addr & 3) & 0x3FFFFF;
break;
}
}
public void StartChannel(AudioChannelNds c)
{
c.SamplePos = 0;
c.Timer = 0;
c.CurrentValue = 0;
c.DebugStartTicks = Nds.Scheduler.CurrentTicks;
}
public void Sample(long cyclesLate)
{
long left = 0;
long right = 0;
for (int i = 0; i < 16; i++)
{
var c = Channels[i];
if (c.Playing)
{
c.Timer += 1024;
while (c.Timer >= c.Interval && c.Interval != 0)
{
c.Timer -= c.Interval;
// Advance sample
switch (c.Format)
{
case 0: // PCM8
if (c.SamplePos >= (c.SOUNDPNT + c.SOUNDLEN) * 4)
{
switch (c.RepeatMode)
{
case 1: // Infinite
c.SamplePos = c.SOUNDPNT * 4;
break;
case 2: // One-shot
c.Playing = false;
if (!c.Hold)
{
c.CurrentValue = 0;
}
break;
}
}
if ((c.SamplePos & 3) == 0)
{
c.CurrentData = Nds.Mem7.Read32(c.SOUNDSAD + c.SamplePos);
}
c.CurrentValue = (short)((byte)c.CurrentData << 8);
c.CurrentData >>= 8;
c.SamplePos++;
break;
case 1: // PCM16
if (c.SamplePos >= (c.SOUNDPNT + c.SOUNDLEN) * 2)
{
switch (c.RepeatMode)
{
case 1: // Infinite
c.SamplePos = c.SOUNDPNT * 2;
break;
case 2: // One-shot
c.Playing = false;
if (!c.Hold)
{
c.CurrentValue = 0;
}
break;
}
}
if ((c.SamplePos & 1) == 0)
{
c.CurrentData = Nds.Mem7.Read32(c.SOUNDSAD + c.SamplePos * 2);
}
c.CurrentValue = (short)c.CurrentData;
c.CurrentData >>= 16;
c.SamplePos++;
break;
case 2: // IMA-ADPCM
if ((c.SamplePos & 7) == 0)
{
c.CurrentData = Nds.Mem7.Read32(c.SOUNDSAD + c.SamplePos / 2);
// ADPCM header
if (c.SamplePos == 0)
{
c.CurrentValue = (short)c.CurrentData;
// Console.WriteLine("header set " + x++);
// Console.WriteLine("interval: " + Util.Hex(c.Interval, 8));
c.AdpcmIndex = Math.Clamp((int)(c.CurrentData >> 16), 0, 88);
}
// Console.WriteLine("addr: " + Util.Hex(c.Source, 8));
}
if (c.SamplePos > 7)
{
// End of sound, loop or stop
if (c.SamplePos >= (c.SOUNDPNT + c.SOUNDLEN) * 8)
{
switch (c.RepeatMode)
{
case 1: // Infinite
c.SamplePos = c.SOUNDPNT * 8;
c.CurrentValue = c.AdpcmLoopValue;
c.AdpcmIndex = c.AdpcmLoopIndex;
c.CurrentData = c.AdpcmLoopCurrentData;
// Console.WriteLine($"Ch{i}: Loaded at " + c.SampleNum);
c.DebugAdpcmRestored = c.SamplePos;
break;
case 2: // One-shot
c.Playing = false;
if (!c.Hold)
{
c.CurrentValue = 0;
}
break;
}
}
else
{
byte data = (byte)(c.CurrentData & 0xF);
short tableVal = AdpcmTable[c.AdpcmIndex];
int diff = tableVal / 8;
if ((data & 1) != 0) diff += tableVal / 4;
if ((data & 2) != 0) diff += tableVal / 2;
if ((data & 4) != 0) diff += tableVal / 1;
if ((data & 8) == 8)
{
c.CurrentValue = Math.Max((int)c.CurrentValue - diff, -0x7FFF);
}
else
{
c.CurrentValue = Math.Min((int)c.CurrentValue + diff, 0x7FFF);
}
c.AdpcmIndex = Math.Clamp(c.AdpcmIndex + IndexTable[data & 7], 0, 88);
c.CurrentData >>= 4;
// Save value and ADPCM table index for loop
if (c.SamplePos == c.SOUNDPNT * 8)
{
c.AdpcmLoopValue = c.CurrentValue;
c.AdpcmLoopIndex = c.AdpcmIndex;
c.AdpcmLoopCurrentData = c.CurrentData;
c.DebugAdpcmSaved = c.SamplePos;
// Console.WriteLine($"Ch{i}: Saved at " + c.SampleNum);
}
}
}
c.SamplePos++;
break;
case 3: // Pulse / Noise
if (((c.SamplePos ^ 7) & 7) <= c.PulseDuty)
{
c.CurrentValue = -0x7FFF;
}
else
{
c.CurrentValue = 0x7FFF;
}
c.SamplePos++;
break;
}
if (EnableBlipBufResampling)
{
long timeTicks = Nds.Scheduler.CurrentTicks - cyclesLate + 1024 - (c.Timer % 1024);
double timeSec = (double)timeTicks / 33513982D;
double timeSample = (double)timeTicks / (33513982D / (double)SampleRate);
if (c.DebugEnable)
{
uint effectiveVol = c.Volume;
if (effectiveVol == 127) effectiveVol++;
long leftCh = ((((long)c.CurrentValue * (16 >> c.VolumeDiv)) * effectiveVol) * (127 - c.Pan)) >> 10;
long rightCh = ((((long)c.CurrentValue * (16 >> c.VolumeDiv)) * effectiveVol) * c.Pan) >> 10;
BlipBuf.SetValue(i, timeSample, leftCh, rightCh);
}
else
{
BlipBuf.SetValue(i, timeSample, 0, 0);
}
}
}
if (c.DebugEnable)
{
uint effectiveVol = c.Volume;
if (effectiveVol == 127) effectiveVol++;
left += ((((long)c.CurrentValue * (16 >> c.VolumeDiv)) * effectiveVol) * (127 - c.Pan)) >> 10;
right += ((((long)c.CurrentValue * (16 >> c.VolumeDiv)) * effectiveVol) * c.Pan) >> 10;
}
}
}
// Decimate samples to 32768 hz
// Since 33513982 hz / 1024 ≅ 32728.498 hz
SampleTimer += SampleRate * 1024;
while (SampleTimer >= 33513982)
{
SampleTimer -= 33513982;
BlipBuf.ReadOutSample();
// 28 bits now, after mixing all channels
// add master volume to get 35 bits
// add
// strip 19 to get 16 bits for our short output
uint effectiveMasterVol = MasterVolume;
if (effectiveMasterVol == 127) effectiveMasterVol++;
short leftFinalSinc = (short)(((long)BlipBuf.CurrentValL * effectiveMasterVol) >> 16);
short rightFinalSinc = (short)(((long)BlipBuf.CurrentValR * effectiveMasterVol) >> 16);
short leftFinal = (short)((left * effectiveMasterVol) >> 16);
short rightFinal = (short)((right * effectiveMasterVol) >> 16);
if (EnableBlipBufResampling)
{
SampleBuffer[SampleBufferPos++] = leftFinalSinc;
SampleBuffer[SampleBufferPos++] = rightFinalSinc;
}
else
{
SampleBuffer[SampleBufferPos++] = leftFinal;
SampleBuffer[SampleBufferPos++] = rightFinal;
}
if (Record)
{
WavWriterSinc.AddSample(leftFinalSinc, rightFinalSinc);
WavWriter.AddSample(leftFinal, rightFinal);
}
if (SampleBufferPos >= SampleBufferMax)
{
SampleBufferPos = 0;
Nds.Provider.AudioCallback(SampleBuffer);
}
}
Nds.Scheduler.AddEventRelative(SchedulerId.ApuSample, 1024 - cyclesLate, Sample);
}
}
}