2022-03-15 11:08:07 +00:00
|
|
|
const std = @import("std");
|
2022-04-10 07:28:05 +00:00
|
|
|
const SDL = @import("sdl2");
|
|
|
|
const io = @import("bus/io.zig");
|
2022-09-19 19:07:19 +00:00
|
|
|
const util = @import("../util.zig");
|
2022-04-14 08:58:32 +00:00
|
|
|
|
2022-10-10 16:20:15 +00:00
|
|
|
const AudioDeviceId = SDL.SDL_AudioDeviceID;
|
|
|
|
|
2022-04-10 07:28:05 +00:00
|
|
|
const Arm7tdmi = @import("cpu.zig").Arm7tdmi;
|
2022-04-14 08:58:32 +00:00
|
|
|
const Scheduler = @import("scheduler.zig").Scheduler;
|
2022-10-10 16:20:15 +00:00
|
|
|
const ToneSweep = @import("apu/ToneSweep.zig");
|
|
|
|
const Tone = @import("apu/Tone.zig");
|
|
|
|
const Wave = @import("apu/Wave.zig");
|
|
|
|
const Noise = @import("apu/Noise.zig");
|
2022-03-15 11:08:07 +00:00
|
|
|
|
|
|
|
const SoundFifo = std.fifo.LinearFifo(u8, .{ .Static = 0x20 });
|
|
|
|
|
2022-09-19 19:07:19 +00:00
|
|
|
const intToBytes = @import("../util.zig").intToBytes;
|
2022-04-10 07:28:05 +00:00
|
|
|
const log = std.log.scoped(.APU);
|
2022-03-15 11:08:07 +00:00
|
|
|
|
2022-04-20 07:43:25 +00:00
|
|
|
pub const host_sample_rate = 1 << 15;
|
2022-04-14 08:58:32 +00:00
|
|
|
|
2022-09-13 02:01:41 +00:00
|
|
|
pub fn read(comptime T: type, apu: *const Apu, addr: u32) ?T {
|
2022-06-15 01:34:33 +00:00
|
|
|
const byte = @truncate(u8, addr);
|
|
|
|
|
|
|
|
return switch (T) {
|
|
|
|
u16 => switch (byte) {
|
2022-06-15 04:08:43 +00:00
|
|
|
0x60 => apu.ch1.getSoundCntL(),
|
2022-06-15 01:34:33 +00:00
|
|
|
0x62 => apu.ch1.getSoundCntH(),
|
2022-06-15 04:08:43 +00:00
|
|
|
0x64 => apu.ch1.getSoundCntX(),
|
2022-06-15 01:34:33 +00:00
|
|
|
0x68 => apu.ch2.getSoundCntL(),
|
2022-06-15 04:08:43 +00:00
|
|
|
0x6C => apu.ch2.getSoundCntH(),
|
2022-06-15 01:34:33 +00:00
|
|
|
|
2022-06-15 04:08:43 +00:00
|
|
|
0x70 => apu.ch3.select.raw & 0xE0, // SOUND3CNT_L
|
|
|
|
0x72 => apu.ch3.getSoundCntH(),
|
|
|
|
0x74 => apu.ch3.freq.raw & 0x4000, // SOUND3CNT_X
|
2022-06-15 01:34:33 +00:00
|
|
|
|
|
|
|
0x78 => apu.ch4.getSoundCntL(),
|
|
|
|
0x7C => apu.ch4.getSoundCntH(),
|
|
|
|
|
2022-06-15 04:08:43 +00:00
|
|
|
0x80 => apu.psg_cnt.raw & 0xFF77, // SOUNDCNT_L
|
|
|
|
0x82 => apu.dma_cnt.raw & 0x770F, // SOUNDCNT_H
|
|
|
|
0x84 => apu.getSoundCntX(),
|
2022-06-15 01:34:33 +00:00
|
|
|
0x88 => apu.bias.raw, // SOUNDBIAS
|
2022-06-15 04:08:43 +00:00
|
|
|
0x90...0x9F => apu.ch3.wave_dev.read(T, apu.ch3.select, addr),
|
2022-09-13 02:01:41 +00:00
|
|
|
else => util.io.read.undef(T, log, "Tried to perform a {} read to 0x{X:0>8}", .{ T, addr }),
|
2022-06-15 01:34:33 +00:00
|
|
|
},
|
|
|
|
u8 => switch (byte) {
|
2022-06-15 04:08:43 +00:00
|
|
|
0x60 => apu.ch1.getSoundCntL(), // NR10
|
2022-06-16 05:32:31 +00:00
|
|
|
0x62 => apu.ch1.duty.raw, // NR11
|
2022-06-15 01:34:33 +00:00
|
|
|
0x63 => apu.ch1.envelope.raw, // NR12
|
2022-06-16 05:32:31 +00:00
|
|
|
0x68 => apu.ch2.duty.raw, // NR21
|
2022-06-15 01:34:33 +00:00
|
|
|
0x69 => apu.ch2.envelope.raw, // NR22
|
|
|
|
0x73 => apu.ch3.vol.raw, // NR32
|
|
|
|
0x79 => apu.ch4.envelope.raw, // NR42
|
|
|
|
0x7C => apu.ch4.poly.raw, // NR43
|
|
|
|
0x81 => @truncate(u8, apu.psg_cnt.raw >> 8), // NR51
|
|
|
|
0x84 => apu.getSoundCntX(),
|
|
|
|
0x89 => @truncate(u8, apu.bias.raw >> 8), // SOUNDBIAS_H
|
2022-09-13 02:01:41 +00:00
|
|
|
else => util.io.read.undef(T, log, "Tried to perform a {} read to 0x{X:0>8}", .{ T, addr }),
|
2022-06-15 01:34:33 +00:00
|
|
|
},
|
2022-09-13 02:01:41 +00:00
|
|
|
u32 => util.io.read.undef(T, log, "Tried to perform a {} read to 0x{X:0>8}", .{ T, addr }),
|
2022-06-15 01:34:33 +00:00
|
|
|
else => @compileError("APU: Unsupported read width"),
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn write(comptime T: type, apu: *Apu, addr: u32, value: T) void {
|
|
|
|
const byte = @truncate(u8, addr);
|
|
|
|
|
|
|
|
switch (T) {
|
|
|
|
u32 => switch (byte) {
|
2022-06-18 20:35:52 +00:00
|
|
|
0x60 => apu.ch1.setSoundCnt(value),
|
|
|
|
0x64 => apu.ch1.setSoundCntX(&apu.fs, @truncate(u16, value)),
|
|
|
|
0x68 => apu.ch2.setSoundCntL(@truncate(u16, value)),
|
|
|
|
0x6C => apu.ch2.setSoundCntH(&apu.fs, @truncate(u16, value)),
|
|
|
|
0x70 => apu.ch3.setSoundCnt(value),
|
|
|
|
0x74 => apu.ch3.setSoundCntX(&apu.fs, @truncate(u16, value)),
|
|
|
|
0x78 => apu.ch4.setSoundCntL(@truncate(u16, value)),
|
|
|
|
0x7C => apu.ch4.setSoundCntH(&apu.fs, @truncate(u16, value)),
|
|
|
|
|
|
|
|
0x80 => apu.setSoundCnt(value),
|
2022-06-15 01:34:33 +00:00
|
|
|
// WAVE_RAM
|
|
|
|
0x90...0x9F => apu.ch3.wave_dev.write(T, apu.ch3.select, addr, value),
|
|
|
|
0xA0 => apu.chA.push(value), // FIFO_A
|
|
|
|
0xA4 => apu.chB.push(value), // FIFO_B
|
2022-09-13 02:01:41 +00:00
|
|
|
else => util.io.write.undef(log, "Tried to write 0x{X:0>8}{} to 0x{X:0>8}", .{ value, T, addr }),
|
2022-06-15 01:34:33 +00:00
|
|
|
},
|
|
|
|
u16 => switch (byte) {
|
2022-06-18 21:16:29 +00:00
|
|
|
0x60 => apu.ch1.setSoundCntL(@truncate(u8, value)), // SOUND1CNT_L
|
2022-06-15 01:34:33 +00:00
|
|
|
0x62 => apu.ch1.setSoundCntH(value),
|
|
|
|
0x64 => apu.ch1.setSoundCntX(&apu.fs, value),
|
|
|
|
|
|
|
|
0x68 => apu.ch2.setSoundCntL(value),
|
|
|
|
0x6C => apu.ch2.setSoundCntH(&apu.fs, value),
|
|
|
|
|
|
|
|
0x70 => apu.ch3.setSoundCntL(@truncate(u8, value)),
|
|
|
|
0x72 => apu.ch3.setSoundCntH(value),
|
|
|
|
0x74 => apu.ch3.setSoundCntX(&apu.fs, value),
|
|
|
|
|
|
|
|
0x78 => apu.ch4.setSoundCntL(value),
|
|
|
|
0x7C => apu.ch4.setSoundCntH(&apu.fs, value),
|
|
|
|
|
|
|
|
0x80 => apu.psg_cnt.raw = value, // SOUNDCNT_L
|
|
|
|
0x82 => apu.setSoundCntH(value),
|
|
|
|
0x84 => apu.setSoundCntX(value >> 7 & 1 == 1),
|
|
|
|
0x88 => apu.bias.raw = value, // SOUNDBIAS
|
|
|
|
// WAVE_RAM
|
|
|
|
0x90...0x9F => apu.ch3.wave_dev.write(T, apu.ch3.select, addr, value),
|
2022-09-13 02:01:41 +00:00
|
|
|
else => util.io.write.undef(log, "Tried to write 0x{X:0>4}{} to 0x{X:0>8}", .{ value, T, addr }),
|
2022-06-15 01:34:33 +00:00
|
|
|
},
|
|
|
|
u8 => switch (byte) {
|
2022-06-18 21:16:29 +00:00
|
|
|
0x60 => apu.ch1.setSoundCntL(value),
|
2022-06-15 01:34:33 +00:00
|
|
|
0x62 => apu.ch1.setNr11(value),
|
|
|
|
0x63 => apu.ch1.setNr12(value),
|
|
|
|
0x64 => apu.ch1.setNr13(value),
|
|
|
|
0x65 => apu.ch1.setNr14(&apu.fs, value),
|
|
|
|
|
|
|
|
0x68 => apu.ch2.setNr21(value),
|
|
|
|
0x69 => apu.ch2.setNr22(value),
|
|
|
|
0x6C => apu.ch2.setNr23(value),
|
|
|
|
0x6D => apu.ch2.setNr24(&apu.fs, value),
|
|
|
|
|
|
|
|
0x70 => apu.ch3.setSoundCntL(value), // NR30
|
|
|
|
0x72 => apu.ch3.setNr31(value),
|
|
|
|
0x73 => apu.ch3.vol.raw = value, // NR32
|
|
|
|
0x74 => apu.ch3.setNr33(value),
|
|
|
|
0x75 => apu.ch3.setNr34(&apu.fs, value),
|
|
|
|
|
|
|
|
0x78 => apu.ch4.setNr41(value),
|
|
|
|
0x79 => apu.ch4.setNr42(value),
|
|
|
|
0x7C => apu.ch4.poly.raw = value, // NR 43
|
|
|
|
0x7D => apu.ch4.setNr44(&apu.fs, value),
|
|
|
|
|
|
|
|
0x80 => apu.setNr50(value),
|
|
|
|
0x81 => apu.setNr51(value),
|
2022-06-16 05:32:31 +00:00
|
|
|
0x82 => apu.setSoundCntHL(value),
|
|
|
|
0x83 => apu.setSoundCntHH(value),
|
2022-06-15 01:34:33 +00:00
|
|
|
0x84 => apu.setSoundCntX(value >> 7 & 1 == 1), // NR52
|
|
|
|
0x89 => apu.setSoundBiasH(value),
|
|
|
|
0x90...0x9F => apu.ch3.wave_dev.write(T, apu.ch3.select, addr, value),
|
2022-09-13 02:01:41 +00:00
|
|
|
else => util.io.write.undef(log, "Tried to write 0x{X:0>2}{} to 0x{X:0>8}", .{ value, T, addr }),
|
2022-06-15 01:34:33 +00:00
|
|
|
},
|
|
|
|
else => @compileError("APU: Unsupported write width"),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-03-15 11:08:07 +00:00
|
|
|
pub const Apu = struct {
|
|
|
|
const Self = @This();
|
|
|
|
|
|
|
|
ch1: ToneSweep,
|
|
|
|
ch2: Tone,
|
|
|
|
ch3: Wave,
|
|
|
|
ch4: Noise,
|
2022-04-10 07:28:05 +00:00
|
|
|
chA: DmaSound(.A),
|
|
|
|
chB: DmaSound(.B),
|
2022-03-15 11:08:07 +00:00
|
|
|
|
|
|
|
bias: io.SoundBias,
|
2022-05-21 18:09:32 +00:00
|
|
|
/// NR50, NR51
|
2022-04-20 23:52:50 +00:00
|
|
|
psg_cnt: io.ChannelVolumeControl,
|
2022-03-15 11:08:07 +00:00
|
|
|
dma_cnt: io.DmaSoundControl,
|
|
|
|
cnt: io.SoundControl,
|
|
|
|
|
2022-04-20 07:43:25 +00:00
|
|
|
sampling_cycle: u2,
|
2022-05-01 03:17:34 +00:00
|
|
|
|
2022-04-20 07:43:25 +00:00
|
|
|
stream: *SDL.SDL_AudioStream,
|
2022-04-14 08:58:32 +00:00
|
|
|
sched: *Scheduler,
|
2022-04-10 07:28:05 +00:00
|
|
|
|
2022-04-20 12:39:12 +00:00
|
|
|
fs: FrameSequencer,
|
2022-04-20 23:52:50 +00:00
|
|
|
capacitor: f32,
|
2022-04-20 12:39:12 +00:00
|
|
|
|
2022-09-08 23:38:42 +00:00
|
|
|
is_buffer_full: bool,
|
|
|
|
|
2022-04-14 08:58:32 +00:00
|
|
|
pub fn init(sched: *Scheduler) Self {
|
|
|
|
const apu: Self = .{
|
2022-04-20 12:39:12 +00:00
|
|
|
.ch1 = ToneSweep.init(sched),
|
2022-04-21 00:33:46 +00:00
|
|
|
.ch2 = Tone.init(sched),
|
2022-04-21 03:21:55 +00:00
|
|
|
.ch3 = Wave.init(sched),
|
2022-04-21 05:40:02 +00:00
|
|
|
.ch4 = Noise.init(sched),
|
2022-04-10 07:28:05 +00:00
|
|
|
.chA = DmaSound(.A).init(),
|
|
|
|
.chB = DmaSound(.B).init(),
|
2022-03-15 11:08:07 +00:00
|
|
|
|
2022-04-20 23:52:50 +00:00
|
|
|
.psg_cnt = .{ .raw = 0 },
|
2022-03-15 11:08:07 +00:00
|
|
|
.dma_cnt = .{ .raw = 0 },
|
|
|
|
.cnt = .{ .raw = 0 },
|
|
|
|
.bias = .{ .raw = 0x0200 },
|
2022-04-10 07:28:05 +00:00
|
|
|
|
2022-04-20 07:43:25 +00:00
|
|
|
.sampling_cycle = 0b00,
|
2022-09-08 23:38:42 +00:00
|
|
|
.stream = SDL.SDL_NewAudioStream(SDL.AUDIO_U16, 2, 1 << 15, SDL.AUDIO_U16, 2, host_sample_rate).?,
|
2022-04-14 08:58:32 +00:00
|
|
|
.sched = sched,
|
2022-04-20 12:39:12 +00:00
|
|
|
|
2022-04-20 23:52:50 +00:00
|
|
|
.capacitor = 0,
|
2022-04-20 12:39:12 +00:00
|
|
|
.fs = FrameSequencer.init(),
|
2022-09-08 23:38:42 +00:00
|
|
|
.is_buffer_full = false,
|
2022-03-15 11:08:07 +00:00
|
|
|
};
|
2022-04-14 08:58:32 +00:00
|
|
|
|
2022-05-20 19:01:12 +00:00
|
|
|
sched.push(.SampleAudio, apu.sampleTicks());
|
2022-10-10 16:20:15 +00:00
|
|
|
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);
|
2022-05-20 19:01:12 +00:00
|
|
|
sched.push(.FrameSequencer, ((1 << 24) / 512));
|
2022-03-15 11:08:07 +00:00
|
|
|
|
2022-04-20 07:43:25 +00:00
|
|
|
return apu;
|
2022-04-14 02:21:25 +00:00
|
|
|
}
|
|
|
|
|
2022-04-20 23:52:50 +00:00
|
|
|
fn reset(self: *Self) void {
|
|
|
|
self.ch1.reset();
|
2022-04-21 03:21:55 +00:00
|
|
|
self.ch2.reset();
|
2022-04-21 05:40:02 +00:00
|
|
|
self.ch3.reset();
|
|
|
|
self.ch4.reset();
|
2022-04-20 23:52:50 +00:00
|
|
|
}
|
|
|
|
|
2022-06-18 20:35:52 +00:00
|
|
|
/// SOUNDCNT
|
|
|
|
fn setSoundCnt(self: *Self, value: u32) void {
|
|
|
|
self.psg_cnt.raw = @truncate(u16, value);
|
|
|
|
self.setSoundCntH(@truncate(u16, value >> 16));
|
|
|
|
}
|
|
|
|
|
2022-06-16 05:32:31 +00:00
|
|
|
/// SOUNDCNT_H_L
|
|
|
|
fn setSoundCntHL(self: *Self, value: u8) void {
|
|
|
|
const merged = (self.dma_cnt.raw & 0xFF00) | value;
|
|
|
|
self.setSoundCntH(merged);
|
|
|
|
}
|
|
|
|
|
|
|
|
/// SOUNDCNT_H_H
|
|
|
|
fn setSoundCntHH(self: *Self, value: u8) void {
|
|
|
|
const merged = (self.dma_cnt.raw & 0x00FF) | (@as(u16, value) << 8);
|
|
|
|
self.setSoundCntH(merged);
|
|
|
|
}
|
|
|
|
|
2022-06-18 20:35:52 +00:00
|
|
|
/// SOUNDCNT_H
|
2022-06-15 01:34:33 +00:00
|
|
|
pub fn setSoundCntH(self: *Self, value: u16) void {
|
2022-04-10 07:28:05 +00:00
|
|
|
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-04-14 08:58:32 +00:00
|
|
|
if (new.chA_reset.read()) self.chA.fifo = SoundFifo.init();
|
|
|
|
if (new.chB_reset.read()) self.chB.fifo = SoundFifo.init();
|
2022-04-10 07:28:05 +00:00
|
|
|
|
|
|
|
self.dma_cnt = new;
|
|
|
|
}
|
|
|
|
|
2022-04-20 12:39:12 +00:00
|
|
|
/// NR52
|
2022-03-15 11:08:07 +00:00
|
|
|
pub fn setSoundCntX(self: *Self, value: bool) void {
|
|
|
|
self.cnt.apu_enable.write(value);
|
2022-04-20 12:39:12 +00:00
|
|
|
|
|
|
|
if (value) {
|
|
|
|
self.fs.step = 0; // Reset Frame Sequencer
|
|
|
|
|
2022-04-21 03:21:55 +00:00
|
|
|
// Reset Square Wave Offsets
|
2022-04-20 23:52:50 +00:00
|
|
|
self.ch1.square.pos = 0;
|
2022-04-21 03:21:55 +00:00
|
|
|
self.ch2.square.pos = 0;
|
2022-04-20 12:39:12 +00:00
|
|
|
|
2022-04-21 03:21:55 +00:00
|
|
|
// Reset Wave Device Offsets
|
|
|
|
self.ch3.wave_dev.offset = 0;
|
2022-04-20 12:39:12 +00:00
|
|
|
} else {
|
2022-04-20 23:52:50 +00:00
|
|
|
self.reset();
|
2022-04-20 12:39:12 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// NR52
|
2022-06-15 01:34:33 +00:00
|
|
|
pub fn getSoundCntX(self: *const Self) u8 {
|
2022-04-25 13:01:34 +00:00
|
|
|
const apu_enable: u8 = @boolToInt(self.cnt.apu_enable.read());
|
2022-04-20 12:39:12 +00:00
|
|
|
|
2022-04-25 13:01:34 +00:00
|
|
|
const ch1_enable: u8 = @boolToInt(self.ch1.enabled);
|
|
|
|
const ch2_enable: u8 = @boolToInt(self.ch2.enabled);
|
|
|
|
const ch3_enable: u8 = @boolToInt(self.ch3.enabled);
|
|
|
|
const ch4_enable: u8 = @boolToInt(self.ch4.enabled);
|
2022-04-20 12:39:12 +00:00
|
|
|
|
|
|
|
return apu_enable << 7 | ch4_enable << 3 | ch3_enable << 2 | ch2_enable << 1 | ch1_enable;
|
2022-03-15 11:08:07 +00:00
|
|
|
}
|
|
|
|
|
2022-04-20 12:39:12 +00:00
|
|
|
/// NR50
|
2022-06-15 01:34:33 +00:00
|
|
|
pub fn setNr50(self: *Self, byte: u8) void {
|
2022-04-20 23:52:50 +00:00
|
|
|
self.psg_cnt.raw = (self.psg_cnt.raw & 0xFF00) | byte;
|
2022-03-15 11:08:07 +00:00
|
|
|
}
|
|
|
|
|
2022-04-20 12:39:12 +00:00
|
|
|
/// NR51
|
2022-06-15 01:34:33 +00:00
|
|
|
pub fn setNr51(self: *Self, byte: u8) void {
|
2022-04-20 23:52:50 +00:00
|
|
|
self.psg_cnt.raw = @as(u16, byte) << 8 | (self.psg_cnt.raw & 0xFF);
|
2022-03-28 22:40:47 +00:00
|
|
|
}
|
|
|
|
|
2022-06-15 01:34:33 +00:00
|
|
|
pub fn setSoundBiasH(self: *Self, byte: u8) void {
|
2022-03-15 11:08:07 +00:00
|
|
|
self.bias.raw = (@as(u16, byte) << 8) | (self.bias.raw & 0xFF);
|
|
|
|
}
|
2022-04-10 07:28:05 +00:00
|
|
|
|
2022-04-14 08:58:32 +00:00
|
|
|
pub fn sampleAudio(self: *Self, late: u64) void {
|
2022-09-08 23:38:42 +00:00
|
|
|
self.sched.push(.SampleAudio, self.sampleTicks() -| late);
|
|
|
|
|
|
|
|
// Whether the APU is busy or not is determined by the main loop in emu.zig
|
|
|
|
// This should only ever be true (because this side of the emu is single threaded)
|
|
|
|
// When audio sync is disaabled
|
|
|
|
if (self.is_buffer_full) return;
|
|
|
|
|
2022-05-22 11:18:29 +00:00
|
|
|
var left: i16 = 0;
|
|
|
|
var right: i16 = 0;
|
2022-04-14 08:58:32 +00:00
|
|
|
|
2022-05-22 11:18:29 +00:00
|
|
|
// SOUNDCNT_L Channel Enable flags
|
2022-05-17 13:49:03 +00:00
|
|
|
const ch_left: u4 = self.psg_cnt.ch_left.read();
|
|
|
|
const ch_right: u4 = self.psg_cnt.ch_right.read();
|
|
|
|
|
2022-05-22 11:18:29 +00:00
|
|
|
// Determine SOUNDCNT_H volume modifications
|
|
|
|
const gba_vol: u4 = switch (self.dma_cnt.ch_vol.read()) {
|
|
|
|
0b00 => 2,
|
|
|
|
0b01 => 1,
|
|
|
|
else => 0,
|
2022-05-17 13:49:03 +00:00
|
|
|
};
|
|
|
|
|
2022-05-22 11:18:29 +00:00
|
|
|
// Add all PSG channels together
|
2022-10-10 23:31:12 +00:00
|
|
|
left += if (ch_left & 1 == 1) @as(i16, self.ch1.sample) else 0;
|
|
|
|
left += if (ch_left >> 1 & 1 == 1) @as(i16, self.ch2.sample) else 0;
|
|
|
|
left += if (ch_left >> 2 & 1 == 1) @as(i16, self.ch3.sample) else 0;
|
|
|
|
left += if (ch_left >> 3 == 1) @as(i16, self.ch4.sample) else 0;
|
|
|
|
|
|
|
|
right += if (ch_right & 1 == 1) @as(i16, self.ch1.sample) else 0;
|
|
|
|
right += if (ch_right >> 1 & 1 == 1) @as(i16, self.ch2.sample) else 0;
|
|
|
|
right += if (ch_right >> 2 & 1 == 1) @as(i16, self.ch3.sample) else 0;
|
|
|
|
right += if (ch_right >> 3 == 1) @as(i16, self.ch4.sample) else 0;
|
2022-04-21 00:33:46 +00:00
|
|
|
|
2022-05-22 11:18:29 +00:00
|
|
|
// Multiply by master channel volume
|
|
|
|
left *= 1 + @as(i16, self.psg_cnt.left_vol.read());
|
|
|
|
right *= 1 + @as(i16, self.psg_cnt.right_vol.read());
|
2022-04-21 03:21:55 +00:00
|
|
|
|
2022-05-22 11:18:29 +00:00
|
|
|
// Apply GBA volume modifications to PSG Channels
|
|
|
|
left >>= gba_vol;
|
|
|
|
right >>= gba_vol;
|
2022-04-21 03:21:55 +00:00
|
|
|
|
2022-05-22 11:18:29 +00:00
|
|
|
const chA_sample = self.chA.amplitude() << if (self.dma_cnt.chA_vol.read()) @as(u4, 2) else 1;
|
|
|
|
const chB_sample = self.chB.amplitude() << if (self.dma_cnt.chB_vol.read()) @as(u4, 2) else 1;
|
2022-04-14 08:58:32 +00:00
|
|
|
|
2022-05-22 11:18:29 +00:00
|
|
|
left += if (self.dma_cnt.chA_left.read()) chA_sample else 0;
|
|
|
|
left += if (self.dma_cnt.chB_left.read()) chB_sample else 0;
|
2022-04-20 12:39:12 +00:00
|
|
|
|
2022-05-22 11:18:29 +00:00
|
|
|
right += if (self.dma_cnt.chA_right.read()) chA_sample else 0;
|
|
|
|
right += if (self.dma_cnt.chB_right.read()) chB_sample else 0;
|
2022-04-20 12:39:12 +00:00
|
|
|
|
2022-05-22 11:18:29 +00:00
|
|
|
// Add SOUNDBIAS
|
|
|
|
// FIXME: Is SOUNDBIAS 9-bit or 10-bit?
|
|
|
|
const bias = @as(i16, self.bias.level.read()) << 1;
|
|
|
|
left += bias;
|
|
|
|
right += bias;
|
|
|
|
|
2022-09-08 23:38:42 +00:00
|
|
|
const clamped_left = std.math.clamp(@bitCast(u16, left), std.math.minInt(u11), std.math.maxInt(u11));
|
|
|
|
const clamped_right = std.math.clamp(@bitCast(u16, right), std.math.minInt(u11), std.math.maxInt(u11));
|
2022-05-22 11:18:29 +00:00
|
|
|
|
|
|
|
// Extend to 16-bit signed audio samples
|
2022-09-08 23:38:42 +00:00
|
|
|
const ext_left = (clamped_left << 5) | (clamped_left >> 6);
|
|
|
|
const ext_right = (clamped_right << 5) | (clamped_right >> 6);
|
2022-04-29 15:28:35 +00:00
|
|
|
|
2022-09-08 23:38:42 +00:00
|
|
|
// FIXME: This rarely happens
|
|
|
|
if (self.sampling_cycle != self.bias.sampling_cycle.read()) self.replaceSDLResampler();
|
2022-04-20 07:43:25 +00:00
|
|
|
|
2022-09-08 23:38:42 +00:00
|
|
|
_ = SDL.SDL_AudioStreamPut(self.stream, &[2]u16{ ext_left, ext_right }, 2 * @sizeOf(u16));
|
|
|
|
}
|
2022-04-20 07:43:25 +00:00
|
|
|
|
2022-09-08 23:38:42 +00:00
|
|
|
fn replaceSDLResampler(self: *Self) void {
|
|
|
|
const sample_rate = Self.sampleRate(self.bias.sampling_cycle.read());
|
|
|
|
log.info("Sample Rate changed from {}Hz to {}Hz", .{ Self.sampleRate(self.sampling_cycle), sample_rate });
|
2022-04-20 07:43:25 +00:00
|
|
|
|
2022-09-08 23:38:42 +00:00
|
|
|
// Sampling Cycle (Sample Rate) changed, Craete a new SDL Audio Resampler
|
|
|
|
// FIXME: Replace SDL's Audio Resampler with either a custom or more reliable one
|
|
|
|
const old_stream = self.stream;
|
|
|
|
defer SDL.SDL_FreeAudioStream(old_stream);
|
|
|
|
|
|
|
|
self.sampling_cycle = self.bias.sampling_cycle.read();
|
|
|
|
self.stream = SDL.SDL_NewAudioStream(SDL.AUDIO_U16, 2, @intCast(c_int, sample_rate), SDL.AUDIO_U16, 2, host_sample_rate).?;
|
2022-04-20 07:43:25 +00:00
|
|
|
}
|
|
|
|
|
2022-04-21 13:15:52 +00:00
|
|
|
fn sampleTicks(self: *const Self) u64 {
|
2022-06-18 21:46:40 +00:00
|
|
|
return (1 << 24) / Self.sampleRate(self.bias.sampling_cycle.read());
|
2022-04-20 07:43:25 +00:00
|
|
|
}
|
2022-04-14 08:58:32 +00:00
|
|
|
|
2022-06-18 21:46:40 +00:00
|
|
|
fn sampleRate(cycle: u2) u64 {
|
|
|
|
return @as(u64, 1) << (15 + @as(u6, cycle));
|
2022-04-14 08:58:32 +00:00
|
|
|
}
|
|
|
|
|
2022-10-11 00:06:29 +00:00
|
|
|
pub fn onSequencerTick(self: *Self, late: u64) void {
|
2022-04-20 12:39:12 +00:00
|
|
|
self.fs.tick();
|
|
|
|
|
|
|
|
switch (self.fs.step) {
|
|
|
|
7 => self.tickEnvelopes(), // Clock Envelope
|
|
|
|
0, 4 => self.tickLengths(), // Clock Length
|
|
|
|
2, 6 => {
|
|
|
|
// Clock Length and Sweep
|
|
|
|
self.tickLengths();
|
|
|
|
self.ch1.tickSweep();
|
|
|
|
},
|
|
|
|
1, 3, 5 => {},
|
|
|
|
}
|
|
|
|
|
2022-05-20 19:01:12 +00:00
|
|
|
self.sched.push(.FrameSequencer, ((1 << 24) / 512) -| late);
|
2022-04-20 12:39:12 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
fn tickLengths(self: *Self) void {
|
|
|
|
self.ch1.tickLength();
|
|
|
|
self.ch2.tickLength();
|
|
|
|
self.ch3.tickLength();
|
|
|
|
self.ch4.tickLength();
|
|
|
|
}
|
|
|
|
|
|
|
|
fn tickEnvelopes(self: *Self) void {
|
|
|
|
self.ch1.tickEnvelope();
|
|
|
|
self.ch2.tickEnvelope();
|
|
|
|
self.ch4.tickEnvelope();
|
|
|
|
}
|
|
|
|
|
2022-10-11 00:06:29 +00:00
|
|
|
pub fn onDmaAudioSampleRequest(self: *Self, cpu: *Arm7tdmi, tim_id: u3) void {
|
2022-04-10 07:28:05 +00:00
|
|
|
if (!self.cnt.apu_enable.read()) return;
|
|
|
|
|
2022-04-14 08:58:32 +00:00
|
|
|
if (@boolToInt(self.dma_cnt.chA_timer.read()) == tim_id) {
|
|
|
|
self.chA.updateSample();
|
2022-10-10 13:47:52 +00:00
|
|
|
if (self.chA.len() <= 15) cpu.bus.dma[1].requestAudio(0x0400_00A0);
|
2022-04-14 08:58:32 +00:00
|
|
|
}
|
2022-04-10 07:28:05 +00:00
|
|
|
|
2022-04-14 08:58:32 +00:00
|
|
|
if (@boolToInt(self.dma_cnt.chB_timer.read()) == tim_id) {
|
|
|
|
self.chB.updateSample();
|
2022-10-10 13:47:52 +00:00
|
|
|
if (self.chB.len() <= 15) cpu.bus.dma[2].requestAudio(0x0400_00A4);
|
2022-04-14 08:58:32 +00:00
|
|
|
}
|
2022-04-10 07:28:05 +00:00
|
|
|
}
|
2022-03-15 11:08:07 +00:00
|
|
|
};
|
|
|
|
|
2022-04-10 07:28:05 +00:00
|
|
|
pub fn DmaSound(comptime kind: DmaSoundKind) type {
|
|
|
|
return struct {
|
|
|
|
const Self = @This();
|
2022-03-15 11:08:07 +00:00
|
|
|
|
2022-04-10 07:28:05 +00:00
|
|
|
fifo: SoundFifo,
|
|
|
|
kind: DmaSoundKind,
|
2022-04-14 08:58:32 +00:00
|
|
|
sample: i8,
|
2022-04-10 07:28:05 +00:00
|
|
|
|
|
|
|
fn init() Self {
|
2022-04-14 08:58:32 +00:00
|
|
|
return .{
|
|
|
|
.fifo = SoundFifo.init(),
|
|
|
|
.kind = kind,
|
|
|
|
.sample = 0,
|
|
|
|
};
|
2022-04-10 07:28:05 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
pub fn push(self: *Self, value: u32) void {
|
2022-05-20 19:01:12 +00:00
|
|
|
self.fifo.write(&intToBytes(u32, value)) catch |e| log.err("{} Error: {}", .{ kind, e });
|
2022-04-10 07:28:05 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
pub fn len(self: *const Self) usize {
|
|
|
|
return self.fifo.readableLength();
|
|
|
|
}
|
|
|
|
|
2022-04-14 08:58:32 +00:00
|
|
|
pub fn updateSample(self: *Self) void {
|
|
|
|
if (self.fifo.readItem()) |sample| self.sample = @bitCast(i8, sample);
|
|
|
|
}
|
2022-04-10 07:28:05 +00:00
|
|
|
|
2022-05-22 11:18:29 +00:00
|
|
|
pub fn amplitude(self: *const Self) i16 {
|
|
|
|
return @as(i16, self.sample);
|
2022-04-10 07:28:05 +00:00
|
|
|
}
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
const DmaSoundKind = enum {
|
|
|
|
A,
|
|
|
|
B,
|
2022-03-15 11:08:07 +00:00
|
|
|
};
|
2022-04-20 12:39:12 +00:00
|
|
|
|
2022-10-10 16:20:15 +00:00
|
|
|
pub const FrameSequencer = struct {
|
2022-04-20 12:39:12 +00:00
|
|
|
const Self = @This();
|
|
|
|
|
|
|
|
step: u3,
|
|
|
|
|
|
|
|
pub fn init() Self {
|
|
|
|
return .{ .step = 0 };
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn tick(self: *Self) void {
|
|
|
|
self.step +%= 1;
|
|
|
|
}
|
2022-04-20 23:52:50 +00:00
|
|
|
|
2022-10-10 16:20:15 +00:00
|
|
|
pub fn isLengthNext(self: *const Self) bool {
|
2022-04-20 23:52:50 +00:00
|
|
|
return (self.step +% 1) & 1 == 0; // Steps, 0, 2, 4, and 6 clock length
|
|
|
|
}
|
2022-04-29 15:28:35 +00:00
|
|
|
|
2022-10-10 16:20:15 +00:00
|
|
|
pub fn isEnvelopeNext(self: *const Self) bool {
|
2022-04-29 15:28:35 +00:00
|
|
|
return (self.step +% 1) == 7;
|
|
|
|
}
|
2022-04-20 12:39:12 +00:00
|
|
|
};
|