Compare commits

..

3 Commits

14 changed files with 270 additions and 154 deletions

View File

@ -15,12 +15,10 @@ const SoundFifo = std.fifo.LinearFifo(u8, .{ .Static = 0x20 });
const getHalf = util.getHalf; const getHalf = util.getHalf;
const setHalf = util.setHalf; const setHalf = util.setHalf;
const intToBytes = util.intToBytes; const intToBytes = util.intToBytes;
const RingBuffer = util.RingBuffer;
const log = std.log.scoped(.APU); const log = std.log.scoped(.APU);
pub const host_rate = @import("../platform.zig").sample_rate;
pub const host_format = @import("../platform.zig").sample_format;
pub fn read(comptime T: type, apu: *const Apu, addr: u32) ?T { pub fn read(comptime T: type, apu: *const Apu, addr: u32) ?T {
const byte_addr = @truncate(u8, addr); const byte_addr = @truncate(u8, addr);
@ -108,8 +106,14 @@ pub fn read(comptime T: type, apu: *const Apu, addr: u32) ?T {
pub fn write(comptime T: type, apu: *Apu, addr: u32, value: T) void { pub fn write(comptime T: type, apu: *Apu, addr: u32, value: T) void {
const byte_addr = @truncate(u8, addr); const byte_addr = @truncate(u8, addr);
if (byte_addr <= 0x81 and !apu.cnt.apu_enable.read()) return;
switch (T) { switch (T) {
u32 => switch (byte_addr) { u32 => {
// 0x80 and 0x81 handled in setSoundCnt
if (byte_addr < 0x80 and !apu.cnt.apu_enable.read()) return;
switch (byte_addr) {
0x60 => apu.ch1.setSound1Cnt(value), 0x60 => apu.ch1.setSound1Cnt(value),
0x64 => apu.ch1.setSound1CntX(&apu.fs, @truncate(u16, value)), 0x64 => apu.ch1.setSound1CntX(&apu.fs, @truncate(u16, value)),
@ -131,8 +135,12 @@ pub fn write(comptime T: type, apu: *Apu, addr: u32, value: T) void {
0xA0 => apu.chA.push(value), // FIFO_A 0xA0 => apu.chA.push(value), // FIFO_A
0xA4 => apu.chB.push(value), // FIFO_B 0xA4 => apu.chB.push(value), // FIFO_B
else => util.io.write.undef(log, "Tried to write 0x{X:0>8}{} to 0x{X:0>8}", .{ value, T, addr }), else => util.io.write.undef(log, "Tried to write 0x{X:0>8}{} to 0x{X:0>8}", .{ value, T, addr }),
}
}, },
u16 => switch (byte_addr) { u16 => {
if (byte_addr <= 0x81 and !apu.cnt.apu_enable.read()) return;
switch (byte_addr) {
0x60 => apu.ch1.setSound1CntL(@truncate(u8, value)), // SOUND1CNT_L 0x60 => apu.ch1.setSound1CntL(@truncate(u8, value)), // SOUND1CNT_L
0x62 => apu.ch1.setSound1CntH(value), 0x62 => apu.ch1.setSound1CntH(value),
0x64 => apu.ch1.setSound1CntX(&apu.fs, value), 0x64 => apu.ch1.setSound1CntX(&apu.fs, value),
@ -164,8 +172,12 @@ pub fn write(comptime T: type, apu: *Apu, addr: u32, value: T) void {
0xA0, 0xA2 => log.err("Tried to write 0x{X:0>4}{} to FIFO_A", .{ value, T }), 0xA0, 0xA2 => log.err("Tried to write 0x{X:0>4}{} to FIFO_A", .{ value, T }),
0xA4, 0xA6 => log.err("Tried to write 0x{X:0>4}{} to FIFO_B", .{ value, T }), 0xA4, 0xA6 => log.err("Tried to write 0x{X:0>4}{} to FIFO_B", .{ value, T }),
else => util.io.write.undef(log, "Tried to write 0x{X:0>4}{} to 0x{X:0>8}", .{ value, T, addr }), else => util.io.write.undef(log, "Tried to write 0x{X:0>4}{} to 0x{X:0>8}", .{ value, T, addr }),
}
}, },
u8 => switch (byte_addr) { u8 => {
if (byte_addr <= 0x81 and !apu.cnt.apu_enable.read()) return;
switch (byte_addr) {
0x60 => apu.ch1.setSound1CntL(value), 0x60 => apu.ch1.setSound1CntL(value),
0x61 => {}, 0x61 => {},
0x62 => apu.ch1.setNr11(value), 0x62 => apu.ch1.setNr11(value),
@ -208,6 +220,7 @@ pub fn write(comptime T: type, apu: *Apu, addr: u32, value: T) void {
0xA0...0xA3 => log.err("Tried to write 0x{X:0>2}{} to FIFO_A", .{ value, T }), 0xA0...0xA3 => log.err("Tried to write 0x{X:0>2}{} to FIFO_A", .{ value, T }),
0xA4...0xA7 => log.err("Tried to write 0x{X:0>2}{} to FIFO_B", .{ value, T }), 0xA4...0xA7 => log.err("Tried to write 0x{X:0>2}{} to FIFO_B", .{ value, T }),
else => util.io.write.undef(log, "Tried to write 0x{X:0>2}{} to 0x{X:0>8}", .{ value, T, addr }), else => util.io.write.undef(log, "Tried to write 0x{X:0>2}{} to 0x{X:0>8}", .{ value, T, addr }),
}
}, },
else => @compileError("APU: Unsupported write width"), else => @compileError("APU: Unsupported write width"),
} }
@ -231,17 +244,20 @@ pub const Apu = struct {
sampling_cycle: u2, sampling_cycle: u2,
stream: *SDL.SDL_AudioStream, sample_queue: RingBuffer(u16),
sched: *Scheduler, sched: *Scheduler,
fs: FrameSequencer, fs: FrameSequencer,
capacitor: f32, capacitor: f32,
is_buffer_full: bool,
pub const Tick = enum { Length, Envelope, Sweep }; pub const Tick = enum { Length, Envelope, Sweep };
pub fn init(sched: *Scheduler) Self { pub fn init(sched: *Scheduler) Self {
const NUM_CHANNELS: usize = 2;
const allocator = std.heap.c_allocator;
const sample_buf = allocator.alloc(u16, 0x800 * NUM_CHANNELS) catch @panic("failed to allocate sample buffer");
const apu: Self = .{ const apu: Self = .{
.ch1 = ToneSweep.init(sched), .ch1 = ToneSweep.init(sched),
.ch2 = Tone.init(sched), .ch2 = Tone.init(sched),
@ -256,12 +272,11 @@ pub const Apu = struct {
.bias = .{ .raw = 0x0200 }, .bias = .{ .raw = 0x0200 },
.sampling_cycle = 0b00, .sampling_cycle = 0b00,
.stream = SDL.SDL_NewAudioStream(SDL.AUDIO_U16, 2, 1 << 15, host_format, 2, host_rate).?, .sample_queue = RingBuffer(u16).init(sample_buf),
.sched = sched, .sched = sched,
.capacitor = 0, .capacitor = 0,
.fs = FrameSequencer.init(), .fs = FrameSequencer.init(),
.is_buffer_full = false,
}; };
sched.push(.SampleAudio, apu.interval()); sched.push(.SampleAudio, apu.interval());
@ -275,15 +290,20 @@ pub const Apu = struct {
} }
fn reset(self: *Self) void { fn reset(self: *Self) void {
// All PSG Registers between 0x0400_0060..0x0400_0081 are zeroed
// 0x0400_0082 and 0x0400_0088 retain their values
self.ch1.reset(); self.ch1.reset();
self.ch2.reset(); self.ch2.reset();
self.ch3.reset(); self.ch3.reset();
self.ch4.reset(); self.ch4.reset();
// GBATEK says 4000060h..4000081h I take this to mean inclusive
self.psg_cnt.raw = 0x0000;
} }
/// SOUNDCNT /// SOUNDCNT
fn setSoundCnt(self: *Self, value: u32) void { fn setSoundCnt(self: *Self, value: u32) void {
self.setSoundCntL(@truncate(u16, value)); if (self.cnt.apu_enable.read()) self.setSoundCntL(@truncate(u16, value));
self.setSoundCntH(@truncate(u16, value >> 16)); self.setSoundCntH(@truncate(u16, value >> 16));
} }
@ -322,11 +342,14 @@ pub const Apu = struct {
self.fs.step = 0; // Reset Frame Sequencer self.fs.step = 0; // Reset Frame Sequencer
// Reset Square Wave Offsets // Reset Square Wave Offsets
self.ch1.square.pos = 0; self.ch1.square.reset();
self.ch2.square.pos = 0; self.ch2.square.reset();
// Reset Wave Device Offsets // Reset Wave
self.ch3.wave_dev.offset = 0; self.ch3.wave_dev.reset();
// Rest Noise
self.ch4.lfsr.reset();
} else { } else {
self.reset(); self.reset();
} }
@ -347,11 +370,6 @@ pub const Apu = struct {
pub fn sampleAudio(self: *Self, late: u64) void { pub fn sampleAudio(self: *Self, late: u64) void {
self.sched.push(.SampleAudio, self.interval() -| late); self.sched.push(.SampleAudio, self.interval() -| late);
// Whether the APU is busy or not is determined by the main loop in emu.zig
// This should only ever be true (because this side of the emu is single threaded)
// When audio sync is disaabled
if (self.is_buffer_full) return;
var left: i16 = 0; var left: i16 = 0;
var right: i16 = 0; var right: i16 = 0;
@ -395,8 +413,8 @@ pub const Apu = struct {
right += if (self.dma_cnt.chB_right.read()) chB_sample else 0; right += if (self.dma_cnt.chB_right.read()) chB_sample else 0;
// Add SOUNDBIAS // Add SOUNDBIAS
// FIXME: Is SOUNDBIAS 9-bit or 10-bit? // FIXME: SOUNDBIAS is 10-bit but The waveform is centered around 0 if I treat it as 11-bit
const bias = @as(i16, self.bias.level.read()) << 1; const bias = @as(i16, self.bias.level.read()) << 2;
left += bias; left += bias;
right += bias; right += bias;
@ -407,23 +425,7 @@ pub const Apu = struct {
const ext_left = (clamped_left << 5) | (clamped_left >> 6); const ext_left = (clamped_left << 5) | (clamped_left >> 6);
const ext_right = (clamped_right << 5) | (clamped_right >> 6); const ext_right = (clamped_right << 5) | (clamped_right >> 6);
if (self.sampling_cycle != self.bias.sampling_cycle.read()) self.replaceSDLResampler(); self.sample_queue.push(ext_left, ext_right) catch {};
_ = SDL.SDL_AudioStreamPut(self.stream, &[2]u16{ ext_left, ext_right }, 2 * @sizeOf(u16));
}
fn replaceSDLResampler(self: *Self) void {
@setCold(true);
const sample_rate = Self.sampleRate(self.bias.sampling_cycle.read());
log.info("Sample Rate changed from {}Hz to {}Hz", .{ Self.sampleRate(self.sampling_cycle), sample_rate });
// Sampling Cycle (Sample Rate) changed, Craete a new SDL Audio Resampler
// FIXME: Replace SDL's Audio Resampler with either a custom or more reliable one
const old_stream = self.stream;
defer SDL.SDL_FreeAudioStream(old_stream);
self.sampling_cycle = self.bias.sampling_cycle.read();
self.stream = SDL.SDL_NewAudioStream(SDL.AUDIO_U16, 2, @intCast(c_int, sample_rate), host_format, 2, host_rate).?;
} }
fn interval(self: *const Self) u64 { fn interval(self: *const Self) u64 {

View File

@ -49,10 +49,13 @@ pub fn init(sched: *Scheduler) Self {
} }
pub fn reset(self: *Self) void { pub fn reset(self: *Self) void {
self.len = 0; self.len = 0; // NR41
self.envelope.raw = 0; self.envelope.raw = 0; // NR42
self.poly.raw = 0; self.poly.raw = 0; // NR43
self.cnt.raw = 0; self.cnt.raw = 0; // NR44
self.len_dev.reset();
self.env_dev.reset();
self.sample = 0; self.sample = 0;
self.enabled = false; self.enabled = false;

View File

@ -43,9 +43,12 @@ pub fn init(sched: *Scheduler) Self {
} }
pub fn reset(self: *Self) void { pub fn reset(self: *Self) void {
self.duty.raw = 0; self.duty.raw = 0; // NR21
self.envelope.raw = 0; self.envelope.raw = 0; // NR22
self.freq.raw = 0; self.freq.raw = 0; // NR32, NR24
self.len_dev.reset();
self.env_dev.reset();
self.sample = 0; self.sample = 0;
self.enabled = false; self.enabled = false;

View File

@ -50,12 +50,14 @@ pub fn init(sched: *Scheduler) Self {
} }
pub fn reset(self: *Self) void { pub fn reset(self: *Self) void {
self.sweep.raw = 0; self.sweep.raw = 0; // NR10
self.sweep_dev.calc_performed = false; self.duty.raw = 0; // NR11
self.envelope.raw = 0; // NR12
self.freq.raw = 0; // NR13, NR14
self.duty.raw = 0; self.len_dev.reset();
self.envelope.raw = 0; self.sweep_dev.reset();
self.freq.raw = 0; self.env_dev.reset();
self.sample = 0; self.sample = 0;
self.enabled = false; self.enabled = false;

View File

@ -42,10 +42,13 @@ pub fn init(sched: *Scheduler) Self {
} }
pub fn reset(self: *Self) void { pub fn reset(self: *Self) void {
self.select.raw = 0; self.select.raw = 0; // NR30
self.length = 0; self.length = 0; // NR31
self.vol.raw = 0; self.vol.raw = 0; // NR32
self.freq.raw = 0; self.freq.raw = 0; // NR33, NR34
self.len_dev.reset();
self.wave_dev.reset();
self.sample = 0; self.sample = 0;
self.enabled = false; self.enabled = false;

View File

@ -11,6 +11,11 @@ pub fn create() Self {
return .{ .timer = 0, .vol = 0 }; return .{ .timer = 0, .vol = 0 };
} }
pub fn reset(self: *Self) void {
self.timer = 0;
self.vol = 0;
}
pub fn tick(self: *Self, nrx2: io.Envelope) void { pub fn tick(self: *Self, nrx2: io.Envelope) void {
if (nrx2.period.read() != 0) { if (nrx2.period.read() != 0) {
if (self.timer != 0) self.timer -= 1; if (self.timer != 0) self.timer -= 1;

View File

@ -6,6 +6,10 @@ pub fn create() Self {
return .{ .timer = 0 }; return .{ .timer = 0 };
} }
pub fn reset(self: *Self) void {
self.timer = 0;
}
pub fn tick(self: *Self, enabled: bool, ch_enable: *bool) void { pub fn tick(self: *Self, enabled: bool, ch_enable: *bool) void {
if (enabled) { if (enabled) {
if (self.timer == 0) return; if (self.timer == 0) return;

View File

@ -18,6 +18,13 @@ pub fn create() Self {
}; };
} }
pub fn reset(self: *Self) void {
self.timer = 0;
self.enabled = false;
self.shadow = 0;
self.calc_performed = false;
}
pub fn tick(self: *Self, ch1: *ToneSweep) void { pub fn tick(self: *Self, ch1: *ToneSweep) void {
if (self.timer != 0) self.timer -= 1; if (self.timer != 0) self.timer -= 1;

View File

@ -19,6 +19,11 @@ pub fn create(sched: *Scheduler) Self {
}; };
} }
pub fn reset(self: *Self) void {
self.shift = 0;
self.timer = 0;
}
pub fn sample(self: *const Self) i8 { pub fn sample(self: *const Self) i8 {
return if ((~self.shift & 1) == 1) 1 else -1; return if ((~self.shift & 1) == 1) 1 else -1;
} }

View File

@ -20,6 +20,11 @@ pub fn init(sched: *Scheduler) Self {
}; };
} }
pub fn reset(self: *Self) void {
self.timer = 0;
self.pos = 0;
}
/// Scheduler Event Handler for Square Synth Timer Expire /// Scheduler Event Handler for Square Synth Timer Expire
pub fn onSquareTimerExpire(self: *Self, comptime T: type, nrx34: io.Frequency, late: u64) void { pub fn onSquareTimerExpire(self: *Self, comptime T: type, nrx34: io.Frequency, late: u64) void {
comptime std.debug.assert(T == ToneSweep or T == Tone); comptime std.debug.assert(T == ToneSweep or T == Tone);

View File

@ -38,6 +38,13 @@ pub fn init(sched: *Scheduler) Self {
}; };
} }
pub fn reset(self: *Self) void {
self.timer = 0;
self.offset = 0;
// sample buffer isn't reset because it's outside of the range of what NR52{7}'s effects
}
/// Reload internal Wave Timer /// Reload internal Wave Timer
pub fn reload(self: *Self, value: u11) void { pub fn reload(self: *Self, value: u11) void {
self.sched.removeScheduledEvent(.{ .ApuChannel = 2 }); self.sched.removeScheduledEvent(.{ .ApuChannel = 2 });

View File

@ -5,6 +5,7 @@ const config = @import("../config.zig");
const Scheduler = @import("scheduler.zig").Scheduler; const Scheduler = @import("scheduler.zig").Scheduler;
const Arm7tdmi = @import("cpu.zig").Arm7tdmi; const Arm7tdmi = @import("cpu.zig").Arm7tdmi;
const FpsTracker = @import("../util.zig").FpsTracker; const FpsTracker = @import("../util.zig").FpsTracker;
const RingBuffer = @import("../util.zig").RingBuffer;
const Timer = std.time.Timer; const Timer = std.time.Timer;
const Atomic = std.atomic.Atomic; const Atomic = std.atomic.Atomic;
@ -58,7 +59,7 @@ fn inner(comptime kind: RunKind, audio_sync: bool, quit: *Atomic(bool), schedule
while (!quit.load(.SeqCst)) { while (!quit.load(.SeqCst)) {
runFrame(scheduler, cpu); runFrame(scheduler, cpu);
audioSync(audio_sync, cpu.bus.apu.stream, &cpu.bus.apu.is_buffer_full); audioSync(audio_sync, &cpu.bus.apu.sample_queue);
if (kind == .UnlimitedFPS) tracker.?.tick(); if (kind == .UnlimitedFPS) tracker.?.tick();
} }
@ -77,7 +78,7 @@ fn inner(comptime kind: RunKind, audio_sync: bool, quit: *Atomic(bool), schedule
// the amount of time needed for audio to catch up rather than // the amount of time needed for audio to catch up rather than
// our expected wake-up time // our expected wake-up time
audioSync(audio_sync, cpu.bus.apu.stream, &cpu.bus.apu.is_buffer_full); audioSync(audio_sync, &cpu.bus.apu.sample_queue);
if (!audio_sync) spinLoop(&timer, wake_time); if (!audio_sync) spinLoop(&timer, wake_time);
wake_time = new_wake_time; wake_time = new_wake_time;
@ -104,22 +105,13 @@ pub fn runFrame(sched: *Scheduler, cpu: *Arm7tdmi) void {
} }
} }
fn audioSync(audio_sync: bool, stream: *SDL.SDL_AudioStream, is_buffer_full: *bool) void { fn audioSync(audio_sync: bool, sample_queue: *RingBuffer(u16)) void {
comptime std.debug.assert(@import("../platform.zig").sample_format == SDL.AUDIO_U16); comptime std.debug.assert(@import("../platform.zig").sample_format == SDL.AUDIO_U16);
const sample_size = 2 * @sizeOf(u16); // const sample_size = 2 * @sizeOf(u16);
const max_buf_size: c_int = 0x400; // const max_buf_size: c_int = 0x400;
// Determine whether the APU is busy right at this moment _ = audio_sync;
var still_full: bool = SDL.SDL_AudioStreamAvailable(stream) > sample_size * if (is_buffer_full.*) max_buf_size >> 1 else max_buf_size; _ = sample_queue;
defer is_buffer_full.* = still_full; // Update APU Busy status right before exiting scope
// If Busy is false, there's no need to sync here
if (!still_full) return;
while (true) {
still_full = SDL.SDL_AudioStreamAvailable(stream) > sample_size * max_buf_size >> 1;
if (!audio_sync or !still_full) break;
}
} }
fn videoSync(timer: *Timer, wake_time: u64) u64 { fn videoSync(timer: *Timer, wake_time: u64) u64 {

View File

@ -12,7 +12,7 @@ const FpsTracker = @import("util.zig").FpsTracker;
const gba_width = @import("core/ppu.zig").width; const gba_width = @import("core/ppu.zig").width;
const gba_height = @import("core/ppu.zig").height; const gba_height = @import("core/ppu.zig").height;
pub const sample_rate = 1 << 15; pub const sample_rate = 1 << 16;
pub const sample_format = SDL.AUDIO_U16; pub const sample_format = SDL.AUDIO_U16;
const default_title: []const u8 = "ZBA"; const default_title: []const u8 = "ZBA";
@ -202,7 +202,7 @@ pub const Gui = struct {
SDL.SDLK_RSHIFT => io.keyinput.select.set(), SDL.SDLK_RSHIFT => io.keyinput.select.set(),
SDL.SDLK_i => { SDL.SDLK_i => {
comptime std.debug.assert(sample_format == SDL.AUDIO_U16); comptime std.debug.assert(sample_format == SDL.AUDIO_U16);
log.err("Sample Count: {}", .{@intCast(u32, SDL.SDL_AudioStreamAvailable(cpu.bus.apu.stream)) / (2 * @sizeOf(u16))}); log.err("Sample Count: {}", .{cpu.bus.apu.sample_queue.len() / 2});
}, },
SDL.SDLK_j => log.err("Scheduler Capacity: {} | Scheduler Event Count: {}", .{ scheduler.queue.capacity(), scheduler.queue.count() }), SDL.SDLK_j => log.err("Scheduler Capacity: {} | Scheduler Event Count: {}", .{ scheduler.queue.capacity(), scheduler.queue.count() }),
SDL.SDLK_k => { SDL.SDLK_k => {
@ -292,7 +292,15 @@ const Audio = struct {
const T = *Apu; const T = *Apu;
const apu = @ptrCast(T, @alignCast(@alignOf(T), userdata)); const apu = @ptrCast(T, @alignCast(@alignOf(T), userdata));
_ = SDL.SDL_AudioStreamGet(apu.stream, stream, len); comptime std.debug.assert(sample_format == SDL.AUDIO_U16);
const sample_buf = @ptrCast([*]u16, @alignCast(@alignOf(u16), stream))[0 .. @intCast(u32, len) / @sizeOf(u16)];
var previous: u16 = 0x8000;
for (sample_buf) |*sample| {
if (apu.sample_queue.pop()) |value| previous = value;
sample.* = previous;
}
} }
}; };

View File

@ -275,3 +275,73 @@ fn HalfInt(comptime T: type) type {
return std.meta.Int(type_info.Int.signedness, type_info.Int.bits >> 1); return std.meta.Int(type_info.Int.signedness, type_info.Int.bits >> 1);
} }
const Mutex = std.Thread.Mutex;
pub fn RingBuffer(comptime T: type) type {
return struct {
const Self = @This();
const Index = usize;
const max_capacity = (std.math.powi(Index, 2, @typeInfo(Index).Int.bits - 1) catch @compileError("uhhhhh")) - 1;
read: Index,
write: Index,
buf: []T,
mutex: Mutex,
const Error = error{buffer_full};
pub fn init(buf: []T) Self {
std.debug.assert(std.math.isPowerOfTwo(buf.len)); // capacity must be a power of two
std.debug.assert(buf.len <= max_capacity); // Capacity must be half the range of the index data types
return .{ .read = 0, .write = 0, .buf = buf, .mutex = .{} };
}
pub fn push(self: *Self, left: T, right: T) Error!void {
self.mutex.lock();
defer self.mutex.unlock();
try self._push(left);
self._push(right) catch |e| {
self.write -= 1; // undo the previous write;
return e;
};
}
pub fn pop(self: *Self) ?T {
self.mutex.lock();
defer self.mutex.unlock();
if (self.isEmpty()) return null;
defer self.read += 1;
return self.buf[self.mask(self.read)];
}
pub fn len(self: *const Self) Index {
return self.write - self.read;
}
fn _push(self: *Self, value: T) Error!void {
if (self.isFull()) return error.buffer_full;
defer self.write += 1;
self.buf[self.mask(self.write)] = value;
}
fn isFull(self: *const Self) bool {
return self.len() == self.buf.len;
}
fn isEmpty(self: *const Self) bool {
return self.read == self.write;
}
fn mask(self: *const Self, idx: Index) Index {
return idx & (self.buf.len - 1);
}
};
}