style(apu): split apu.zig into multiple files + refactor
This commit is contained in:
parent
b5d8a65e69
commit
dc7cad9691
938
src/core/apu.zig
938
src/core/apu.zig
|
@ -3,11 +3,16 @@ const SDL = @import("sdl2");
|
||||||
const io = @import("bus/io.zig");
|
const io = @import("bus/io.zig");
|
||||||
const util = @import("../util.zig");
|
const util = @import("../util.zig");
|
||||||
|
|
||||||
|
const AudioDeviceId = SDL.SDL_AudioDeviceID;
|
||||||
|
|
||||||
const Arm7tdmi = @import("cpu.zig").Arm7tdmi;
|
const Arm7tdmi = @import("cpu.zig").Arm7tdmi;
|
||||||
const Scheduler = @import("scheduler.zig").Scheduler;
|
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 SoundFifo = std.fifo.LinearFifo(u8, .{ .Static = 0x20 });
|
||||||
const AudioDeviceId = SDL.SDL_AudioDeviceID;
|
|
||||||
|
|
||||||
const intToBytes = @import("../util.zig").intToBytes;
|
const intToBytes = @import("../util.zig").intToBytes;
|
||||||
const log = std.log.scoped(.APU);
|
const log = std.log.scoped(.APU);
|
||||||
|
@ -188,10 +193,10 @@ pub const Apu = struct {
|
||||||
};
|
};
|
||||||
|
|
||||||
sched.push(.SampleAudio, apu.sampleTicks());
|
sched.push(.SampleAudio, apu.sampleTicks());
|
||||||
sched.push(.{ .ApuChannel = 0 }, SquareWave.tickInterval); // Channel 1
|
sched.push(.{ .ApuChannel = 0 }, @import("apu/signal/Square.zig").interval);
|
||||||
sched.push(.{ .ApuChannel = 1 }, SquareWave.tickInterval); // Channel 2
|
sched.push(.{ .ApuChannel = 1 }, @import("apu/signal/Square.zig").interval);
|
||||||
sched.push(.{ .ApuChannel = 2 }, WaveDevice.tickInterval); // Channel 3
|
sched.push(.{ .ApuChannel = 2 }, @import("apu/signal/Wave.zig").interval);
|
||||||
sched.push(.{ .ApuChannel = 3 }, Lfsr.tickInterval); // Channel 4
|
sched.push(.{ .ApuChannel = 3 }, @import("apu/signal/Lfsr.zig").interval);
|
||||||
sched.push(.FrameSequencer, ((1 << 24) / 512));
|
sched.push(.FrameSequencer, ((1 << 24) / 512));
|
||||||
|
|
||||||
return apu;
|
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 {
|
pub fn DmaSound(comptime kind: DmaSoundKind) type {
|
||||||
return struct {
|
return struct {
|
||||||
const Self = @This();
|
const Self = @This();
|
||||||
|
@ -1086,7 +457,7 @@ const DmaSoundKind = enum {
|
||||||
B,
|
B,
|
||||||
};
|
};
|
||||||
|
|
||||||
const FrameSequencer = struct {
|
pub const FrameSequencer = struct {
|
||||||
const Self = @This();
|
const Self = @This();
|
||||||
|
|
||||||
step: u3,
|
step: u3,
|
||||||
|
@ -1099,294 +470,11 @@ const FrameSequencer = struct {
|
||||||
self.step +%= 1;
|
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
|
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;
|
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;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue