From 208f4b522d1d2826cb46ad0bc7a80be14751192a Mon Sep 17 00:00:00 2001 From: Rekai Musuka Date: Fri, 21 Oct 2022 05:13:03 -0300 Subject: [PATCH] style(apu): split apu.zig into multiple files + refactor --- src/core/apu.zig | 938 +------------------------------ src/core/apu/Noise.zig | 144 +++++ src/core/apu/Tone.zig | 140 +++++ src/core/apu/ToneSweep.zig | 190 +++++++ src/core/apu/Wave.zig | 129 +++++ src/core/apu/device/Envelope.zig | 28 + src/core/apu/device/Length.zig | 18 + src/core/apu/device/Sweep.zig | 52 ++ src/core/apu/signal/Lfsr.zig | 75 +++ src/core/apu/signal/Square.zig | 75 +++ src/core/apu/signal/Wave.zig | 95 ++++ 11 files changed, 959 insertions(+), 925 deletions(-) create mode 100644 src/core/apu/Noise.zig create mode 100644 src/core/apu/Tone.zig create mode 100644 src/core/apu/ToneSweep.zig create mode 100644 src/core/apu/Wave.zig create mode 100644 src/core/apu/device/Envelope.zig create mode 100644 src/core/apu/device/Length.zig create mode 100644 src/core/apu/device/Sweep.zig create mode 100644 src/core/apu/signal/Lfsr.zig create mode 100644 src/core/apu/signal/Square.zig create mode 100644 src/core/apu/signal/Wave.zig diff --git a/src/core/apu.zig b/src/core/apu.zig index a4dcdf5..61cc64c 100644 --- a/src/core/apu.zig +++ b/src/core/apu.zig @@ -3,11 +3,16 @@ const SDL = @import("sdl2"); const io = @import("bus/io.zig"); const util = @import("../util.zig"); +const AudioDeviceId = SDL.SDL_AudioDeviceID; + const Arm7tdmi = @import("cpu.zig").Arm7tdmi; const Scheduler = @import("scheduler.zig").Scheduler; +const ToneSweep = @import("apu/ToneSweep.zig"); +const Tone = @import("apu/Tone.zig"); +const Wave = @import("apu/Wave.zig"); +const Noise = @import("apu/Noise.zig"); const SoundFifo = std.fifo.LinearFifo(u8, .{ .Static = 0x20 }); -const AudioDeviceId = SDL.SDL_AudioDeviceID; const intToBytes = @import("../util.zig").intToBytes; const log = std.log.scoped(.APU); @@ -188,10 +193,10 @@ pub const Apu = struct { }; sched.push(.SampleAudio, apu.sampleTicks()); - sched.push(.{ .ApuChannel = 0 }, SquareWave.tickInterval); // Channel 1 - sched.push(.{ .ApuChannel = 1 }, SquareWave.tickInterval); // Channel 2 - sched.push(.{ .ApuChannel = 2 }, WaveDevice.tickInterval); // Channel 3 - sched.push(.{ .ApuChannel = 3 }, Lfsr.tickInterval); // Channel 4 + sched.push(.{ .ApuChannel = 0 }, @import("apu/signal/Square.zig").interval); + sched.push(.{ .ApuChannel = 1 }, @import("apu/signal/Square.zig").interval); + sched.push(.{ .ApuChannel = 2 }, @import("apu/signal/Wave.zig").interval); + sched.push(.{ .ApuChannel = 3 }, @import("apu/signal/Lfsr.zig").interval); sched.push(.FrameSequencer, ((1 << 24) / 512)); return apu; @@ -413,640 +418,6 @@ pub const Apu = struct { } }; -const ToneSweep = struct { - const Self = @This(); - - /// NR10 - sweep: io.Sweep, - /// NR11 - duty: io.Duty, - /// NR12 - envelope: io.Envelope, - /// NR13, NR14 - freq: io.Frequency, - - /// Length Functionality - len_dev: LengthDevice, - /// Sweep Functionality - sweep_dev: SweepDevice, - /// Envelope Functionality - env_dev: EnvelopeDevice, - /// Frequency Timer Functionality - square: SquareWave, - enabled: bool, - - sample: i8, - - const SweepDevice = struct { - const This = @This(); - - timer: u8, - enabled: bool, - shadow: u11, - - calc_performed: bool, - - pub fn init() This { - return .{ - .timer = 0, - .enabled = false, - .shadow = 0, - .calc_performed = false, - }; - } - - pub fn tick(self: *This, ch1: *Self) void { - if (self.timer != 0) self.timer -= 1; - - if (self.timer == 0) { - const period = ch1.sweep.period.read(); - self.timer = if (period == 0) 8 else period; - if (!self.calc_performed) self.calc_performed = true; - - if (self.enabled and period != 0) { - const new_freq = self.calcFrequency(ch1); - - if (new_freq <= 0x7FF and ch1.sweep.shift.read() != 0) { - ch1.freq.frequency.write(@truncate(u11, new_freq)); - self.shadow = @truncate(u11, new_freq); - - _ = self.calcFrequency(ch1); - } - } - } - } - - fn calcFrequency(self: *This, ch1: *Self) u12 { - const shadow = @as(u12, self.shadow); - const shadow_shifted = shadow >> ch1.sweep.shift.read(); - const decrease = ch1.sweep.direction.read(); - - const freq = if (decrease) shadow - shadow_shifted else shadow + shadow_shifted; - - if (freq > 0x7FF) ch1.enabled = false; - - return freq; - } - }; - - fn init(sched: *Scheduler) Self { - return .{ - .sweep = .{ .raw = 0 }, - .duty = .{ .raw = 0 }, - .envelope = .{ .raw = 0 }, - .freq = .{ .raw = 0 }, - .sample = 0, - .enabled = false, - - .square = SquareWave.init(sched), - .len_dev = LengthDevice.init(), - .sweep_dev = SweepDevice.init(), - .env_dev = EnvelopeDevice.init(), - }; - } - - fn reset(self: *Self) void { - self.sweep.raw = 0; - self.sweep_dev.calc_performed = false; - - self.duty.raw = 0; - self.envelope.raw = 0; - self.freq.raw = 0; - - self.sample = 0; - self.enabled = false; - } - - fn tickSweep(self: *Self) void { - self.sweep_dev.tick(self); - } - - pub fn tickLength(self: *Self) void { - self.len_dev.tick(self.freq.length_enable.read(), &self.enabled); - } - - pub fn tickEnvelope(self: *Self) void { - self.env_dev.tick(self.envelope); - } - - pub fn channelTimerOverflow(self: *Self, late: u64) void { - self.square.handleTimerOverflow(.Ch1, self.freq, late); - - self.sample = 0; - if (!self.isDacEnabled()) return; - self.sample = if (self.enabled) self.square.sample(self.duty) * @as(i8, self.env_dev.vol) else 0; - } - - fn amplitude(self: *const Self) i16 { - return @as(i16, self.sample); - } - - /// NR10, NR11, NR12 - fn setSoundCnt(self: *Self, value: u32) void { - self.setSoundCntL(@truncate(u8, value)); - self.setSoundCntH(@truncate(u16, value >> 16)); - } - - /// NR10 - pub fn getSoundCntL(self: *const Self) u8 { - return self.sweep.raw & 0x7F; - } - - /// NR10 - fn setSoundCntL(self: *Self, value: u8) void { - const new = io.Sweep{ .raw = value }; - - if (self.sweep.direction.read() and !new.direction.read()) { - // Sweep Negate bit has been cleared - // If At least 1 Sweep Calculation has been made since - // the last trigger, the channel is immediately disabled - - if (self.sweep_dev.calc_performed) self.enabled = false; - } - - self.sweep.raw = value; - } - - /// NR11, NR12 - pub fn getSoundCntH(self: *const Self) u16 { - return @as(u16, self.envelope.raw) << 8 | (self.duty.raw & 0xC0); - } - - /// NR11, NR12 - pub fn setSoundCntH(self: *Self, value: u16) void { - self.setNr11(@truncate(u8, value)); - self.setNr12(@truncate(u8, value >> 8)); - } - - /// NR11 - pub fn setNr11(self: *Self, value: u8) void { - self.duty.raw = value; - self.len_dev.timer = @as(u7, 64) - @truncate(u6, value); - } - - /// NR12 - pub fn setNr12(self: *Self, value: u8) void { - self.envelope.raw = value; - if (!self.isDacEnabled()) self.enabled = false; - } - - /// NR13, NR14 - pub fn getSoundCntX(self: *const Self) u16 { - return self.freq.raw & 0x4000; - } - - /// NR13, NR14 - pub fn setSoundCntX(self: *Self, fs: *const FrameSequencer, value: u16) void { - self.setNr13(@truncate(u8, value)); - self.setNr14(fs, @truncate(u8, value >> 8)); - } - - /// NR13 - pub fn setNr13(self: *Self, byte: u8) void { - self.freq.raw = (self.freq.raw & 0xFF00) | byte; - } - - /// NR14 - pub fn setNr14(self: *Self, fs: *const FrameSequencer, byte: u8) void { - var new: io.Frequency = .{ .raw = (@as(u16, byte) << 8) | (self.freq.raw & 0xFF) }; - - if (new.trigger.read()) { - self.enabled = true; - - if (self.len_dev.timer == 0) { - self.len_dev.timer = - if (!fs.isLengthNext() and new.length_enable.read()) 63 else 64; - } - - self.square.reloadTimer(.Ch1, self.freq.frequency.read()); - - // Reload Envelope period and timer - self.env_dev.timer = self.envelope.period.read(); - if (fs.isEnvelopeNext() and self.env_dev.timer != 0b111) self.env_dev.timer += 1; - - self.env_dev.vol = self.envelope.init_vol.read(); - - // Sweep Trigger Behaviour - const sw_period = self.sweep.period.read(); - const sw_shift = self.sweep.shift.read(); - - self.sweep_dev.calc_performed = false; - self.sweep_dev.shadow = self.freq.frequency.read(); - self.sweep_dev.timer = if (sw_period == 0) 8 else sw_period; - self.sweep_dev.enabled = sw_period != 0 or sw_shift != 0; - if (sw_shift != 0) _ = self.sweep_dev.calcFrequency(self); - - self.enabled = self.isDacEnabled(); - } - - self.square.updateToneSweepLength(fs, self, new); - self.freq = new; - } - - fn isDacEnabled(self: *const Self) bool { - return self.envelope.raw & 0xF8 != 0; - } -}; - -const Tone = struct { - const Self = @This(); - - /// NR21 - duty: io.Duty, - /// NR22 - envelope: io.Envelope, - /// NR23, NR24 - freq: io.Frequency, - - /// Length Functionarlity - len_dev: LengthDevice, - /// Envelope Functionality - env_dev: EnvelopeDevice, - /// FrequencyTimer Functionality - square: SquareWave, - - enabled: bool, - sample: i8, - - fn init(sched: *Scheduler) Self { - return .{ - .duty = .{ .raw = 0 }, - .envelope = .{ .raw = 0 }, - .freq = .{ .raw = 0 }, - .enabled = false, - - .square = SquareWave.init(sched), - .len_dev = LengthDevice.init(), - .env_dev = EnvelopeDevice.init(), - - .sample = 0, - }; - } - - fn reset(self: *Self) void { - self.duty.raw = 0; - self.envelope.raw = 0; - self.freq.raw = 0; - - self.sample = 0; - self.enabled = false; - } - - pub fn tickLength(self: *Self) void { - self.len_dev.tick(self.freq.length_enable.read(), &self.enabled); - } - - pub fn tickEnvelope(self: *Self) void { - self.env_dev.tick(self.envelope); - } - - pub fn channelTimerOverflow(self: *Self, late: u64) void { - self.square.handleTimerOverflow(.Ch2, self.freq, late); - - self.sample = 0; - if (!self.isDacEnabled()) return; - self.sample = if (self.enabled) self.square.sample(self.duty) * @as(i8, self.env_dev.vol) else 0; - } - - fn amplitude(self: *const Self) i16 { - return @as(i16, self.sample); - } - - /// NR21, NR22 - pub fn getSoundCntL(self: *const Self) u16 { - return @as(u16, self.envelope.raw) << 8 | (self.duty.raw & 0xC0); - } - - /// NR21, NR22 - pub fn setSoundCntL(self: *Self, value: u16) void { - self.setNr21(@truncate(u8, value)); - self.setNr22(@truncate(u8, value >> 8)); - } - - /// NR21 - pub fn setNr21(self: *Self, value: u8) void { - self.duty.raw = value; - self.len_dev.timer = @as(u7, 64) - @truncate(u6, value); - } - - /// NR22 - pub fn setNr22(self: *Self, value: u8) void { - self.envelope.raw = value; - if (!self.isDacEnabled()) self.enabled = false; - } - - /// NR23, NR24 - pub fn getSoundCntH(self: *const Self) u16 { - return self.freq.raw & 0x4000; - } - - /// NR23, NR24 - pub fn setSoundCntH(self: *Self, fs: *const FrameSequencer, value: u16) void { - self.setNr23(@truncate(u8, value)); - self.setNr24(fs, @truncate(u8, value >> 8)); - } - - /// NR23 - pub fn setNr23(self: *Self, byte: u8) void { - self.freq.raw = (self.freq.raw & 0xFF00) | byte; - } - - /// NR24 - pub fn setNr24(self: *Self, fs: *const FrameSequencer, byte: u8) void { - var new: io.Frequency = .{ .raw = (@as(u16, byte) << 8) | (self.freq.raw & 0xFF) }; - - if (new.trigger.read()) { - self.enabled = true; - - if (self.len_dev.timer == 0) { - self.len_dev.timer = - if (!fs.isLengthNext() and new.length_enable.read()) 63 else 64; - } - - self.square.reloadTimer(.Ch2, self.freq.frequency.read()); - - // Reload Envelope period and timer - self.env_dev.timer = self.envelope.period.read(); - if (fs.isEnvelopeNext() and self.env_dev.timer != 0b111) self.env_dev.timer += 1; - - self.env_dev.vol = self.envelope.init_vol.read(); - - self.enabled = self.isDacEnabled(); - } - - self.square.updateToneLength(fs, self, new); - self.freq = new; - } - - fn isDacEnabled(self: *const Self) bool { - return self.envelope.raw & 0xF8 != 0; - } -}; - -const Wave = struct { - const Self = @This(); - - /// Write-only - /// NR30 - select: io.WaveSelect, - /// NR31 - length: u8, - /// NR32 - vol: io.WaveVolume, - /// NR33, NR34 - freq: io.Frequency, - - /// Length Functionarlity - len_dev: LengthDevice, - wave_dev: WaveDevice, - - enabled: bool, - sample: i8, - - fn init(sched: *Scheduler) Self { - return .{ - .select = .{ .raw = 0 }, - .vol = .{ .raw = 0 }, - .freq = .{ .raw = 0 }, - .length = 0, - - .len_dev = LengthDevice.init(), - .wave_dev = WaveDevice.init(sched), - .enabled = false, - .sample = 0, - }; - } - - fn reset(self: *Self) void { - self.select.raw = 0; - self.length = 0; - self.vol.raw = 0; - self.freq.raw = 0; - - self.sample = 0; - self.enabled = false; - } - - pub fn tickLength(self: *Self) void { - self.len_dev.tick(self.freq.length_enable.read(), &self.enabled); - } - - /// NR30, NR31, NR32 - fn setSoundCnt(self: *Self, value: u32) void { - self.setSoundCntL(@truncate(u8, value)); - self.setSoundCntH(@truncate(u16, value >> 16)); - } - - /// NR30 - pub fn setSoundCntL(self: *Self, value: u8) void { - self.select.raw = value; - if (!self.select.enabled.read()) self.enabled = false; - } - - /// NR31, NR32 - fn getSoundCntH(self: *const Self) u16 { - return @as(u16, self.length & 0xE0) << 8; - } - - /// NR31, NR32 - pub fn setSoundCntH(self: *Self, value: u16) void { - self.setNr31(@truncate(u8, value)); - self.vol.raw = (@truncate(u8, value >> 8)); - } - - /// NR31 - pub fn setNr31(self: *Self, len: u8) void { - self.length = len; - self.len_dev.timer = 256 - @as(u9, len); - } - - /// NR33, NR34 - pub fn setSoundCntX(self: *Self, fs: *const FrameSequencer, value: u16) void { - self.setNr33(@truncate(u8, value)); - self.setNr34(fs, @truncate(u8, value >> 8)); - } - - /// NR33 - pub fn setNr33(self: *Self, byte: u8) void { - self.freq.raw = (self.freq.raw & 0xFF00) | byte; - } - - /// NR34 - pub fn setNr34(self: *Self, fs: *const FrameSequencer, byte: u8) void { - var new: io.Frequency = .{ .raw = (@as(u16, byte) << 8) | (self.freq.raw & 0xFF) }; - - if (new.trigger.read()) { - self.enabled = true; - - if (self.len_dev.timer == 0) { - self.len_dev.timer = - if (!fs.isLengthNext() and new.length_enable.read()) 255 else 256; - } - - // Update The Frequency Timer - self.wave_dev.reloadTimer(self.freq.frequency.read()); - self.wave_dev.offset = 0; - - self.enabled = self.select.enabled.read(); - } - - self.wave_dev.updateLength(fs, self, new); - self.freq = new; - } - - pub fn channelTimerOverflow(self: *Self, late: u64) void { - self.wave_dev.handleTimerOverflow(self.freq, self.select, late); - - self.sample = 0; - if (!self.select.enabled.read()) return; - // Convert unsigned 4-bit wave sample to signed 8-bit sample - self.sample = (2 * @as(i8, self.wave_dev.sample(self.select)) - 15) >> self.wave_dev.shift(self.vol); - } - - fn amplitude(self: *const Self) i16 { - return @as(i16, self.sample); - } -}; - -const Noise = struct { - const Self = @This(); - - /// Write-only - /// NR41 - len: u6, - /// NR42 - envelope: io.Envelope, - /// NR43 - poly: io.PolyCounter, - /// NR44 - cnt: io.NoiseControl, - - /// Length Functionarlity - len_dev: LengthDevice, - - /// Envelope Functionality - env_dev: EnvelopeDevice, - - // Linear Feedback Shift Register - lfsr: Lfsr, - - enabled: bool, - sample: i8, - - fn init(sched: *Scheduler) Self { - return .{ - .len = 0, - .envelope = .{ .raw = 0 }, - .poly = .{ .raw = 0 }, - .cnt = .{ .raw = 0 }, - .enabled = false, - - .len_dev = LengthDevice.init(), - .env_dev = EnvelopeDevice.init(), - .lfsr = Lfsr.init(sched), - - .sample = 0, - }; - } - - fn reset(self: *Self) void { - self.len = 0; - self.envelope.raw = 0; - self.poly.raw = 0; - self.cnt.raw = 0; - - self.sample = 0; - self.enabled = false; - } - - pub fn tickLength(self: *Self) void { - self.len_dev.tick(self.cnt.length_enable.read(), &self.enabled); - } - - pub fn tickEnvelope(self: *Self) void { - self.env_dev.tick(self.envelope); - } - - /// NR41, NR42 - pub fn getSoundCntL(self: *const Self) u16 { - return @as(u16, self.envelope.raw) << 8; - } - - /// NR41, NR42 - pub fn setSoundCntL(self: *Self, value: u16) void { - self.setNr41(@truncate(u8, value)); - self.setNr42(@truncate(u8, value >> 8)); - } - - /// NR41 - pub fn setNr41(self: *Self, len: u8) void { - self.len = @truncate(u6, len); - self.len_dev.timer = @as(u7, 64) - @truncate(u6, len); - } - - /// NR42 - pub fn setNr42(self: *Self, value: u8) void { - self.envelope.raw = value; - if (!self.isDacEnabled()) self.enabled = false; - } - - /// NR43, NR44 - pub fn getSoundCntH(self: *const Self) u16 { - return @as(u16, self.poly.raw & 0x40) << 8 | self.cnt.raw; - } - - /// NR43, NR44 - pub fn setSoundCntH(self: *Self, fs: *const FrameSequencer, value: u16) void { - self.poly.raw = @truncate(u8, value); - self.setNr44(fs, @truncate(u8, value >> 8)); - } - - /// NR44 - pub fn setNr44(self: *Self, fs: *const FrameSequencer, byte: u8) void { - var new: io.NoiseControl = .{ .raw = byte }; - - if (new.trigger.read()) { - self.enabled = true; - - if (self.len_dev.timer == 0) { - self.len_dev.timer = - if (!fs.isLengthNext() and new.length_enable.read()) 63 else 64; - } - - // Update The Frequency Timer - self.lfsr.reloadTimer(self.poly); - self.lfsr.shift = 0x7FFF; - - // Update Envelope and Volume - self.env_dev.timer = self.envelope.period.read(); - if (fs.isEnvelopeNext() and self.env_dev.timer != 0b111) self.env_dev.timer += 1; - - self.env_dev.vol = self.envelope.init_vol.read(); - - self.enabled = self.isDacEnabled(); - } - - self.lfsr.updateLength(fs, self, new); - self.cnt = new; - } - - pub fn channelTimerOverflow(self: *Self, late: u64) void { - self.lfsr.handleTimerOverflow(self.poly, late); - - self.sample = 0; - if (!self.isDacEnabled()) return; - self.sample = if (self.enabled) self.lfsr.sample() * @as(i8, self.env_dev.vol) else 0; - } - - fn amplitude(self: *const Self) i16 { - return @as(i16, self.sample); - } - - fn isDacEnabled(self: *const Self) bool { - return self.envelope.raw & 0xF8 != 0x00; - } -}; - pub fn DmaSound(comptime kind: DmaSoundKind) type { return struct { const Self = @This(); @@ -1086,7 +457,7 @@ const DmaSoundKind = enum { B, }; -const FrameSequencer = struct { +pub const FrameSequencer = struct { const Self = @This(); step: u3, @@ -1099,294 +470,11 @@ const FrameSequencer = struct { self.step +%= 1; } - fn isLengthNext(self: *const Self) bool { + pub fn isLengthNext(self: *const Self) bool { return (self.step +% 1) & 1 == 0; // Steps, 0, 2, 4, and 6 clock length } - fn isEnvelopeNext(self: *const Self) bool { + pub fn isEnvelopeNext(self: *const Self) bool { return (self.step +% 1) == 7; } }; - -const LengthDevice = struct { - const Self = @This(); - - timer: u9, - - pub fn init() Self { - return .{ .timer = 0 }; - } - - fn tick(self: *Self, length_enable: bool, ch_enabled: *bool) void { - if (length_enable) { - if (self.timer == 0) return; - self.timer -= 1; - - // By returning early if timer == 0, this is only - // true if timer == 0 because of the decrement we just did - if (self.timer == 0) ch_enabled.* = false; - } - } -}; - -const EnvelopeDevice = struct { - const Self = @This(); - - /// Period Timer - timer: u3, - /// Current Volume - vol: u4, - - pub fn init() Self { - return .{ .timer = 0, .vol = 0 }; - } - - pub fn tick(self: *Self, cnt: io.Envelope) void { - if (cnt.period.read() != 0) { - if (self.timer != 0) self.timer -= 1; - - if (self.timer == 0) { - self.timer = cnt.period.read(); - - if (cnt.direction.read()) { - if (self.vol < 0xF) self.vol += 1; - } else { - if (self.vol > 0x0) self.vol -= 1; - } - } - } - } -}; - -const WaveDevice = struct { - const Self = @This(); - const wave_len = 0x20; - const tickInterval: u64 = (1 << 24) / (1 << 22); - - buf: [wave_len]u8, - timer: u16, - offset: u12, - - sched: *Scheduler, - - pub fn init(sched: *Scheduler) Self { - return .{ - .buf = [_]u8{0x00} ** wave_len, - .timer = 0, - .offset = 0, - .sched = sched, - }; - } - - fn reloadTimer(self: *Self, value: u11) void { - self.sched.removeScheduledEvent(.{ .ApuChannel = 2 }); - - self.timer = (@as(u16, 2048) - value) * 2; - self.sched.push(.{ .ApuChannel = 2 }, @as(u64, self.timer) * tickInterval); - } - - fn handleTimerOverflow(self: *Self, cnt_freq: io.Frequency, cnt_sel: io.WaveSelect, late: u64) void { - if (cnt_sel.dimension.read()) { - self.offset = (self.offset + 1) % 0x40; // 0x20 bytes (both banks), which contain 2 samples each - } else { - self.offset = (self.offset + 1) % 0x20; // 0x10 bytes, which contain 2 samples each - } - - self.timer = (@as(u16, 2048) - cnt_freq.frequency.read()) * 2; - self.sched.push(.{ .ApuChannel = 2 }, @as(u64, self.timer) * tickInterval -| late); - } - - fn sample(self: *const Self, cnt: io.WaveSelect) u4 { - const base = if (cnt.bank.read()) @as(u32, 0x10) else 0; - - const value = self.buf[base + self.offset / 2]; - return if (self.offset & 1 == 0) @truncate(u4, value >> 4) else @truncate(u4, value); - } - - fn shift(_: *const Self, cnt: io.WaveVolume) u2 { - return switch (cnt.kind.read()) { - 0b00 => 3, // Mute / Zero - 0b01 => 0, // 100% Volume - 0b10 => 1, // 50% Volume - 0b11 => 2, // 25% Volume - }; - } - - fn updateLength(_: *Self, fs: *const FrameSequencer, ch3: *Wave, new: io.Frequency) void { - // Write to NRx4 when FS's next step is not one that clocks the length counter - if (!fs.isLengthNext()) { - // If length_enable was disabled but is now enabled and length timer is not 0 already, - // decrement the length timer - - if (!ch3.freq.length_enable.read() and new.length_enable.read() and ch3.len_dev.timer != 0) { - ch3.len_dev.timer -= 1; - - // If Length Timer is now 0 and trigger is clear, disable the channel - if (ch3.len_dev.timer == 0 and !new.trigger.read()) ch3.enabled = false; - } - } - } - - pub fn write(self: *Self, comptime T: type, cnt: io.WaveSelect, addr: u32, value: T) void { - // TODO: Handle writes when Channel 3 is disabled - const base = if (!cnt.bank.read()) @as(u32, 0x10) else 0; // Write to the Opposite Bank in Use - - const i = base + addr - 0x0400_0090; - std.mem.writeIntSliceLittle(T, self.buf[i..][0..@sizeOf(T)], value); - } - - fn read(self: *const Self, comptime T: type, cnt: io.WaveSelect, addr: u32) T { - // TODO: Handle reads when Channel 3 is disabled - const base = if (!cnt.bank.read()) @as(u32, 0x10) else 0; // Read from the Opposite Bank in Use - - const i = base + addr - 0x0400_0090; - return std.mem.readIntSliceLittle(T, self.buf[i..][0..@sizeOf(T)]); - } -}; - -const SquareWave = struct { - const Self = @This(); - const tickInterval: u64 = (1 << 24) / (1 << 22); - - pos: u3, - sched: *Scheduler, - timer: u16, - - pub fn init(sched: *Scheduler) Self { - return .{ - .timer = 0, - .pos = 0, - .sched = sched, - }; - } - - const ChannelKind = enum { Ch1, Ch2 }; - - fn updateToneSweepLength(_: *Self, fs: *const FrameSequencer, ch1: *ToneSweep, new: io.Frequency) void { - // Write to NRx4 when FS's next step is not one that clocks the length counter - if (!fs.isLengthNext()) { - // If length_enable was disabled but is now enabled and length timer is not 0 already, - // decrement the length timer - - if (!ch1.freq.length_enable.read() and new.length_enable.read() and ch1.len_dev.timer != 0) { - ch1.len_dev.timer -= 1; - - // If Length Timer is now 0 and trigger is clear, disable the channel - if (ch1.len_dev.timer == 0 and !new.trigger.read()) ch1.enabled = false; - } - } - } - - fn updateToneLength(_: *Self, fs: *const FrameSequencer, ch2: *Tone, new: io.Frequency) void { - // Write to NRx4 when FS's next step is not one that clocks the length counter - if (!fs.isLengthNext()) { - // If length_enable was disabled but is now enabled and length timer is not 0 already, - // decrement the length timer - - if (!ch2.freq.length_enable.read() and new.length_enable.read() and ch2.len_dev.timer != 0) { - ch2.len_dev.timer -= 1; - - // If Length Timer is now 0 and trigger is clear, disable the channel - if (ch2.len_dev.timer == 0 and !new.trigger.read()) ch2.enabled = false; - } - } - } - - fn handleTimerOverflow(self: *Self, comptime kind: ChannelKind, cnt: io.Frequency, late: u64) void { - self.pos +%= 1; - - self.timer = (@as(u16, 2048) - cnt.frequency.read()) * 4; - self.sched.push(.{ .ApuChannel = if (kind == .Ch1) 0 else 1 }, @as(u64, self.timer) * tickInterval -| late); - } - - fn reloadTimer(self: *Self, comptime kind: ChannelKind, value: u11) void { - self.sched.removeScheduledEvent(.{ .ApuChannel = if (kind == .Ch1) 0 else 1 }); - - const tmp = (@as(u16, 2048) - value) * 4; // What Freq Timer should be assuming no weird behaviour - self.timer = (tmp & ~@as(u16, 0x3)) | self.timer & 0x3; // Keep the last two bits from the old timer; - - self.sched.push(.{ .ApuChannel = if (kind == .Ch1) 0 else 1 }, @as(u64, self.timer) * tickInterval); - } - - fn sample(self: *const Self, cnt: io.Duty) i8 { - const pattern = cnt.pattern.read(); - - const i = self.pos ^ 7; // index of 0 should get highest bit - const result = switch (pattern) { - 0b00 => @as(u8, 0b00000001) >> i, // 12.5% - 0b01 => @as(u8, 0b00000011) >> i, // 25% - 0b10 => @as(u8, 0b00001111) >> i, // 50% - 0b11 => @as(u8, 0b11111100) >> i, // 75% - }; - - return if (result & 1 == 1) 1 else -1; - } -}; - -// Linear Feedback Shift Register -const Lfsr = struct { - const Self = @This(); - const tickInterval: u64 = (1 << 24) / (1 << 22); - - shift: u15, - timer: u16, - - sched: *Scheduler, - - pub fn init(sched: *Scheduler) Self { - return .{ - .shift = 0, - .timer = 0, - .sched = sched, - }; - } - - fn sample(self: *const Self) i8 { - return if ((~self.shift & 1) == 1) 1 else -1; - } - - fn updateLength(_: *Self, fs: *const FrameSequencer, ch4: *Noise, new: io.NoiseControl) void { - // Write to NRx4 when FS's next step is not one that clocks the length counter - if (!fs.isLengthNext()) { - // If length_enable was disabled but is now enabled and length timer is not 0 already, - // decrement the length timer - - if (!ch4.cnt.length_enable.read() and new.length_enable.read() and ch4.len_dev.timer != 0) { - ch4.len_dev.timer -= 1; - - // If Length Timer is now 0 and trigger is clear, disable the channel - if (ch4.len_dev.timer == 0 and !new.trigger.read()) ch4.enabled = false; - } - } - } - - fn reloadTimer(self: *Self, poly: io.PolyCounter) void { - self.sched.removeScheduledEvent(.{ .ApuChannel = 3 }); - - const div = Self.divisor(poly.div_ratio.read()); - const timer = div << poly.shift.read(); - self.sched.push(.{ .ApuChannel = 3 }, @as(u64, timer) * tickInterval); - } - - fn handleTimerOverflow(self: *Self, poly: io.PolyCounter, late: u64) void { - // Obscure: "Using a noise channel clock shift of 14 or 15 - // results in the LFSR receiving no clocks." - if (poly.shift.read() >= 14) return; - - const div = Self.divisor(poly.div_ratio.read()); - const timer = div << poly.shift.read(); - - const tmp = (self.shift & 1) ^ ((self.shift & 2) >> 1); - self.shift = (self.shift >> 1) | (tmp << 14); - - if (poly.width.read()) - self.shift = (self.shift & ~@as(u15, 0x40)) | tmp << 6; - - self.sched.push(.{ .ApuChannel = 3 }, @as(u64, timer) * tickInterval -| late); - } - - fn divisor(code: u3) u16 { - if (code == 0) return 8; - return @as(u16, code) << 4; - } -}; diff --git a/src/core/apu/Noise.zig b/src/core/apu/Noise.zig new file mode 100644 index 0000000..6f816c5 --- /dev/null +++ b/src/core/apu/Noise.zig @@ -0,0 +1,144 @@ +const io = @import("../bus/io.zig"); + +const Scheduler = @import("../scheduler.zig").Scheduler; +const FrameSequencer = @import("../apu.zig").FrameSequencer; +const Envelope = @import("device/Envelope.zig"); +const Length = @import("device/Length.zig"); +const Lfsr = @import("signal/Lfsr.zig"); + +const Self = @This(); + +/// Write-only +/// NR41 +len: u6, +/// NR42 +envelope: io.Envelope, +/// NR43 +poly: io.PolyCounter, +/// NR44 +cnt: io.NoiseControl, + +/// Length Functionarlity +len_dev: Length, + +/// Envelope Functionality +env_dev: Envelope, + +// Linear Feedback Shift Register +lfsr: Lfsr, + +enabled: bool, +sample: i8, + +pub fn init(sched: *Scheduler) Self { + return .{ + .len = 0, + .envelope = .{ .raw = 0 }, + .poly = .{ .raw = 0 }, + .cnt = .{ .raw = 0 }, + .enabled = false, + + .len_dev = Length.create(), + .env_dev = Envelope.create(), + .lfsr = Lfsr.create(sched), + + .sample = 0, + }; +} + +pub fn reset(self: *Self) void { + self.len = 0; + self.envelope.raw = 0; + self.poly.raw = 0; + self.cnt.raw = 0; + + self.sample = 0; + self.enabled = false; +} + +pub fn tickLength(self: *Self) void { + self.len_dev.tick(self.cnt.length_enable.read(), &self.enabled); +} + +pub fn tickEnvelope(self: *Self) void { + self.env_dev.tick(self.envelope); +} + +/// NR41, NR42 +pub fn getSoundCntL(self: *const Self) u16 { + return @as(u16, self.envelope.raw) << 8; +} + +/// NR41, NR42 +pub fn setSoundCntL(self: *Self, value: u16) void { + self.setNr41(@truncate(u8, value)); + self.setNr42(@truncate(u8, value >> 8)); +} + +/// NR41 +pub fn setNr41(self: *Self, len: u8) void { + self.len = @truncate(u6, len); + self.len_dev.timer = @as(u7, 64) - @truncate(u6, len); +} + +/// NR42 +pub fn setNr42(self: *Self, value: u8) void { + self.envelope.raw = value; + if (!self.isDacEnabled()) self.enabled = false; +} + +/// NR43, NR44 +pub fn getSoundCntH(self: *const Self) u16 { + return @as(u16, self.poly.raw & 0x40) << 8 | self.cnt.raw; +} + +/// NR43, NR44 +pub fn setSoundCntH(self: *Self, fs: *const FrameSequencer, value: u16) void { + self.poly.raw = @truncate(u8, value); + self.setNr44(fs, @truncate(u8, value >> 8)); +} + +/// NR44 +pub fn setNr44(self: *Self, fs: *const FrameSequencer, byte: u8) void { + var new: io.NoiseControl = .{ .raw = byte }; + + if (new.trigger.read()) { + self.enabled = true; + + if (self.len_dev.timer == 0) { + self.len_dev.timer = + if (!fs.isLengthNext() and new.length_enable.read()) 63 else 64; + } + + // Update The Frequency Timer + self.lfsr.reload(self.poly); + self.lfsr.shift = 0x7FFF; + + // Update Envelope and Volume + self.env_dev.timer = self.envelope.period.read(); + if (fs.isEnvelopeNext() and self.env_dev.timer != 0b111) self.env_dev.timer += 1; + + self.env_dev.vol = self.envelope.init_vol.read(); + + self.enabled = self.isDacEnabled(); + } + + self.lfsr.updateLength(fs, self, new); + self.cnt = new; +} + +pub fn channelTimerOverflow(self: *Self, late: u64) void { + self.lfsr.onLfsrTimerExpire(self.poly, late); + + self.sample = 0; + if (!self.isDacEnabled()) return; + self.sample = if (self.enabled) self.lfsr.sample() * @as(i8, self.env_dev.vol) else 0; +} + +pub fn amplitude(self: *const Self) i16 { + return @as(i16, self.sample); +} + +fn isDacEnabled(self: *const Self) bool { + return self.envelope.raw & 0xF8 != 0x00; +} diff --git a/src/core/apu/Tone.zig b/src/core/apu/Tone.zig new file mode 100644 index 0000000..a60ce3b --- /dev/null +++ b/src/core/apu/Tone.zig @@ -0,0 +1,140 @@ +const io = @import("../bus/io.zig"); + +const Scheduler = @import("../scheduler.zig").Scheduler; +const FrameSequencer = @import("../apu.zig").FrameSequencer; +const Length = @import("device/Length.zig"); +const Envelope = @import("device/Envelope.zig"); +const Square = @import("signal/Square.zig"); + +const Self = @This(); + +/// NR21 +duty: io.Duty, +/// NR22 +envelope: io.Envelope, +/// NR23, NR24 +freq: io.Frequency, + +/// Length Functionarlity +len_dev: Length, +/// Envelope Functionality +env_dev: Envelope, +/// FrequencyTimer Functionality +square: Square, + +enabled: bool, +sample: i8, + +pub fn init(sched: *Scheduler) Self { + return .{ + .duty = .{ .raw = 0 }, + .envelope = .{ .raw = 0 }, + .freq = .{ .raw = 0 }, + .enabled = false, + + .square = Square.init(sched), + .len_dev = Length.create(), + .env_dev = Envelope.create(), + + .sample = 0, + }; +} + +pub fn reset(self: *Self) void { + self.duty.raw = 0; + self.envelope.raw = 0; + self.freq.raw = 0; + + self.sample = 0; + self.enabled = false; +} + +pub fn tickLength(self: *Self) void { + self.len_dev.tick(self.freq.length_enable.read(), &self.enabled); +} + +pub fn tickEnvelope(self: *Self) void { + self.env_dev.tick(self.envelope); +} + +pub fn channelTimerOverflow(self: *Self, late: u64) void { + self.square.onSquareTimerExpire(Self, self.freq, late); + + self.sample = 0; + if (!self.isDacEnabled()) return; + self.sample = if (self.enabled) self.square.sample(self.duty) * @as(i8, self.env_dev.vol) else 0; +} + +pub fn amplitude(self: *const Self) i16 { + return @as(i16, self.sample); +} + +/// NR21, NR22 +pub fn getSoundCntL(self: *const Self) u16 { + return @as(u16, self.envelope.raw) << 8 | (self.duty.raw & 0xC0); +} + +/// NR21, NR22 +pub fn setSoundCntL(self: *Self, value: u16) void { + self.setNr21(@truncate(u8, value)); + self.setNr22(@truncate(u8, value >> 8)); +} + +/// NR21 +pub fn setNr21(self: *Self, value: u8) void { + self.duty.raw = value; + self.len_dev.timer = @as(u7, 64) - @truncate(u6, value); +} + +/// NR22 +pub fn setNr22(self: *Self, value: u8) void { + self.envelope.raw = value; + if (!self.isDacEnabled()) self.enabled = false; +} + +/// NR23, NR24 +pub fn getSoundCntH(self: *const Self) u16 { + return self.freq.raw & 0x4000; +} + +/// NR23, NR24 +pub fn setSoundCntH(self: *Self, fs: *const FrameSequencer, value: u16) void { + self.setNr23(@truncate(u8, value)); + self.setNr24(fs, @truncate(u8, value >> 8)); +} + +/// NR23 +pub fn setNr23(self: *Self, byte: u8) void { + self.freq.raw = (self.freq.raw & 0xFF00) | byte; +} + +/// NR24 +pub fn setNr24(self: *Self, fs: *const FrameSequencer, byte: u8) void { + var new: io.Frequency = .{ .raw = (@as(u16, byte) << 8) | (self.freq.raw & 0xFF) }; + + if (new.trigger.read()) { + self.enabled = true; + + if (self.len_dev.timer == 0) { + self.len_dev.timer = + if (!fs.isLengthNext() and new.length_enable.read()) 63 else 64; + } + + self.square.reload(Self, self.freq.frequency.read()); + + // Reload Envelope period and timer + self.env_dev.timer = self.envelope.period.read(); + if (fs.isEnvelopeNext() and self.env_dev.timer != 0b111) self.env_dev.timer += 1; + + self.env_dev.vol = self.envelope.init_vol.read(); + + self.enabled = self.isDacEnabled(); + } + + self.square.updateLength(Self, fs, self, new); + self.freq = new; +} + +fn isDacEnabled(self: *const Self) bool { + return self.envelope.raw & 0xF8 != 0; +} diff --git a/src/core/apu/ToneSweep.zig b/src/core/apu/ToneSweep.zig new file mode 100644 index 0000000..3502327 --- /dev/null +++ b/src/core/apu/ToneSweep.zig @@ -0,0 +1,190 @@ +// const std = @import("std"); +const io = @import("../bus/io.zig"); + +const Scheduler = @import("../scheduler.zig").Scheduler; +const FrameSequencer = @import("../apu.zig").FrameSequencer; +const Length = @import("device/Length.zig"); +const Envelope = @import("device/Envelope.zig"); +const Sweep = @import("device/Sweep.zig"); +const Square = @import("signal/Square.zig"); + +const Self = @This(); + +/// NR10 +sweep: io.Sweep, +/// NR11 +duty: io.Duty, +/// NR12 +envelope: io.Envelope, +/// NR13, NR14 +freq: io.Frequency, + +/// Length Functionality +len_dev: Length, +/// Sweep Functionality +sweep_dev: Sweep, +/// Envelope Functionality +env_dev: Envelope, +/// Frequency Timer Functionality +square: Square, +enabled: bool, + +sample: i8, + +pub fn init(sched: *Scheduler) Self { + return .{ + .sweep = .{ .raw = 0 }, + .duty = .{ .raw = 0 }, + .envelope = .{ .raw = 0 }, + .freq = .{ .raw = 0 }, + .sample = 0, + .enabled = false, + + .square = Square.init(sched), + .len_dev = Length.create(), + .sweep_dev = Sweep.create(), + .env_dev = Envelope.create(), + }; +} + +pub fn reset(self: *Self) void { + self.sweep.raw = 0; + self.sweep_dev.calc_performed = false; + + self.duty.raw = 0; + self.envelope.raw = 0; + self.freq.raw = 0; + + self.sample = 0; + self.enabled = false; +} + +pub fn tickSweep(self: *Self) void { + self.sweep_dev.tick(self); +} + +pub fn tickLength(self: *Self) void { + self.len_dev.tick(self.freq.length_enable.read(), &self.enabled); +} + +pub fn tickEnvelope(self: *Self) void { + self.env_dev.tick(self.envelope); +} + +pub fn channelTimerOverflow(self: *Self, late: u64) void { + self.square.onSquareTimerExpire(Self, self.freq, late); + + self.sample = 0; + if (!self.isDacEnabled()) return; + self.sample = if (self.enabled) self.square.sample(self.duty) * @as(i8, self.env_dev.vol) else 0; +} + +pub fn amplitude(self: *const Self) i16 { + return @as(i16, self.sample); +} + +/// NR10, NR11, NR12 +pub fn setSoundCnt(self: *Self, value: u32) void { + self.setSoundCntL(@truncate(u8, value)); + self.setSoundCntH(@truncate(u16, value >> 16)); +} + +/// NR10 +pub fn getSoundCntL(self: *const Self) u8 { + return self.sweep.raw & 0x7F; +} + +/// NR10 +pub fn setSoundCntL(self: *Self, value: u8) void { + const new = io.Sweep{ .raw = value }; + + if (self.sweep.direction.read() and !new.direction.read()) { + // Sweep Negate bit has been cleared + // If At least 1 Sweep Calculation has been made since + // the last trigger, the channel is immediately disabled + + if (self.sweep_dev.calc_performed) self.enabled = false; + } + + self.sweep.raw = value; +} + +/// NR11, NR12 +pub fn getSoundCntH(self: *const Self) u16 { + return @as(u16, self.envelope.raw) << 8 | (self.duty.raw & 0xC0); +} + +/// NR11, NR12 +pub fn setSoundCntH(self: *Self, value: u16) void { + self.setNr11(@truncate(u8, value)); + self.setNr12(@truncate(u8, value >> 8)); +} + +/// NR11 +pub fn setNr11(self: *Self, value: u8) void { + self.duty.raw = value; + self.len_dev.timer = @as(u7, 64) - @truncate(u6, value); +} + +/// NR12 +pub fn setNr12(self: *Self, value: u8) void { + self.envelope.raw = value; + if (!self.isDacEnabled()) self.enabled = false; +} + +/// NR13, NR14 +pub fn getSoundCntX(self: *const Self) u16 { + return self.freq.raw & 0x4000; +} + +/// NR13, NR14 +pub fn setSoundCntX(self: *Self, fs: *const FrameSequencer, value: u16) void { + self.setNr13(@truncate(u8, value)); + self.setNr14(fs, @truncate(u8, value >> 8)); +} + +/// NR13 +pub fn setNr13(self: *Self, byte: u8) void { + self.freq.raw = (self.freq.raw & 0xFF00) | byte; +} + +/// NR14 +pub fn setNr14(self: *Self, fs: *const FrameSequencer, byte: u8) void { + var new: io.Frequency = .{ .raw = (@as(u16, byte) << 8) | (self.freq.raw & 0xFF) }; + + if (new.trigger.read()) { + self.enabled = true; + + if (self.len_dev.timer == 0) { + self.len_dev.timer = + if (!fs.isLengthNext() and new.length_enable.read()) 63 else 64; + } + + self.square.reload(Self, self.freq.frequency.read()); + + // Reload Envelope period and timer + self.env_dev.timer = self.envelope.period.read(); + if (fs.isEnvelopeNext() and self.env_dev.timer != 0b111) self.env_dev.timer += 1; + + self.env_dev.vol = self.envelope.init_vol.read(); + + // Sweep Trigger Behaviour + const sw_period = self.sweep.period.read(); + const sw_shift = self.sweep.shift.read(); + + self.sweep_dev.calc_performed = false; + self.sweep_dev.shadow = self.freq.frequency.read(); + self.sweep_dev.timer = if (sw_period == 0) 8 else sw_period; + self.sweep_dev.enabled = sw_period != 0 or sw_shift != 0; + if (sw_shift != 0) _ = self.sweep_dev.calculate(self.sweep, &self.enabled); + + self.enabled = self.isDacEnabled(); + } + + self.square.updateLength(Self, fs, self, new); + self.freq = new; +} + +fn isDacEnabled(self: *const Self) bool { + return self.envelope.raw & 0xF8 != 0; +} diff --git a/src/core/apu/Wave.zig b/src/core/apu/Wave.zig new file mode 100644 index 0000000..ec38960 --- /dev/null +++ b/src/core/apu/Wave.zig @@ -0,0 +1,129 @@ +const io = @import("../bus/io.zig"); + +const Scheduler = @import("../scheduler.zig").Scheduler; +const FrameSequencer = @import("../apu.zig").FrameSequencer; +const Length = @import("device/Length.zig"); +const Wave = @import("signal/Wave.zig"); + +const Self = @This(); + +/// Write-only +/// NR30 +select: io.WaveSelect, +/// NR31 +length: u8, +/// NR32 +vol: io.WaveVolume, +/// NR33, NR34 +freq: io.Frequency, + +/// Length Functionarlity +len_dev: Length, +wave_dev: Wave, + +enabled: bool, +sample: i8, + +pub fn init(sched: *Scheduler) Self { + return .{ + .select = .{ .raw = 0 }, + .vol = .{ .raw = 0 }, + .freq = .{ .raw = 0 }, + .length = 0, + + .len_dev = Length.create(), + .wave_dev = Wave.init(sched), + .enabled = false, + .sample = 0, + }; +} + +pub fn reset(self: *Self) void { + self.select.raw = 0; + self.length = 0; + self.vol.raw = 0; + self.freq.raw = 0; + + self.sample = 0; + self.enabled = false; +} + +pub fn tickLength(self: *Self) void { + self.len_dev.tick(self.freq.length_enable.read(), &self.enabled); +} + +/// NR30, NR31, NR32 +pub fn setSoundCnt(self: *Self, value: u32) void { + self.setSoundCntL(@truncate(u8, value)); + self.setSoundCntH(@truncate(u16, value >> 16)); +} + +/// NR30 +pub fn setSoundCntL(self: *Self, value: u8) void { + self.select.raw = value; + if (!self.select.enabled.read()) self.enabled = false; +} + +/// NR31, NR32 +pub fn getSoundCntH(self: *const Self) u16 { + return @as(u16, self.length & 0xE0) << 8; +} + +/// NR31, NR32 +pub fn setSoundCntH(self: *Self, value: u16) void { + self.setNr31(@truncate(u8, value)); + self.vol.raw = (@truncate(u8, value >> 8)); +} + +/// NR31 +pub fn setNr31(self: *Self, len: u8) void { + self.length = len; + self.len_dev.timer = 256 - @as(u9, len); +} + +/// NR33, NR34 +pub fn setSoundCntX(self: *Self, fs: *const FrameSequencer, value: u16) void { + self.setNr33(@truncate(u8, value)); + self.setNr34(fs, @truncate(u8, value >> 8)); +} + +/// NR33 +pub fn setNr33(self: *Self, byte: u8) void { + self.freq.raw = (self.freq.raw & 0xFF00) | byte; +} + +/// NR34 +pub fn setNr34(self: *Self, fs: *const FrameSequencer, byte: u8) void { + var new: io.Frequency = .{ .raw = (@as(u16, byte) << 8) | (self.freq.raw & 0xFF) }; + + if (new.trigger.read()) { + self.enabled = true; + + if (self.len_dev.timer == 0) { + self.len_dev.timer = + if (!fs.isLengthNext() and new.length_enable.read()) 255 else 256; + } + + // Update The Frequency Timer + self.wave_dev.reload(self.freq.frequency.read()); + self.wave_dev.offset = 0; + + self.enabled = self.select.enabled.read(); + } + + self.wave_dev.updateLength(fs, self, new); + self.freq = new; +} + +pub fn channelTimerOverflow(self: *Self, late: u64) void { + self.wave_dev.onWaveTimerExpire(self.freq, self.select, late); + + self.sample = 0; + if (!self.select.enabled.read()) return; + // Convert unsigned 4-bit wave sample to signed 8-bit sample + self.sample = (2 * @as(i8, self.wave_dev.sample(self.select)) - 15) >> self.wave_dev.shift(self.vol); +} + +pub fn amplitude(self: *const Self) i16 { + return @as(i16, self.sample); +} diff --git a/src/core/apu/device/Envelope.zig b/src/core/apu/device/Envelope.zig new file mode 100644 index 0000000..2fb3a8c --- /dev/null +++ b/src/core/apu/device/Envelope.zig @@ -0,0 +1,28 @@ +const io = @import("../../bus/io.zig"); + +const Self = @This(); + +/// Period Timer +timer: u3, +/// Current Volume +vol: u4, + +pub fn create() Self { + return .{ .timer = 0, .vol = 0 }; +} + +pub fn tick(self: *Self, nrx2: io.Envelope) void { + if (nrx2.period.read() != 0) { + if (self.timer != 0) self.timer -= 1; + + if (self.timer == 0) { + self.timer = nrx2.period.read(); + + if (nrx2.direction.read()) { + if (self.vol < 0xF) self.vol += 1; + } else { + if (self.vol > 0x0) self.vol -= 1; + } + } + } +} diff --git a/src/core/apu/device/Length.zig b/src/core/apu/device/Length.zig new file mode 100644 index 0000000..4e7138b --- /dev/null +++ b/src/core/apu/device/Length.zig @@ -0,0 +1,18 @@ +const Self = @This(); + +timer: u9, + +pub fn create() Self { + return .{ .timer = 0 }; +} + +pub fn tick(self: *Self, enabled: bool, ch_enable: *bool) void { + if (enabled) { + if (self.timer == 0) return; + self.timer -= 1; + + // By returning early if timer == 0, this is only + // true if timer == 0 because of the decrement we just did + if (self.timer == 0) ch_enable.* = false; + } +} diff --git a/src/core/apu/device/Sweep.zig b/src/core/apu/device/Sweep.zig new file mode 100644 index 0000000..b326a62 --- /dev/null +++ b/src/core/apu/device/Sweep.zig @@ -0,0 +1,52 @@ +const io = @import("../../bus/io.zig"); +const ToneSweep = @import("../ToneSweep.zig"); + +const Self = @This(); + +timer: u8, +enabled: bool, +shadow: u11, + +calc_performed: bool, + +pub fn create() Self { + return .{ + .timer = 0, + .enabled = false, + .shadow = 0, + .calc_performed = false, + }; +} + +pub fn tick(self: *Self, ch1: *ToneSweep) void { + if (self.timer != 0) self.timer -= 1; + + if (self.timer == 0) { + const period = ch1.sweep.period.read(); + self.timer = if (period == 0) 8 else period; + if (!self.calc_performed) self.calc_performed = true; + + if (self.enabled and period != 0) { + const new_freq = self.calculate(ch1.sweep, &ch1.enabled); + + if (new_freq <= 0x7FF and ch1.sweep.shift.read() != 0) { + ch1.freq.frequency.write(@truncate(u11, new_freq)); + self.shadow = @truncate(u11, new_freq); + + _ = self.calculate(ch1.sweep, &ch1.enabled); + } + } + } +} + +/// Calculates the Sweep Frequency +pub fn calculate(self: *Self, sweep: io.Sweep, ch_enable: *bool) u12 { + const shadow = @as(u12, self.shadow); + const shadow_shifted = shadow >> sweep.shift.read(); + const decrease = sweep.direction.read(); + + const freq = if (decrease) shadow - shadow_shifted else shadow + shadow_shifted; + if (freq > 0x7FF) ch_enable.* = false; + + return freq; +} diff --git a/src/core/apu/signal/Lfsr.zig b/src/core/apu/signal/Lfsr.zig new file mode 100644 index 0000000..ffa5fe8 --- /dev/null +++ b/src/core/apu/signal/Lfsr.zig @@ -0,0 +1,75 @@ +const io = @import("../../bus/io.zig"); + +/// Linear Feedback Shift Register +const Scheduler = @import("../../scheduler.zig").Scheduler; +const FrameSequencer = @import("../../apu.zig").FrameSequencer; +const Noise = @import("../Noise.zig"); + +const Self = @This(); +pub const interval: u64 = (1 << 24) / (1 << 22); + +shift: u15, +timer: u16, + +sched: *Scheduler, + +pub fn create(sched: *Scheduler) Self { + return .{ + .shift = 0, + .timer = 0, + .sched = sched, + }; +} + +pub fn sample(self: *const Self) i8 { + return if ((~self.shift & 1) == 1) 1 else -1; +} + +/// Update the sate of the Channel Length TImer +pub fn updateLength(_: *Self, fs: *const FrameSequencer, ch4: *Noise, nr44: io.NoiseControl) void { + // Write to NRx4 when FS's next step is not one that clocks the length counter + if (!fs.isLengthNext()) { + // If length_enable was disabled but is now enabled and length timer is not 0 already, + // decrement the length timer + + if (!ch4.cnt.length_enable.read() and nr44.length_enable.read() and ch4.len_dev.timer != 0) { + ch4.len_dev.timer -= 1; + + // If Length Timer is now 0 and trigger is clear, disable the channel + if (ch4.len_dev.timer == 0 and !nr44.trigger.read()) ch4.enabled = false; + } + } +} + +/// Reload LFSR Timer +pub fn reload(self: *Self, poly: io.PolyCounter) void { + self.sched.removeScheduledEvent(.{ .ApuChannel = 3 }); + + const div = Self.divisor(poly.div_ratio.read()); + const timer = div << poly.shift.read(); + self.sched.push(.{ .ApuChannel = 3 }, @as(u64, timer) * interval); +} + +/// Scheduler Event Handler for LFSR Timer Expire +/// FIXME: This gets called a lot, clogging up the Scheduler +pub fn onLfsrTimerExpire(self: *Self, poly: io.PolyCounter, late: u64) void { + // Obscure: "Using a noise channel clock shift of 14 or 15 + // results in the LFSR receiving no clocks." + if (poly.shift.read() >= 14) return; + + const div = Self.divisor(poly.div_ratio.read()); + const timer = div << poly.shift.read(); + + const tmp = (self.shift & 1) ^ ((self.shift & 2) >> 1); + self.shift = (self.shift >> 1) | (tmp << 14); + + if (poly.width.read()) + self.shift = (self.shift & ~@as(u15, 0x40)) | tmp << 6; + + self.sched.push(.{ .ApuChannel = 3 }, @as(u64, timer) * interval -| late); +} + +fn divisor(code: u3) u16 { + if (code == 0) return 8; + return @as(u16, code) << 4; +} diff --git a/src/core/apu/signal/Square.zig b/src/core/apu/signal/Square.zig new file mode 100644 index 0000000..444dd39 --- /dev/null +++ b/src/core/apu/signal/Square.zig @@ -0,0 +1,75 @@ +const std = @import("std"); +const io = @import("../../bus/io.zig"); + +const Scheduler = @import("../../scheduler.zig").Scheduler; +const FrameSequencer = @import("../../apu.zig").FrameSequencer; +const ToneSweep = @import("../ToneSweep.zig"); +const Tone = @import("../Tone.zig"); + +const Self = @This(); +pub const interval: u64 = (1 << 24) / (1 << 22); + +pos: u3, +sched: *Scheduler, +timer: u16, + +pub fn init(sched: *Scheduler) Self { + return .{ + .timer = 0, + .pos = 0, + .sched = sched, + }; +} + +/// Updates the State of either Ch1 or Ch2's Length Timer +pub fn updateLength(_: *const Self, comptime T: type, fs: *const FrameSequencer, ch: *T, nrx34: io.Frequency) void { + comptime std.debug.assert(T == ToneSweep or T == Tone); + // Write to NRx4 when FS's next step is not one that clocks the length counter + if (!fs.isLengthNext()) { + // If length_enable was disabled but is now enabled and length timer is not 0 already, + // decrement the length timer + + if (!ch.freq.length_enable.read() and nrx34.length_enable.read() and ch.len_dev.timer != 0) { + ch.len_dev.timer -= 1; + + // If Length Timer is now 0 and trigger is clear, disable the channel + if (ch.len_dev.timer == 0 and !nrx34.trigger.read()) ch.enabled = false; + } + } +} + +/// Scheduler Event Handler for Square Synth Timer Expire +pub fn onSquareTimerExpire(self: *Self, comptime T: type, nrx34: io.Frequency, late: u64) void { + comptime std.debug.assert(T == ToneSweep or T == Tone); + self.pos +%= 1; + + self.timer = (@as(u16, 2048) - nrx34.frequency.read()) * 4; + self.sched.push(.{ .ApuChannel = if (T == ToneSweep) 0 else 1 }, @as(u64, self.timer) * interval -| late); +} + +/// Reload Square Wave Timer +pub fn reload(self: *Self, comptime T: type, value: u11) void { + comptime std.debug.assert(T == ToneSweep or T == Tone); + const channel = if (T == ToneSweep) 0 else 1; + + self.sched.removeScheduledEvent(.{ .ApuChannel = channel }); + + const tmp = (@as(u16, 2048) - value) * 4; // What Freq Timer should be assuming no weird behaviour + self.timer = (tmp & ~@as(u16, 0x3)) | self.timer & 0x3; // Keep the last two bits from the old timer; + + self.sched.push(.{ .ApuChannel = channel }, @as(u64, self.timer) * interval); +} + +pub fn sample(self: *const Self, nrx1: io.Duty) i8 { + const pattern = nrx1.pattern.read(); + + const i = self.pos ^ 7; // index of 0 should get highest bit + const result = switch (pattern) { + 0b00 => @as(u8, 0b00000001) >> i, // 12.5% + 0b01 => @as(u8, 0b00000011) >> i, // 25% + 0b10 => @as(u8, 0b00001111) >> i, // 50% + 0b11 => @as(u8, 0b11111100) >> i, // 75% + }; + + return if (result & 1 == 1) 1 else -1; +} diff --git a/src/core/apu/signal/Wave.zig b/src/core/apu/signal/Wave.zig new file mode 100644 index 0000000..dc0f852 --- /dev/null +++ b/src/core/apu/signal/Wave.zig @@ -0,0 +1,95 @@ +const std = @import("std"); +const io = @import("../../bus/io.zig"); + +const Scheduler = @import("../../scheduler.zig").Scheduler; +const FrameSequencer = @import("../../apu.zig").FrameSequencer; +const Wave = @import("../Wave.zig"); + +const buf_len = 0x20; +pub const interval: u64 = (1 << 24) / (1 << 22); +const Self = @This(); + +buf: [buf_len]u8, +timer: u16, +offset: u12, + +sched: *Scheduler, + +pub fn read(self: *const Self, comptime T: type, nr30: io.WaveSelect, addr: u32) T { + // TODO: Handle reads when Channel 3 is disabled + const base = if (!nr30.bank.read()) @as(u32, 0x10) else 0; // Read from the Opposite Bank in Use + + const i = base + addr - 0x0400_0090; + return std.mem.readIntSliceLittle(T, self.buf[i..][0..@sizeOf(T)]); +} + +pub fn write(self: *Self, comptime T: type, nr30: io.WaveSelect, addr: u32, value: T) void { + // TODO: Handle writes when Channel 3 is disabled + const base = if (!nr30.bank.read()) @as(u32, 0x10) else 0; // Write to the Opposite Bank in Use + + const i = base + addr - 0x0400_0090; + std.mem.writeIntSliceLittle(T, self.buf[i..][0..@sizeOf(T)], value); +} + +pub fn init(sched: *Scheduler) Self { + return .{ + .buf = [_]u8{0x00} ** buf_len, + .timer = 0, + .offset = 0, + .sched = sched, + }; +} + +/// Reload internal Wave Timer +pub fn reload(self: *Self, value: u11) void { + self.sched.removeScheduledEvent(.{ .ApuChannel = 2 }); + + self.timer = (@as(u16, 2048) - value) * 2; + self.sched.push(.{ .ApuChannel = 2 }, @as(u64, self.timer) * interval); +} + +/// Scheduler Event Handler +pub fn onWaveTimerExpire(self: *Self, nrx34: io.Frequency, nr30: io.WaveSelect, late: u64) void { + if (nr30.dimension.read()) { + self.offset = (self.offset + 1) % 0x40; // 0x20 bytes (both banks), which contain 2 samples each + } else { + self.offset = (self.offset + 1) % 0x20; // 0x10 bytes, which contain 2 samples each + } + + self.timer = (@as(u16, 2048) - nrx34.frequency.read()) * 2; + self.sched.push(.{ .ApuChannel = 2 }, @as(u64, self.timer) * interval -| late); +} + +/// Generate Sample from Wave Synth +pub fn sample(self: *const Self, nr30: io.WaveSelect) u4 { + const base = if (nr30.bank.read()) @as(u32, 0x10) else 0; + + const value = self.buf[base + self.offset / 2]; + return if (self.offset & 1 == 0) @truncate(u4, value >> 4) else @truncate(u4, value); +} + +/// TODO: Write comment +pub fn shift(_: *const Self, nr32: io.WaveVolume) u2 { + return switch (nr32.kind.read()) { + 0b00 => 3, // Mute / Zero + 0b01 => 0, // 100% Volume + 0b10 => 1, // 50% Volume + 0b11 => 2, // 25% Volume + }; +} + +/// Update state of Channel 3 Length Device +pub fn updateLength(_: *Self, fs: *const FrameSequencer, ch3: *Wave, nrx34: io.Frequency) void { + // Write to NRx4 when FS's next step is not one that clocks the length counter + if (!fs.isLengthNext()) { + // If length_enable was disabled but is now enabled and length timer is not 0 already, + // decrement the length timer + + if (!ch3.freq.length_enable.read() and nrx34.length_enable.read() and ch3.len_dev.timer != 0) { + ch3.len_dev.timer -= 1; + + // If Length Timer is now 0 and trigger is clear, disable the channel + if (ch3.len_dev.timer == 0 and !nrx34.trigger.read()) ch3.enabled = false; + } + } +}