diff --git a/src/core/apu.zig b/src/core/apu.zig index 05b6cea..8cf2a81 100644 --- a/src/core/apu.zig +++ b/src/core/apu.zig @@ -15,12 +15,10 @@ const SoundFifo = std.fifo.LinearFifo(u8, .{ .Static = 0x20 }); const getHalf = util.getHalf; const setHalf = util.setHalf; const intToBytes = util.intToBytes; +const RingBuffer = util.RingBuffer; 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 { const byte_addr = @truncate(u8, addr); @@ -246,17 +244,20 @@ pub const Apu = struct { sampling_cycle: u2, - stream: *SDL.SDL_AudioStream, + sample_queue: RingBuffer(u16), sched: *Scheduler, fs: FrameSequencer, capacitor: f32, - is_buffer_full: bool, - pub const Tick = enum { Length, Envelope, Sweep }; 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 = .{ .ch1 = ToneSweep.init(sched), .ch2 = Tone.init(sched), @@ -271,12 +272,11 @@ pub const Apu = struct { .bias = .{ .raw = 0x0200 }, .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, .capacitor = 0, .fs = FrameSequencer.init(), - .is_buffer_full = false, }; sched.push(.SampleAudio, apu.interval()); @@ -370,11 +370,6 @@ pub const Apu = struct { pub fn sampleAudio(self: *Self, late: u64) void { 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 right: i16 = 0; @@ -430,23 +425,7 @@ pub const Apu = struct { 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()) self.replaceSDLResampler(); - - _ = 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).?; + self.sample_queue.push(ext_left, ext_right) catch {}; } fn interval(self: *const Self) u64 { diff --git a/src/core/emu.zig b/src/core/emu.zig index d8d2375..5f62d89 100644 --- a/src/core/emu.zig +++ b/src/core/emu.zig @@ -5,6 +5,7 @@ const config = @import("../config.zig"); const Scheduler = @import("scheduler.zig").Scheduler; const Arm7tdmi = @import("cpu.zig").Arm7tdmi; const FpsTracker = @import("../util.zig").FpsTracker; +const RingBuffer = @import("../util.zig").RingBuffer; const Timer = std.time.Timer; const Atomic = std.atomic.Atomic; @@ -58,7 +59,7 @@ fn inner(comptime kind: RunKind, audio_sync: bool, quit: *Atomic(bool), schedule while (!quit.load(.Monotonic)) { 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(); } @@ -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 // 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); 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); - const sample_size = 2 * @sizeOf(u16); - const max_buf_size: c_int = 0x400; + // const sample_size = 2 * @sizeOf(u16); + // const max_buf_size: c_int = 0x400; - // 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 (!audio_sync or !still_full) break; - } + _ = audio_sync; + _ = sample_queue; } fn videoSync(timer: *Timer, wake_time: u64) u64 { diff --git a/src/platform.zig b/src/platform.zig index c5adb8d..5a4baf0 100644 --- a/src/platform.zig +++ b/src/platform.zig @@ -12,7 +12,7 @@ const FpsTracker = @import("util.zig").FpsTracker; const gba_width = @import("core/ppu.zig").width; 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; const default_title = "ZBA"; @@ -216,7 +216,7 @@ pub const Gui = struct { SDL.SDLK_RSHIFT => keyinput.select.set(), SDL.SDLK_i => { 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_k => {}, @@ -299,7 +299,15 @@ const Audio = struct { const T = *Apu; 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; + } } }; diff --git a/src/util.zig b/src/util.zig index 803e14f..716f301 100644 --- a/src/util.zig +++ b/src/util.zig @@ -300,26 +300,15 @@ pub fn RingBuffer(comptime T: type) type { return .{ .read = 0, .write = 0, .buf = buf, .mutex = .{} }; } - pub fn pushPair(self: *Self, left: T, right: T) Error!void { + pub fn push(self: *Self, left: T, right: T) Error!void { self.mutex.lock(); defer self.mutex.unlock(); - // TODO: Make this less convoluted - if (self.len() + 1 >= self.buf.len) return error.buffer_full; - defer self.write += 2; - - self.buf[self.write & (self.buf.len - 1)] = left; - self.buf[(self.write + 1) & (self.buf.len - 1)] = right; - } - - pub fn push(self: *Self, value: T) Error!void { - self.mutex.lock(); - defer self.mutex.unlock(); - - if (self.isFull()) return error.buffer_full; - defer self.write += 1; - - self.buf[self.write & (self.buf.len - 1)] = value; + try self._push(left); + self._push(right) catch |e| { + self.write -= 1; // undo the previous write; + return e; + }; } pub fn pop(self: *Self) ?T { @@ -329,7 +318,18 @@ pub fn RingBuffer(comptime T: type) type { if (self.isEmpty()) return null; defer self.read += 1; - return self.buf[self.read & (self.buf.len - 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 { @@ -340,8 +340,8 @@ pub fn RingBuffer(comptime T: type) type { return self.read == self.write; } - pub fn len(self: *const Self) Index { - return self.write - self.read; + fn mask(self: *const Self, idx: Index) Index { + return idx & (self.buf.len - 1); } }; }