Skip to content

Commit ecd9e56

Browse files
committed
feat: add initial APU
1 parent 9a5ebbc commit ecd9e56

File tree

2 files changed

+188
-0
lines changed

2 files changed

+188
-0
lines changed

src/apu/README.md

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# APU - Audio Processing Unit
2+
3+
4+
5+
6+
Rust libs:
7+
- synthesis, has osc for common wavs.. will it work well to generate one for each register?
8+
https://docs.rs/twang/latest/twang/
9+
- SDL2 can play sounds
10+
- ex of squarewave: https://github.com/Rust-SDL2/rust-sdl2/blob/master/examples/audio-queue-squarewave.rs
11+
- can play multiple sounds with mixer:
12+
- https://rust-sdl2.github.io/rust-sdl2/sdl2/mixer/index.html
13+
- usage ex: https://github.com/Rust-SDL2/rust-sdl2/blob/master/examples/mixer-demo.rs
14+
15+
Combing wave forms:
16+
https://0xc45.com/blog/digital-audio-synthesizer-in-rust/
17+
18+
Other synthesizer examples:
19+
- https://www.reddit.com/r/rust/comments/mcbx48/fullyfeatured_fm_synthesizer_running_in_the/
20+
21+
Very basic synthesis:
22+
- https://thewolfsound.com/sound-synthesis/wavetable-synth-in-rust/

src/apu/mod.rs

+166
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
use crate::core::Mem;
2+
3+
use bitflags::bitflags;
4+
5+
/// Apu is the Audio Processing Unit
6+
pub struct Apu {
7+
// pulse_register:
8+
// Pulse ($4000–$4007)
9+
pulse1: PulseRegister,
10+
pulse2: PulseRegister,
11+
// Triangle ($4008–$400B)
12+
// 2.4 Noise ($400C–$400F)
13+
// 2.5 DMC ($4010–$4013)
14+
// 2.5.1 Other Uses
15+
// 2.6 Status ($4015)
16+
// 2.7 Frame Counter ($4017)
17+
// 2.7.1 Length Counter
18+
length_counter: usize,
19+
}
20+
21+
bitflags! {
22+
/// Pulse
23+
/// https://www.nesdev.org/wiki/APU_Sweep
24+
struct Pulse: u8 {
25+
/// Duty Cycle
26+
const D = 0b1100_0000;
27+
/// Envelope loop / length counter halt
28+
const L = 0b0010_0000;
29+
/// Constant volume/envelope flag
30+
const C = 0b0001_0000;
31+
/// Volume/envelope divider period
32+
const V = 0b0000_1111;
33+
}
34+
}
35+
36+
bitflags! {
37+
/// APU Sweep
38+
/// https://www.nesdev.org/wiki/APU_Sweep
39+
struct Sweep: u8 {
40+
/// Enabled flag
41+
const E = 0b1000_0000;
42+
/// Period
43+
/// The divider's period is P + 1 half-frames
44+
const P = 0b0111_0000;
45+
/// Negate flag
46+
/// 0: add to period, sweeping toward lower frequencies
47+
/// 1: subtract from period, sweeping toward higher frequencies
48+
const N = 0b0000_1000;
49+
/// Shift count (number of bits).
50+
/// If SSS is 0, then behaves like E=0.
51+
const S = 0b0000_0111;
52+
}
53+
}
54+
55+
/// https://www.nesdev.org/wiki/APU_Pulse
56+
struct PulseRegister {
57+
data: [u8; 4],
58+
}
59+
60+
/// https://www.nesdev.org/wiki/APU_Length_Counter
61+
const LENGTH_TABLE: [u8; 32] = [
62+
10, 254, 20, 2, 40, 4, 80, 6, 160, 8, 60, 10, 14, 12, 26, 14, 12, 16, 24, 18, 48, 20, 96, 22,
63+
192, 24, 72, 26, 16, 28, 32, 30,
64+
];
65+
66+
impl PulseRegister {
67+
fn duty_cycle(&self) -> u8 {
68+
let duty = (self.data[0] & Pulse::D.bits()) >> 6;
69+
assert!(duty < 4);
70+
duty
71+
}
72+
73+
fn is_length_counter_halted(&self) -> bool {
74+
Pulse::from_bits_truncate(self.data[0]).contains(Pulse::L)
75+
}
76+
77+
fn is_constant_volume(&self) -> bool {
78+
Pulse::from_bits_truncate(self.data[1]).contains(Pulse::C)
79+
}
80+
81+
/// The sequencer is clocked by an 11-bit timer.
82+
/// The timer value t = HHHLLLLLLLL is formed by timer high and timer low.
83+
fn timer(&self) -> u16 {
84+
let low8 = self.data[2] as u16;
85+
let hi3 = ((self.data[3] & 0b0000_0111) as u16) << 8;
86+
hi3 + low8
87+
}
88+
89+
/// Gives an index into the LENGTH_TABLE
90+
fn length_counter_load(&self) -> u8 {
91+
(self.data[3] & 0b1111_1000) >> 3
92+
}
93+
94+
fn is_sweep_enabled(&self) -> bool {
95+
let ssc = self.sweep_shift_count();
96+
let enabled = Sweep::from_bits_truncate(self.data[1]).contains(Sweep::E);
97+
ssc > 0 && enabled
98+
}
99+
100+
fn is_sweep_negated(&self) -> bool {
101+
Sweep::from_bits_truncate(self.data[1]).contains(Sweep::N)
102+
}
103+
104+
fn sweep_period(&self) -> u8 {
105+
let s = Sweep::from_bits_truncate(self.data[1]);
106+
Sweep::P.intersection(s).bits() >> 4
107+
}
108+
109+
fn sweep_shift_count(&self) -> u8 {
110+
let s = Sweep::from_bits_truncate(self.data[1]);
111+
Sweep::S.intersection(s).bits()
112+
}
113+
}
114+
115+
// // https://www.nesdev.org/wiki/APU_registers
116+
117+
impl Mem for Apu {
118+
fn mem_read(&mut self, addr: u16) -> u8 {
119+
match addr {
120+
// They are write-only except $4015 which is read/write
121+
0x4000..=0x4013 | 0x4017 => panic!(
122+
"attempt to read from write only APU register: 0x{:04X}",
123+
addr
124+
),
125+
0x04015 => todo!("read APU status"),
126+
_ => panic!("invalid lookup: 0x{:04X} is not in APU memory map", addr),
127+
}
128+
}
129+
130+
fn mem_write(&mut self, addr: u16, val: u8) {
131+
match addr {
132+
0x4000..=0x4003 => self.pulse1.data[(0x4000 - addr) as usize] = val,
133+
0x4004..=0x4007 => self.pulse2.data[(0x4004 - addr) as usize] = val,
134+
// 0x4008..=0x400B => panic!("APU write-only register: triangle register"),
135+
// 0x400C..=0x400F => panic!("APU write-only register: noise register"),
136+
// 0x4010..=0x4013 => todo!("dmc"),
137+
// They are write-only except $4015 which is read/write
138+
// 0x04015 => todo!("status"),
139+
// 0x04017 => todo!("frame counter"),
140+
_ => todo!(),
141+
}
142+
}
143+
}
144+
145+
#[cfg(test)]
146+
mod tests {
147+
use super::PulseRegister;
148+
149+
#[test]
150+
fn test_pulse_register_getters() {
151+
let mut p = PulseRegister { data: [0; 4] };
152+
assert_eq!(p.timer(), 0);
153+
assert_eq!(p.length_counter_load(), 0);
154+
p.data[2] = 0xff;
155+
p.data[3] = 0b0000_0111;
156+
assert_eq!(p.length_counter_load(), 0);
157+
assert_eq!(p.timer(), 2048 - 1);
158+
p.data[3] = 0xff;
159+
assert_eq!(p.length_counter_load(), 32 - 1);
160+
161+
assert_eq!(p.duty_cycle(), 0);
162+
assert_eq!(p.is_constant_volume(), false);
163+
164+
assert_eq!(p.is_length_counter_halted(), true);
165+
}
166+
}

0 commit comments

Comments
 (0)