2022-10-21 08:12:27 +00:00
|
|
|
const std = @import("std");
|
2022-10-21 08:12:33 +00:00
|
|
|
const SDL = @import("sdl2");
|
|
|
|
const io = @import("bus/io.zig");
|
2022-10-21 08:12:36 +00:00
|
|
|
|
2022-10-21 08:12:33 +00:00
|
|
|
const Arm7tdmi = @import("cpu.zig").Arm7tdmi;
|
2022-10-21 08:12:36 +00:00
|
|
|
const Scheduler = @import("scheduler.zig").Scheduler;
|
2022-10-21 08:12:27 +00:00
|
|
|
|
|
|
|
const SoundFifo = std.fifo.LinearFifo(u8, .{ .Static = 0x20 });
|
2022-10-21 08:12:33 +00:00
|
|
|
const AudioDeviceId = SDL.SDL_AudioDeviceID;
|
2022-10-21 08:12:27 +00:00
|
|
|
|
2022-10-21 08:12:33 +00:00
|
|
|
const intToBytes = @import("util.zig").intToBytes;
|
|
|
|
const log = std.log.scoped(.APU);
|
2022-10-21 08:12:27 +00:00
|
|
|
|
2022-10-21 08:12:36 +00:00
|
|
|
pub const host_sample_rate = 1 << 15;
|
2022-10-21 08:12:36 +00:00
|
|
|
|
2022-10-21 08:12:27 +00:00
|
|
|
pub const Apu = struct {
|
|
|
|
const Self = @This();
|
|
|
|
|
|
|
|
ch1: ToneSweep,
|
|
|
|
ch2: Tone,
|
|
|
|
ch3: Wave,
|
|
|
|
ch4: Noise,
|
2022-10-21 08:12:33 +00:00
|
|
|
chA: DmaSound(.A),
|
|
|
|
chB: DmaSound(.B),
|
2022-10-21 08:12:27 +00:00
|
|
|
|
|
|
|
bias: io.SoundBias,
|
|
|
|
ch_vol_cnt: io.ChannelVolumeControl,
|
|
|
|
dma_cnt: io.DmaSoundControl,
|
|
|
|
cnt: io.SoundControl,
|
|
|
|
|
2022-10-21 08:12:36 +00:00
|
|
|
sampling_cycle: u2,
|
|
|
|
stream: *SDL.SDL_AudioStream,
|
2022-10-21 08:12:36 +00:00
|
|
|
sched: *Scheduler,
|
2022-10-21 08:12:33 +00:00
|
|
|
|
2022-10-21 08:12:36 +00:00
|
|
|
pub fn init(sched: *Scheduler) Self {
|
|
|
|
const apu: Self = .{
|
2022-10-21 08:12:27 +00:00
|
|
|
.ch1 = ToneSweep.init(),
|
|
|
|
.ch2 = Tone.init(),
|
|
|
|
.ch3 = Wave.init(),
|
|
|
|
.ch4 = Noise.init(),
|
2022-10-21 08:12:33 +00:00
|
|
|
.chA = DmaSound(.A).init(),
|
|
|
|
.chB = DmaSound(.B).init(),
|
2022-10-21 08:12:27 +00:00
|
|
|
|
|
|
|
.ch_vol_cnt = .{ .raw = 0 },
|
|
|
|
.dma_cnt = .{ .raw = 0 },
|
|
|
|
.cnt = .{ .raw = 0 },
|
|
|
|
.bias = .{ .raw = 0x0200 },
|
2022-10-21 08:12:33 +00:00
|
|
|
|
2022-10-21 08:12:36 +00:00
|
|
|
.sampling_cycle = 0b00,
|
|
|
|
.stream = SDL.SDL_NewAudioStream(SDL.AUDIO_F32, 2, 1 << 15, SDL.AUDIO_F32, 2, host_sample_rate) orelse unreachable,
|
2022-10-21 08:12:36 +00:00
|
|
|
.sched = sched,
|
2022-10-21 08:12:27 +00:00
|
|
|
};
|
2022-10-21 08:12:36 +00:00
|
|
|
|
2022-10-21 08:12:36 +00:00
|
|
|
sched.push(.SampleAudio, sched.now() + apu.sampleTicks());
|
2022-10-21 08:12:27 +00:00
|
|
|
|
2022-10-21 08:12:36 +00:00
|
|
|
return apu;
|
2022-10-21 08:12:35 +00:00
|
|
|
}
|
|
|
|
|
2022-10-21 08:12:33 +00:00
|
|
|
pub fn setDmaCnt(self: *Self, value: u16) void {
|
|
|
|
const new: io.DmaSoundControl = .{ .raw = value };
|
|
|
|
|
|
|
|
// Reinitializing instead of resetting is fine because
|
|
|
|
// the FIFOs I'm using are stack allocated and 0x20 bytes big
|
2022-10-21 08:12:36 +00:00
|
|
|
if (new.chA_reset.read()) self.chA.fifo = SoundFifo.init();
|
|
|
|
if (new.chB_reset.read()) self.chB.fifo = SoundFifo.init();
|
2022-10-21 08:12:33 +00:00
|
|
|
|
|
|
|
self.dma_cnt = new;
|
|
|
|
}
|
|
|
|
|
2022-10-21 08:12:27 +00:00
|
|
|
pub fn setSoundCntX(self: *Self, value: bool) void {
|
|
|
|
self.cnt.apu_enable.write(value);
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn setSoundCntLLow(self: *Self, byte: u8) void {
|
|
|
|
self.ch_vol_cnt.raw = (self.ch_vol_cnt.raw & 0xFF00) | byte;
|
|
|
|
}
|
|
|
|
|
2022-10-21 08:12:30 +00:00
|
|
|
pub fn setSoundCntLHigh(self: *Self, byte: u8) void {
|
|
|
|
self.ch_vol_cnt.raw = @as(u16, byte) << 8 | (self.ch_vol_cnt.raw & 0xFF);
|
|
|
|
}
|
|
|
|
|
2022-10-21 08:12:27 +00:00
|
|
|
pub fn setBiasHigh(self: *Self, byte: u8) void {
|
|
|
|
self.bias.raw = (@as(u16, byte) << 8) | (self.bias.raw & 0xFF);
|
|
|
|
}
|
2022-10-21 08:12:33 +00:00
|
|
|
|
2022-10-21 08:12:36 +00:00
|
|
|
pub fn sampleAudio(self: *Self, late: u64) void {
|
|
|
|
const chA = if (self.dma_cnt.chA_vol.read()) self.chA.amplitude() else self.chA.amplitude() / 2;
|
|
|
|
const chA_left = if (self.dma_cnt.chA_left.read()) chA else 0;
|
|
|
|
const chA_right = if (self.dma_cnt.chA_right.read()) chA else 0;
|
|
|
|
|
|
|
|
const chB = if (self.dma_cnt.chB_vol.read()) self.chB.amplitude() else self.chB.amplitude() / 2;
|
|
|
|
const chB_left = if (self.dma_cnt.chB_left.read()) chB else 0;
|
|
|
|
const chB_right = if (self.dma_cnt.chB_right.read()) chB else 0;
|
|
|
|
|
|
|
|
const left = (chA_left + chB_left) / 2;
|
|
|
|
const right = (chA_right + chB_right) / 2;
|
|
|
|
|
2022-10-21 08:12:36 +00:00
|
|
|
if (self.sampling_cycle != self.bias.sampling_cycle.read()) {
|
|
|
|
log.warn("Sampling Cycle changed from {} to {}", .{ self.sampling_cycle, self.bias.sampling_cycle.read() });
|
|
|
|
|
|
|
|
// Sample Rate Changed, Create a new Resampler since i can't figure out how to change
|
|
|
|
// the parameters of the old one
|
|
|
|
const old = self.stream;
|
|
|
|
defer SDL.SDL_FreeAudioStream(old);
|
|
|
|
|
|
|
|
self.sampling_cycle = self.bias.sampling_cycle.read();
|
|
|
|
self.stream = SDL.SDL_NewAudioStream(SDL.AUDIO_F32, 2, @intCast(c_int, self.sampleRate()), SDL.AUDIO_F32, 2, host_sample_rate) orelse unreachable;
|
|
|
|
}
|
|
|
|
|
|
|
|
while (SDL.SDL_AudioStreamAvailable(self.stream) > (@sizeOf(f32) * 2 * 0x800)) {}
|
|
|
|
|
|
|
|
_ = SDL.SDL_AudioStreamPut(self.stream, &[2]f32{ left, right }, 2 * @sizeOf(f32));
|
|
|
|
self.sched.push(.SampleAudio, self.sched.now() + self.sampleTicks() - late);
|
|
|
|
}
|
|
|
|
|
|
|
|
inline fn sampleTicks(self: *const Self) u64 {
|
|
|
|
return (1 << 24) / self.sampleRate();
|
|
|
|
}
|
2022-10-21 08:12:36 +00:00
|
|
|
|
2022-10-21 08:12:36 +00:00
|
|
|
inline fn sampleRate(self: *const Self) u64 {
|
|
|
|
return @as(u64, 1) << (15 + @as(u6, self.bias.sampling_cycle.read()));
|
2022-10-21 08:12:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
pub fn handleTimerOverflow(self: *Self, cpu: *Arm7tdmi, tim_id: u3) void {
|
2022-10-21 08:12:33 +00:00
|
|
|
if (!self.cnt.apu_enable.read()) return;
|
|
|
|
|
2022-10-21 08:12:36 +00:00
|
|
|
if (@boolToInt(self.dma_cnt.chA_timer.read()) == tim_id) {
|
|
|
|
self.chA.updateSample();
|
|
|
|
if (self.chA.len() <= 15) cpu.bus.dma._1.enableSoundDma(0x0400_00A0);
|
|
|
|
}
|
2022-10-21 08:12:33 +00:00
|
|
|
|
2022-10-21 08:12:36 +00:00
|
|
|
if (@boolToInt(self.dma_cnt.chB_timer.read()) == tim_id) {
|
|
|
|
self.chB.updateSample();
|
|
|
|
if (self.chB.len() <= 15) cpu.bus.dma._2.enableSoundDma(0x0400_00A4);
|
|
|
|
}
|
2022-10-21 08:12:33 +00:00
|
|
|
}
|
2022-10-21 08:12:27 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
const ToneSweep = struct {
|
|
|
|
const Self = @This();
|
|
|
|
|
2022-10-21 08:12:30 +00:00
|
|
|
/// NR10
|
2022-10-21 08:12:27 +00:00
|
|
|
sweep: io.Sweep,
|
2022-10-21 08:12:30 +00:00
|
|
|
/// NR11
|
2022-10-21 08:12:27 +00:00
|
|
|
duty: io.Duty,
|
2022-10-21 08:12:30 +00:00
|
|
|
/// NR12
|
2022-10-21 08:12:27 +00:00
|
|
|
envelope: io.Envelope,
|
2022-10-21 08:12:30 +00:00
|
|
|
/// NR13, NR14
|
2022-10-21 08:12:27 +00:00
|
|
|
freq: io.Frequency,
|
|
|
|
|
|
|
|
fn init() Self {
|
|
|
|
return .{
|
|
|
|
.sweep = .{ .raw = 0 },
|
|
|
|
.duty = .{ .raw = 0 },
|
|
|
|
.envelope = .{ .raw = 0 },
|
|
|
|
.freq = .{ .raw = 0 },
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2022-10-21 08:12:30 +00:00
|
|
|
pub fn setFreqLow(self: *Self, byte: u8) void {
|
|
|
|
self.freq.raw = (self.freq.raw & 0xFF00) | byte;
|
|
|
|
}
|
|
|
|
|
2022-10-21 08:12:27 +00:00
|
|
|
pub fn setFreqHigh(self: *Self, byte: u8) void {
|
|
|
|
self.freq.raw = (@as(u16, byte) << 8) | (self.freq.raw & 0xFF);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
const Tone = struct {
|
|
|
|
const Self = @This();
|
|
|
|
|
2022-10-21 08:12:30 +00:00
|
|
|
/// NR21
|
2022-10-21 08:12:27 +00:00
|
|
|
duty: io.Duty,
|
2022-10-21 08:12:30 +00:00
|
|
|
/// NR22
|
2022-10-21 08:12:27 +00:00
|
|
|
envelope: io.Envelope,
|
2022-10-21 08:12:30 +00:00
|
|
|
/// NR23, NR24
|
2022-10-21 08:12:27 +00:00
|
|
|
freq: io.Frequency,
|
|
|
|
|
|
|
|
fn init() Self {
|
|
|
|
return .{
|
|
|
|
.duty = .{ .raw = 0 },
|
|
|
|
.envelope = .{ .raw = 0 },
|
|
|
|
.freq = .{ .raw = 0 },
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2022-10-21 08:12:30 +00:00
|
|
|
pub fn setFreqLow(self: *Self, byte: u8) void {
|
|
|
|
self.freq.raw = (self.freq.raw & 0xFF00) | byte;
|
|
|
|
}
|
|
|
|
|
2022-10-21 08:12:27 +00:00
|
|
|
pub fn setFreqHigh(self: *Self, byte: u8) void {
|
2022-10-21 08:12:30 +00:00
|
|
|
self.freq.raw = @as(u16, byte) << 8 | (self.freq.raw & 0xFF);
|
2022-10-21 08:12:27 +00:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
const Wave = struct {
|
|
|
|
const Self = @This();
|
|
|
|
|
|
|
|
/// Write-only
|
2022-10-21 08:12:30 +00:00
|
|
|
/// NR30
|
2022-10-21 08:12:27 +00:00
|
|
|
select: io.WaveSelect,
|
2022-10-21 08:12:32 +00:00
|
|
|
/// NR31
|
2022-10-21 08:12:27 +00:00
|
|
|
length: u8,
|
2022-10-21 08:12:30 +00:00
|
|
|
/// NR32
|
2022-10-21 08:12:27 +00:00
|
|
|
vol: io.WaveVolume,
|
2022-10-21 08:12:30 +00:00
|
|
|
/// NR33, NR34
|
2022-10-21 08:12:27 +00:00
|
|
|
freq: io.Frequency,
|
|
|
|
|
|
|
|
fn init() Self {
|
|
|
|
return .{
|
|
|
|
.select = .{ .raw = 0 },
|
|
|
|
.vol = .{ .raw = 0 },
|
|
|
|
.freq = .{ .raw = 0 },
|
|
|
|
.length = 0,
|
|
|
|
};
|
|
|
|
}
|
2022-10-21 08:12:30 +00:00
|
|
|
|
|
|
|
pub fn setFreqLow(self: *Self, byte: u8) void {
|
|
|
|
self.freq.raw = (self.freq.raw & 0xFF00) | byte;
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn setFreqHigh(self: *Self, byte: u8) void {
|
|
|
|
self.freq.raw = @as(u16, byte) << 8 | (self.freq.raw & 0xFF);
|
|
|
|
}
|
2022-10-21 08:12:27 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
const Noise = struct {
|
|
|
|
const Self = @This();
|
|
|
|
|
|
|
|
/// Write-only
|
|
|
|
/// NR41
|
|
|
|
len: u6,
|
2022-10-21 08:12:30 +00:00
|
|
|
/// NR42
|
2022-10-21 08:12:27 +00:00
|
|
|
envelope: io.Envelope,
|
2022-10-21 08:12:30 +00:00
|
|
|
/// NR43
|
2022-10-21 08:12:27 +00:00
|
|
|
poly: io.PolyCounter,
|
2022-10-21 08:12:30 +00:00
|
|
|
/// NR44
|
2022-10-21 08:12:27 +00:00
|
|
|
cnt: io.NoiseControl,
|
|
|
|
|
|
|
|
fn init() Self {
|
|
|
|
return .{
|
|
|
|
.len = 0,
|
|
|
|
.envelope = .{ .raw = 0 },
|
|
|
|
.poly = .{ .raw = 0 },
|
|
|
|
.cnt = .{ .raw = 0 },
|
|
|
|
};
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2022-10-21 08:12:33 +00:00
|
|
|
pub fn DmaSound(comptime kind: DmaSoundKind) type {
|
|
|
|
return struct {
|
|
|
|
const Self = @This();
|
2022-10-21 08:12:27 +00:00
|
|
|
|
2022-10-21 08:12:33 +00:00
|
|
|
fifo: SoundFifo,
|
|
|
|
kind: DmaSoundKind,
|
2022-10-21 08:12:36 +00:00
|
|
|
sample: i8,
|
2022-10-21 08:12:33 +00:00
|
|
|
|
|
|
|
fn init() Self {
|
2022-10-21 08:12:36 +00:00
|
|
|
return .{
|
|
|
|
.fifo = SoundFifo.init(),
|
|
|
|
.kind = kind,
|
|
|
|
.sample = 0,
|
|
|
|
};
|
2022-10-21 08:12:33 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
pub fn push(self: *Self, value: u32) void {
|
|
|
|
self.fifo.write(&intToBytes(u32, value)) catch {};
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn len(self: *const Self) usize {
|
|
|
|
return self.fifo.readableLength();
|
|
|
|
}
|
|
|
|
|
2022-10-21 08:12:36 +00:00
|
|
|
pub fn updateSample(self: *Self) void {
|
|
|
|
if (self.fifo.readItem()) |sample| self.sample = @bitCast(i8, sample);
|
|
|
|
}
|
2022-10-21 08:12:33 +00:00
|
|
|
|
2022-10-21 08:12:36 +00:00
|
|
|
pub fn amplitude(self: *const Self) f32 {
|
|
|
|
return @intToFloat(f32, self.sample) / 127.5 - (1 / 255);
|
2022-10-21 08:12:33 +00:00
|
|
|
}
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
const DmaSoundKind = enum {
|
|
|
|
A,
|
|
|
|
B,
|
2022-10-21 08:12:27 +00:00
|
|
|
};
|