From af1887e0a6937cc4cf5ad5d1a15919ff9d93a35f Mon Sep 17 00:00:00 2001 From: Rekai Musuka Date: Fri, 21 Oct 2022 05:12:36 -0300 Subject: [PATCH] feat: schedule audio sampling on scheduler DMA sound in games like Pokemon Emerald, Chobits, Love Hina, and Kirby: Nightmare in Dream Land sound great save for conerns about resampling --- src/Bus.zig | 2 +- src/apu.zig | 100 +++++++++++++++++++++++----------------------- src/bus/io.zig | 20 +++++----- src/bus/timer.zig | 8 +--- src/main.zig | 4 +- src/scheduler.zig | 2 + 6 files changed, 67 insertions(+), 69 deletions(-) diff --git a/src/Bus.zig b/src/Bus.zig index c1ae26a..37c8d7e 100644 --- a/src/Bus.zig +++ b/src/Bus.zig @@ -41,7 +41,7 @@ pub fn init(alloc: Allocator, sched: *Scheduler, paths: FilePaths) !Self { .pak = try GamePak.init(alloc, paths.rom, paths.save), .bios = try Bios.init(alloc, paths.bios), .ppu = try Ppu.init(alloc, sched), - .apu = Apu.init(), + .apu = Apu.init(sched), .iwram = try Iwram.init(alloc), .ewram = try Ewram.init(alloc), .dma = DmaControllers.init(), diff --git a/src/apu.zig b/src/apu.zig index 43bf872..9b7f51d 100644 --- a/src/apu.zig +++ b/src/apu.zig @@ -1,7 +1,9 @@ const std = @import("std"); const SDL = @import("sdl2"); const io = @import("bus/io.zig"); + const Arm7tdmi = @import("cpu.zig").Arm7tdmi; +const Scheduler = @import("scheduler.zig").Scheduler; const SoundFifo = std.fifo.LinearFifo(u8, .{ .Static = 0x20 }); const AudioDeviceId = SDL.SDL_AudioDeviceID; @@ -9,6 +11,9 @@ const AudioDeviceId = SDL.SDL_AudioDeviceID; const intToBytes = @import("util.zig").intToBytes; const log = std.log.scoped(.APU); +const sample_rate = 32768; +const sample_ticks = 280896 * 60 / sample_rate; + pub const Apu = struct { const Self = @This(); @@ -25,9 +30,10 @@ pub const Apu = struct { cnt: io.SoundControl, dev: ?AudioDeviceId, + sched: *Scheduler, - pub fn init() Self { - return .{ + pub fn init(sched: *Scheduler) Self { + const apu: Self = .{ .ch1 = ToneSweep.init(), .ch2 = Tone.init(), .ch3 = Wave.init(), @@ -41,7 +47,11 @@ pub const Apu = struct { .bias = .{ .raw = 0x0200 }, .dev = null, + .sched = sched, }; + sched.push(.SampleAudio, sched.now() + sample_ticks); + + return apu; } pub fn attachAudioDevice(self: *Self, dev: AudioDeviceId) void { @@ -53,8 +63,8 @@ pub const Apu = struct { // Reinitializing instead of resetting is fine because // the FIFOs I'm using are stack allocated and 0x20 bytes big - if (new.sa_reset.read()) self.chA.fifo = SoundFifo.init(); - if (new.sb_reset.read()) self.chB.fifo = SoundFifo.init(); + if (new.chA_reset.read()) self.chA.fifo = SoundFifo.init(); + if (new.chB_reset.read()) self.chB.fifo = SoundFifo.init(); self.dma_cnt = new; } @@ -75,19 +85,35 @@ pub const Apu = struct { self.bias.raw = (@as(u16, byte) << 8) | (self.bias.raw & 0xFF); } - pub fn handleTimerOverflow(self: *Self, kind: DmaSoundKind, cpu: *Arm7tdmi) void { + pub fn sampleAudio(self: *Self, late: u64) void { + const chA = if (self.dma_cnt.chA_vol.read()) self.chA.amplitude() else self.chA.amplitude() / 2; + const chA_left = if (self.dma_cnt.chA_left.read()) chA else 0; + const chA_right = if (self.dma_cnt.chA_right.read()) chA else 0; + + const chB = if (self.dma_cnt.chB_vol.read()) self.chB.amplitude() else self.chB.amplitude() / 2; + const chB_left = if (self.dma_cnt.chB_left.read()) chB else 0; + const chB_right = if (self.dma_cnt.chB_right.read()) chB else 0; + + const left = (chA_left + chB_left) / 2; + const right = (chA_right + chB_right) / 2; + + if (self.dev) |dev| _ = SDL.SDL_QueueAudio(dev, &[2]f32{ left, right }, 2 * @sizeOf(f32)); + + self.sched.push(.SampleAudio, self.sched.now() + sample_ticks - late); + } + + pub fn handleTimerOverflow(self: *Self, cpu: *Arm7tdmi, tim_id: u3) void { if (!self.cnt.apu_enable.read()) return; - const samples = switch (kind) { - .A => blk: { - break :blk self.chA.handleTimerOverflow(cpu, self.dma_cnt); - }, - .B => blk: { - break :blk self.chB.handleTimerOverflow(cpu, self.dma_cnt); - }, - }; + if (@boolToInt(self.dma_cnt.chA_timer.read()) == tim_id) { + self.chA.updateSample(); + if (self.chA.len() <= 15) cpu.bus.dma._1.enableSoundDma(0x0400_00A0); + } - if (self.dev) |dev| _ = SDL.SDL_QueueAudio(dev, &samples, 2); + if (@boolToInt(self.dma_cnt.chB_timer.read()) == tim_id) { + self.chB.updateSample(); + if (self.chB.len() <= 15) cpu.bus.dma._2.enableSoundDma(0x0400_00A4); + } } }; @@ -207,55 +233,31 @@ pub fn DmaSound(comptime kind: DmaSoundKind) type { const Self = @This(); fifo: SoundFifo, - kind: DmaSoundKind, + sample: i8, fn init() Self { - return .{ .fifo = SoundFifo.init(), .kind = kind }; + return .{ + .fifo = SoundFifo.init(), + .kind = kind, + .sample = 0, + }; } pub fn push(self: *Self, value: u32) void { self.fifo.write(&intToBytes(u32, value)) catch {}; } - pub fn pop(self: *Self) u8 { - return self.fifo.readItem() orelse 0; - } - pub fn len(self: *const Self) usize { return self.fifo.readableLength(); } - pub fn handleTimerOverflow(self: *Self, cpu: *Arm7tdmi, cnt: io.DmaSoundControl) [2]u8 { - const sample = self.pop(); + pub fn updateSample(self: *Self) void { + if (self.fifo.readItem()) |sample| self.sample = @bitCast(i8, sample); + } - var left: u8 = 0; - var right: u8 = 0; - var fifo_addr: u32 = undefined; - - switch (kind) { - .A => { - const vol = @boolToInt(!cnt.sa_vol.read()); // if unset, vol is 50% - if (cnt.sa_left_enable.read()) left = sample >> vol; - if (cnt.sa_right_enable.read()) right = sample >> vol; - - fifo_addr = 0x0400_00A0; - }, - .B => { - const vol = @boolToInt(!cnt.sb_vol.read()); // if unset, vol is 50% - if (cnt.sb_left_enable.read()) left = sample >> vol; - if (cnt.sb_right_enable.read()) right = sample >> vol; - - fifo_addr = 0x0400_00A4; - }, - } - - if (self.len() <= 15) { - cpu.bus.dma._1.enableSoundDma(fifo_addr); - cpu.bus.dma._2.enableSoundDma(fifo_addr); - } - - return .{ left, right }; + pub fn amplitude(self: *const Self) f32 { + return @intToFloat(f32, self.sample) / 127.5 - (1 / 255); } }; } diff --git a/src/bus/io.zig b/src/bus/io.zig index 35e4508..31ff65f 100644 --- a/src/bus/io.zig +++ b/src/bus/io.zig @@ -595,20 +595,20 @@ pub const ChannelVolumeControl = extern union { /// Read / Write pub const DmaSoundControl = extern union { ch_vol: Bitfield(u16, 0, 2), - sa_vol: Bit(u16, 2), - sb_vol: Bit(u16, 3), + chA_vol: Bit(u16, 2), + chB_vol: Bit(u16, 3), - sa_right_enable: Bit(u16, 8), - sa_left_enable: Bit(u16, 9), - sa_timer: Bit(u16, 10), + chA_right: Bit(u16, 8), + chA_left: Bit(u16, 9), + chA_timer: Bit(u16, 10), /// Write only? - sa_reset: Bit(u16, 11), + chA_reset: Bit(u16, 11), - sb_right_enable: Bit(u16, 12), - sb_left_enable: Bit(u16, 13), - sb_timer: Bit(u16, 14), + chB_right: Bit(u16, 12), + chB_left: Bit(u16, 13), + chB_timer: Bit(u16, 14), /// Write only? - sb_reset: Bit(u16, 15), + chB_reset: Bit(u16, 15), raw: u16, }; diff --git a/src/bus/timer.zig b/src/bus/timer.zig index d68f9fe..cabc4a6 100644 --- a/src/bus/timer.zig +++ b/src/bus/timer.zig @@ -109,13 +109,7 @@ fn Timer(comptime id: u2) type { // DMA Sound Things if (id == 0 or id == 1) { - const apu = &cpu.bus.apu; - - const a_tim = @boolToInt(apu.dma_cnt.sa_timer.read()); - const b_tim = @boolToInt(apu.dma_cnt.sb_timer.read()); - - if (a_tim == id) apu.handleTimerOverflow(.A, cpu); - if (b_tim == id) apu.handleTimerOverflow(.B, cpu); + cpu.bus.apu.handleTimerOverflow(cpu, id); } // Perform Cascade Behaviour diff --git a/src/main.zig b/src/main.zig index cdc25e0..34481a3 100644 --- a/src/main.zig +++ b/src/main.zig @@ -242,9 +242,9 @@ fn initAudio() SDL.SDL_AudioDeviceID { var have: SDL.SDL_AudioSpec = undefined; var want = std.mem.zeroes(SDL.SDL_AudioSpec); want.freq = 32768; - want.format = SDL.AUDIO_S8; + want.format = SDL.AUDIO_F32; want.channels = 2; - want.samples = 0x200; + want.samples = 0x100; want.callback = null; const dev = SDL.SDL_OpenAudioDevice(null, 0, &want, &have, 0); diff --git a/src/scheduler.zig b/src/scheduler.zig index 534aebc..b4b1d4a 100644 --- a/src/scheduler.zig +++ b/src/scheduler.zig @@ -51,6 +51,7 @@ pub const Scheduler = struct { 3 => cpu.bus.tim._3.handleOverflow(cpu, late), } }, + .SampleAudio => cpu.bus.apu.sampleAudio(late), .HBlank => cpu.bus.ppu.handleHBlankEnd(cpu, late), // The end of a HBlank .VBlank => cpu.bus.ppu.handleHDrawEnd(cpu, late), // The end of a VBlank } @@ -102,4 +103,5 @@ pub const EventKind = union(enum) { VBlank, Draw, TimerOverflow: u2, + SampleAudio, };