525 lines
20 KiB
C#
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);
|
|
}
|
|
}
|
|
} |