Skip to content

Commit

Permalink
ws: APU emulation improvements (#1425)
Browse files Browse the repository at this point in the history
* Significantly improved Hyper Voice implementation accuracy (verified,
at least for Sound DMA writes, with a test ROM)
* Implemented emulation of unused/undocumented debug ports (0x64-0x67,
0x69, 0x95-0x9B)
* Improved cycle accuracy of APU channel handling
* Added slightly faster "inaccurate" APU emulation mode (for most games,
output should be essentially identical; debug ports will not match up,
though)
  • Loading branch information
asiekierka committed Mar 24, 2024
1 parent f0fd88f commit 0c8eaf0
Show file tree
Hide file tree
Showing 10 changed files with 244 additions and 125 deletions.
111 changes: 71 additions & 40 deletions ares/ws/apu/apu.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -37,64 +37,95 @@ auto APU::unload() -> void {
}

auto APU::main() -> void {
// further verification could always be useful
u32 steps = accurate ? 1 : 128;
for(u32 s = 0; s < steps; s++) {
// TODO: is the period value (run()) updated before or after the outputs (runOutput())?
channel1.run();
channel2.run();
channel3.run();
if(++state.sweepClock == 0) channel3.sweep(); // TODO: which cycle is this, or is it separate?
channel4.run();

// TODO: are voice/noise modes handled on different cycles than tone modes?
if(accurate) {
// TODO: Are the channels ticked before or after the memory fetch?
channel1.tick();
channel2.tick();
channel3.tick();
if(++state.sweepClock == 0) channel3.sweep(); // TODO: Is there any relationship between this clock and I/O ports?
channel4.tick();

switch(state.apuClock++) {
case 0: if(channel1.io.enable) channel1.runOutput(); break;
case 1: if(channel2.io.enable || channel2.io.voice) channel2.runOutput(); break;
case 2: if(channel3.io.enable) channel3.runOutput(); break;
case 3: if(channel4.io.enable) channel4.runOutput(); break;
case 4: if(channel5.io.enable) channel5.runOutput(); break; // TODO: which cycle is this?
case 5: dma.run(); break; // TODO: which cycle is this?
case 6: dacRun(); break; // TODO: which cycle is this?
case 0: apu.output(); break;
case 122:
// TODO: Sound DMA should be blocking the CPU instead.
dma.run();
apu.sequencerClear();
break;
case 123: if(channel5.io.enable) channel5.runOutput(); break; // TODO: Which cycle is this performed on?
case 124: if(channel1.io.enable) channel1.output(); break;
case 125: if(channel2.io.enable || channel2.io.voice) channel2.output(); break;
case 126: if(channel3.io.enable) channel3.output(); break;
case 127: if(channel4.io.enable) channel4.output(); break;
}

step(1);
} else {
dma.run();
apu.sequencerClear();

for(u32 s = 0; s < 128; s++) {
channel1.tick();
channel2.tick();
channel3.tick();
if(++state.sweepClock == 0) channel3.sweep();
channel4.tick();
}

channel5.runOutput();
if(channel1.io.enable) channel1.output();
if(channel2.io.enable || channel2.io.voice) channel2.output();
if(channel3.io.enable) channel3.output();
if(channel4.io.enable) channel4.output();

apu.output();

step(128);
}
}

auto APU::sequencerClear() -> void {
io.output = {};
if(io.seqDbgOutputForce55) {
io.output.left = 0x55;
io.output.right = 0x55;
}
step(steps);
}

auto APU::sequencerHeld() -> bool {
return io.seqDbgHold || io.seqDbgOutputForce55;
}

auto APU::sample(u32 channel, n5 index) -> n4 {
if(io.seqDbgChForce4) return 4;
if(io.seqDbgChForce2) return 2;

n8 data = iram.read((io.waveBase << 6) + (--channel << 4) + (index >> 1));
if(index.bit(0) == 0) return data.bit(0,3);
if(index.bit(0) == 1) return data.bit(4,7);
unreachable;
}

auto APU::dacRun() -> void {
auto APU::output() -> void {
bool outputEnable = io.headphonesConnected ? io.headphonesEnable : io.speakerEnable;

if(!outputEnable) {
stream->frame(0, 0);
return;
}

s32 left = 0;
if(channel1.io.enable) left += channel1.output.left;
if(channel2.io.enable || channel2.io.voice) left += channel2.output.left;
if(channel3.io.enable) left += channel3.output.left;
if(channel4.io.enable) left += channel4.output.left;
if(channel5.io.enable) left += channel5.output.left * io.headphonesConnected;

s32 right = 0;
if(channel1.io.enable) right += channel1.output.right;
if(channel2.io.enable || channel2.io.voice) right += channel2.output.right;
if(channel3.io.enable) right += channel3.output.right;
if(channel4.io.enable) right += channel4.output.right;
if(channel5.io.enable) right += channel5.output.right * io.headphonesConnected;

if(!io.headphonesConnected) {
left = right = sclamp<16>((((left + right) >> io.speakerShift) & 0xFF) << 7);
} else {
s32 left = io.output.left;
s32 right = io.output.right;

if(io.headphonesConnected) {
left = sclip<16>(left << 5);
right = sclip<16>(right << 5);
if(channel5.io.enable) {
left += channel5.output.left;
right += channel5.output.right;
}
} else {
left = right = sclamp<16>((((left + right) >> io.speakerShift) & 0xFF) << 7);
}

//ASWAN has three volume steps (0%, 50%, 100%); SPHINX and SPHINX2 have four (0%, 33%, 66%, 100%)
Expand All @@ -113,8 +144,8 @@ auto APU::power() -> void {
bus.map(this, 0x004a, 0x004c);
bus.map(this, 0x004e, 0x0050);
bus.map(this, 0x0052);
bus.map(this, 0x006a, 0x006b);
bus.map(this, 0x0080, 0x0095);
bus.map(this, 0x0064, 0x006b);
bus.map(this, 0x0080, 0x009b);
bus.map(this, 0x009e);

dma.power();
Expand All @@ -128,7 +159,7 @@ auto APU::power() -> void {
io.headphonesConnected = system.headphones->value();
io.masterVolume = SoC::ASWAN() ? 2 : 3;
state = {};

state.apuClock = 0;
state.sweepClock = 0;
}
Expand Down
69 changes: 35 additions & 34 deletions ares/ws/apu/apu.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@ struct APU : Thread, IO {

auto main() -> void;
auto sample(u32 channel, n5 index) -> n4;
auto dacRun() -> void;
auto output() -> void;
auto step(u32 clocks) -> void;
auto power() -> void;
auto sequencerClear() -> void;
auto sequencerHeld() -> bool;

//io.cpp
auto readIO(n16 address) -> n8;
Expand Down Expand Up @@ -64,8 +66,8 @@ struct APU : Thread, IO {

struct Channel1 {
//channel1.cpp
auto run() -> void;
auto runOutput() -> void;
auto tick() -> void;
auto output() -> void;
auto power() -> void;

//serialization.cpp
Expand All @@ -82,17 +84,12 @@ struct APU : Thread, IO {
n11 period;
n5 sampleOffset;
} state;

struct Output {
n8 left;
n8 right;
} output;
} channel1;

struct Channel2 {
//channel2.cpp
auto run() -> void;
auto runOutput() -> void;
auto tick() -> void;
auto output() -> void;
auto power() -> void;

//serialization.cpp
Expand All @@ -114,18 +111,13 @@ struct APU : Thread, IO {
n11 period;
n5 sampleOffset;
} state;

struct Output {
n8 left;
n8 right;
} output;
} channel2;

struct Channel3 {
//channel3.cpp
auto sweep() -> void;
auto run() -> void;
auto runOutput() -> void;
auto tick() -> void;
auto output() -> void;
auto power() -> void;

//serialization.cpp
Expand All @@ -146,18 +138,13 @@ struct APU : Thread, IO {
n5 sampleOffset;
i32 sweepCounter;
} state;

struct Output {
n8 left;
n8 right;
} output;
} channel3;

struct Channel4 {
//channel4.cpp
auto noiseSample() -> n4;
auto run() -> void;
auto runOutput() -> void;
auto tick() -> void;
auto output() -> void;
auto power() -> void;

//serialization.cpp
Expand All @@ -180,15 +167,14 @@ struct APU : Thread, IO {
n1 noiseOutput;
n15 noiseLFSR;
} state;

struct Output {
n8 left;
n8 right;
} output;
} channel4;

struct Channel5 {
//channel5.cpp
auto dmaWrite(n8 sample) -> void;
auto manualWrite(n8 sample) -> void;
auto write(n8 sample) -> void;
auto scale(i8 sample) -> i16;
auto runOutput() -> void;
auto power() -> void;

Expand All @@ -201,18 +187,21 @@ struct APU : Thread, IO {
n3 speed;
n1 enable;
n4 unknown;
n1 leftEnable;
n1 rightEnable;
n2 mode;
} io;

struct State {
n32 clock;
i8 data;
n1 channel;
n8 left;
n8 right;
n1 leftChanged;
n1 rightChanged;
} state;

struct Output {
i11 left;
i11 right;
i16 left;
i16 right;
} output;
} channel5;

Expand All @@ -223,6 +212,18 @@ struct APU : Thread, IO {
n1 headphonesEnable;
n1 headphonesConnected;
n2 masterVolume;

n1 seqDbgHold;
n1 seqDbgOutputForce55;
n1 seqDbgChForce4;
n1 seqDbgChForce2;
n4 seqDbgUnknown;

// This output covers Channels 1-4 (excluding Hyper Voice)
struct Output {
n10 left;
n10 right;
} output;
} io;

struct State {
Expand Down
10 changes: 5 additions & 5 deletions ares/ws/apu/channel1.cpp
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
auto APU::Channel1::run() -> void {
auto APU::Channel1::tick() -> void {
if(--state.period == io.pitch) {
state.period = 0;
state.sampleOffset++;
}
}

auto APU::Channel1::runOutput() -> void {
auto APU::Channel1::output() -> void {
if(apu.sequencerHeld()) return;
auto sample = apu.sample(1, state.sampleOffset);
output.left = sample * io.volumeLeft;
output.right = sample * io.volumeRight;
apu.io.output.left += sample * io.volumeLeft;
apu.io.output.right += sample * io.volumeRight;
}

auto APU::Channel1::power() -> void {
io = {};
state = {};
output = {};
}
16 changes: 8 additions & 8 deletions ares/ws/apu/channel2.cpp
Original file line number Diff line number Diff line change
@@ -1,26 +1,26 @@
auto APU::Channel2::run() -> void {
if (!io.voice) {
auto APU::Channel2::tick() -> void {
if(!io.voice) {
if(--state.period == io.pitch) {
state.period = 0;
state.sampleOffset++;
}
}
}

auto APU::Channel2::runOutput() -> void {
auto APU::Channel2::output() -> void {
if(apu.sequencerHeld()) return;
if(io.voice) {
n8 volume = io.volumeLeft << 4 | io.volumeRight << 0;
output.left = io.voiceEnableLeftFull ? volume : (n8)(io.voiceEnableLeftHalf ? (volume >> 1) : 0);
output.right = io.voiceEnableRightFull ? volume : (n8)(io.voiceEnableRightHalf ? (volume >> 1) : 0);
apu.io.output.left += io.voiceEnableLeftFull ? volume : (n8)(io.voiceEnableLeftHalf ? (volume >> 1) : 0);
apu.io.output.right += io.voiceEnableRightFull ? volume : (n8)(io.voiceEnableRightHalf ? (volume >> 1) : 0);
} else {
auto sample = apu.sample(2, state.sampleOffset);
output.left = sample * io.volumeLeft;
output.right = sample * io.volumeRight;
apu.io.output.left += sample * io.volumeLeft;
apu.io.output.right += sample * io.volumeRight;
}
}

auto APU::Channel2::power() -> void {
io = {};
state = {};
output = {};
}
10 changes: 5 additions & 5 deletions ares/ws/apu/channel3.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,21 @@ auto APU::Channel3::sweep() -> void {
}
}

auto APU::Channel3::run() -> void {
auto APU::Channel3::tick() -> void {
if(--state.period == io.pitch) {
state.period = 0;
state.sampleOffset++;
}
}

auto APU::Channel3::runOutput() -> void {
auto APU::Channel3::output() -> void {
if(apu.sequencerHeld()) return;
auto sample = apu.sample(3, state.sampleOffset);
output.left = sample * io.volumeLeft;
output.right = sample * io.volumeRight;
apu.io.output.left += sample * io.volumeLeft;
apu.io.output.right += sample * io.volumeRight;
}

auto APU::Channel3::power() -> void {
io = {};
state = {};
output = {};
}
Loading

0 comments on commit 0c8eaf0

Please sign in to comment.