From d2a50cf9d28f90c9908d008af22b22f46b005d84 Mon Sep 17 00:00:00 2001 From: Rekai Musuka Date: Fri, 21 Oct 2022 05:12:57 -0300 Subject: [PATCH] feat: reimplement audio sync APU will now drop samples if the Audio Queue is already full, therefore creating a "sped-up" effect when the emulator runs faster than 100% --- src/Gui.zig | 4 ++-- src/core/apu.zig | 46 +++++++++++++++++++++++++++++----------------- src/core/emu.zig | 41 +++++++++++++++++++++++++++-------------- 3 files changed, 58 insertions(+), 33 deletions(-) diff --git a/src/Gui.zig b/src/Gui.zig index cb1f2e2..1d0be00 100644 --- a/src/Gui.zig +++ b/src/Gui.zig @@ -180,11 +180,11 @@ const Audio = struct { export fn callback(userdata: ?*anyopaque, stream: [*c]u8, len: c_int) void { const apu = @ptrCast(*Apu, @alignCast(@alignOf(*Apu), userdata)); - const written = SDL.SDL_AudioStreamGet(apu.stream, stream, len); + _ = SDL.SDL_AudioStreamGet(apu.stream, stream, len); // If we don't write anything, play silence otherwise garbage will be played // FIXME: I don't think this hack to remove DC Offset is acceptable :thinking: - if (written == 0) std.mem.set(u8, stream[0..@intCast(usize, len)], 0x40); + // if (written == 0) std.mem.set(u8, stream[0..@intCast(usize, len)], 0x40); } }; diff --git a/src/core/apu.zig b/src/core/apu.zig index c0eb749..c100dd0 100644 --- a/src/core/apu.zig +++ b/src/core/apu.zig @@ -163,6 +163,8 @@ pub const Apu = struct { fs: FrameSequencer, capacitor: f32, + is_buffer_full: bool, + pub fn init(sched: *Scheduler) Self { const apu: Self = .{ .ch1 = ToneSweep.init(sched), @@ -178,11 +180,12 @@ pub const Apu = struct { .bias = .{ .raw = 0x0200 }, .sampling_cycle = 0b00, - .stream = SDL.SDL_NewAudioStream(SDL.AUDIO_U16, 2, 1 << 15, SDL.AUDIO_U16, 2, host_sample_rate) orelse unreachable, + .stream = SDL.SDL_NewAudioStream(SDL.AUDIO_U16, 2, 1 << 15, SDL.AUDIO_U16, 2, host_sample_rate).?, .sched = sched, .capacitor = 0, .fs = FrameSequencer.init(), + .is_buffer_full = false, }; sched.push(.SampleAudio, apu.sampleTicks()); @@ -277,6 +280,13 @@ pub const Apu = struct { } pub fn sampleAudio(self: *Self, late: u64) void { + self.sched.push(.SampleAudio, self.sampleTicks() -| 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 right: i16 = 0; @@ -325,28 +335,30 @@ pub const Apu = struct { left += bias; right += bias; - const tmp_left = std.math.clamp(@bitCast(u16, left), std.math.minInt(u11), std.math.maxInt(u11)); - const tmp_right = std.math.clamp(@bitCast(u16, right), std.math.minInt(u11), std.math.maxInt(u11)); + const clamped_left = std.math.clamp(@bitCast(u16, left), std.math.minInt(u11), std.math.maxInt(u11)); + const clamped_right = std.math.clamp(@bitCast(u16, right), std.math.minInt(u11), std.math.maxInt(u11)); // Extend to 16-bit signed audio samples - const final_left = (tmp_left << 5) | (tmp_left >> 6); - const final_right = (tmp_right << 5) | (tmp_right >> 6); + const ext_left = (clamped_left << 5) | (clamped_left >> 6); + const ext_right = (clamped_right << 5) | (clamped_right >> 6); - if (self.sampling_cycle != self.bias.sampling_cycle.read()) { - const new_sample_rate = Self.sampleRate(self.bias.sampling_cycle.read()); - log.info("Sample Rate changed from {}Hz to {}Hz", .{ Self.sampleRate(self.sampling_cycle), new_sample_rate }); + // FIXME: This rarely happens + if (self.sampling_cycle != self.bias.sampling_cycle.read()) self.replaceSDLResampler(); - // Sample Rate Changed, Create a new Resampler since i can't figure out how to change - // the parameters of the old one - const old = self.stream; - defer SDL.SDL_FreeAudioStream(old); + _ = SDL.SDL_AudioStreamPut(self.stream, &[2]u16{ ext_left, ext_right }, 2 * @sizeOf(u16)); + } - self.sampling_cycle = self.bias.sampling_cycle.read(); - self.stream = SDL.SDL_NewAudioStream(SDL.AUDIO_U16, 2, @intCast(c_int, new_sample_rate), SDL.AUDIO_U16, 2, host_sample_rate) orelse unreachable; - } + fn replaceSDLResampler(self: *Self) void { + 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 }); - _ = SDL.SDL_AudioStreamPut(self.stream, &[2]u16{ final_left, final_right }, 2 * @sizeOf(u16)); - self.sched.push(.SampleAudio, self.sampleTicks() -| late); + // 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), SDL.AUDIO_U16, 2, host_sample_rate).?; } fn sampleTicks(self: *const Self) u64 { diff --git a/src/core/emu.zig b/src/core/emu.zig index 0e32291..be33d57 100644 --- a/src/core/emu.zig +++ b/src/core/emu.zig @@ -70,12 +70,21 @@ pub fn runFrame(sched: *Scheduler, cpu: *Arm7tdmi) void { } } -fn syncToAudio(cpu: *const Arm7tdmi) void { - const stream = cpu.bus.apu.stream; - const min_sample_count = 0x800; +fn syncToAudio(stream: *SDL.SDL_AudioStream, is_buffer_full: *bool) void { + const sample_size = 2 * @sizeOf(u16); + const max_buf_size: c_int = 0x400; - // Busy Loop while we wait for the Audio system to catch up - while (SDL.SDL_AudioStreamAvailable(stream) > (@sizeOf(u16) * 2) * min_sample_count) {} + // Determine whether the APU is busy right at this moment + var still_full: bool = SDL.SDL_AudioStreamAvailable(stream) > sample_size * if (is_buffer_full.*) max_buf_size >> 1 else max_buf_size; + 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 (!sync_audio or !still_full) break; + } } pub fn runUnsynchronized(quit: *Atomic(bool), sched: *Scheduler, cpu: *Arm7tdmi, fps: ?*FpsTracker) void { @@ -86,21 +95,21 @@ pub fn runUnsynchronized(quit: *Atomic(bool), sched: *Scheduler, cpu: *Arm7tdmi, while (!quit.load(.SeqCst)) { runFrame(sched, cpu); - if (sync_audio) syncToAudio(cpu); + syncToAudio(cpu.bus.apu.stream, &cpu.bus.apu.is_buffer_full); tracker.tick(); } } else { while (!quit.load(.SeqCst)) { runFrame(sched, cpu); - if (sync_audio) syncToAudio(cpu); + syncToAudio(cpu.bus.apu.stream, &cpu.bus.apu.is_buffer_full); } } } pub fn runSynchronized(quit: *Atomic(bool), sched: *Scheduler, cpu: *Arm7tdmi, fps: ?*FpsTracker) void { log.info("Emulation thread w/ video sync", .{}); - var timer = Timer.start() catch unreachable; + var timer = Timer.start() catch std.debug.panic("Failed to initialize std.timer.Timer", .{}); var wake_time: u64 = frame_period; if (fps) |tracker| { @@ -108,13 +117,14 @@ pub fn runSynchronized(quit: *Atomic(bool), sched: *Scheduler, cpu: *Arm7tdmi, f while (!quit.load(.SeqCst)) { runFrame(sched, cpu); - const new_wake_time = syncToVideo(&timer, wake_time); + const new_wake_time = blockOnVideo(&timer, wake_time); // Spin to make up the difference of OS scheduler innacuracies // If we happen to also be syncing to audio, we choose to spin on // the amount of time needed for audio to catch up rather than // our expected wake-up time - if (sync_audio) syncToAudio(cpu) else spinLoop(&timer, wake_time); + syncToAudio(cpu.bus.apu.stream, &cpu.bus.apu.is_buffer_full); + if (!sync_audio) spinLoop(&timer, wake_time); wake_time = new_wake_time; tracker.tick(); @@ -122,16 +132,17 @@ pub fn runSynchronized(quit: *Atomic(bool), sched: *Scheduler, cpu: *Arm7tdmi, f } else { while (!quit.load(.SeqCst)) { runFrame(sched, cpu); - const new_wake_time = syncToVideo(&timer, wake_time); - // see above comment - if (sync_audio) syncToAudio(cpu) else spinLoop(&timer, wake_time); + const new_wake_time = blockOnVideo(&timer, wake_time); + // see above comment + syncToAudio(cpu.bus.apu.stream, &cpu.bus.apu.is_buffer_full); + if (!sync_audio) spinLoop(&timer, wake_time); wake_time = new_wake_time; } } } -inline fn syncToVideo(timer: *Timer, wake_time: u64) u64 { +inline fn blockOnVideo(timer: *Timer, wake_time: u64) u64 { // Use the OS scheduler to put the emulation thread to sleep const maybe_recalc_wake_time = sleep(timer, wake_time); @@ -149,6 +160,8 @@ pub fn runBusyLoop(quit: *Atomic(bool), sched: *Scheduler, cpu: *Arm7tdmi) void runFrame(sched, cpu); spinLoop(&timer, wake_time); + syncToAudio(cpu.bus.apu.stream, &cpu.bus.apu.is_buffer_full); + // Update to the new wake time wake_time += frame_period; }