|
| 1 | +mod pulse_register; |
| 2 | + |
1 | 3 | use crate::core::Mem;
|
2 | 4 |
|
3 |
| -use bitflags::bitflags; |
| 5 | +use pulse_register::PulseRegister; |
4 | 6 |
|
5 | 7 | /// Apu is the Audio Processing Unit
|
| 8 | +/// https://www.nesdev.org/wiki/APU |
6 | 9 | pub struct Apu {
|
7 |
| - // pulse_register: |
8 |
| - // Pulse ($4000–$4007) |
9 | 10 | pulse1: PulseRegister,
|
10 | 11 | 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], |
| 12 | + triangle: [u8; 4], |
| 13 | + noise: [u8; 4], |
| 14 | + dmc: [u8; 4], |
| 15 | + status: u8, |
| 16 | + frame_counter: u8, |
58 | 17 | }
|
59 | 18 |
|
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() |
| 19 | +impl Apu { |
| 20 | + pub fn new() -> Self { |
| 21 | + Self { |
| 22 | + pulse1: PulseRegister::new(), |
| 23 | + pulse2: PulseRegister::new(), |
| 24 | + triangle: [0; 4], |
| 25 | + noise: [0; 4], |
| 26 | + dmc: [0; 4], |
| 27 | + status: 0, |
| 28 | + frame_counter: 0, |
| 29 | + } |
112 | 30 | }
|
113 | 31 | }
|
114 | 32 |
|
115 |
| -// // https://www.nesdev.org/wiki/APU_registers |
| 33 | +// https://www.nesdev.org/wiki/APU_Length_Counter |
| 34 | +// const LENGTH_TABLE: [u8; 32] = [ |
| 35 | +// 10, 254, 20, 2, 40, 4, 80, 6, 160, 8, 60, 10, 14, 12, 26, 14, 12, 16, 24, 18, 48, 20, 96, 22, |
| 36 | +// 192, 24, 72, 26, 16, 28, 32, 30, |
| 37 | +// ]; |
116 | 38 |
|
117 | 39 | impl Mem for Apu {
|
118 | 40 | fn mem_read(&mut self, addr: u16) -> u8 {
|
119 | 41 | match addr {
|
120 |
| - // They are write-only except $4015 which is read/write |
121 | 42 | 0x4000..=0x4013 | 0x4017 => panic!(
|
122 | 43 | "attempt to read from write only APU register: 0x{:04X}",
|
123 | 44 | addr
|
124 | 45 | ),
|
125 |
| - 0x04015 => todo!("read APU status"), |
| 46 | + 0x04015 => self.status, |
126 | 47 | _ => panic!("invalid lookup: 0x{:04X} is not in APU memory map", addr),
|
127 | 48 | }
|
128 | 49 | }
|
129 | 50 |
|
130 | 51 | fn mem_write(&mut self, addr: u16, val: u8) {
|
| 52 | + let idx = (addr % 4) as usize; |
131 | 53 | 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!(), |
| 54 | + 0x4000..=0x4003 => self.pulse1.write_data(idx, val), |
| 55 | + 0x4004..=0x4007 => self.pulse2.write_data(idx, val), |
| 56 | + 0x4008..=0x400B => self.triangle[idx] = val, |
| 57 | + 0x400C..=0x400F => self.noise[idx] = val, |
| 58 | + 0x4010..=0x4013 => self.dmc[idx] = val, |
| 59 | + 0x04015 => self.status = val, |
| 60 | + 0x04017 => self.frame_counter = val, |
| 61 | + _ => panic!("invalid lookup: 0x{:04X} is not in APU memory map", addr), |
141 | 62 | }
|
142 | 63 | }
|
143 | 64 | }
|
144 | 65 |
|
145 | 66 | #[cfg(test)]
|
146 | 67 | mod tests {
|
147 |
| - use super::PulseRegister; |
| 68 | + use super::*; |
148 | 69 |
|
149 | 70 | #[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); |
| 71 | + fn test_apu_mem_writes() { |
| 72 | + let mut apu = Apu::new(); |
| 73 | + for x in 0..=0x13 { |
| 74 | + apu.mem_write(0x4000 + x, 0xff); |
| 75 | + } |
| 76 | + |
| 77 | + apu.mem_write(0x4015, 123); |
| 78 | + assert_eq!(apu.status, 123); |
| 79 | + apu.mem_write(0x4017, 222); |
| 80 | + assert_eq!(apu.frame_counter, 222); |
| 81 | + } |
160 | 82 |
|
161 |
| - assert_eq!(p.duty_cycle(), 0); |
162 |
| - assert_eq!(p.is_constant_volume(), false); |
| 83 | + #[test] |
| 84 | + #[should_panic(expected = "attempt to read from write only APU register")] |
| 85 | + fn test_apu_invalid_mem_read_panics() { |
| 86 | + let mut apu = Apu::new(); |
| 87 | + _ = apu.mem_read(0x4000); |
| 88 | + } |
163 | 89 |
|
164 |
| - assert_eq!(p.is_length_counter_halted(), true); |
| 90 | + #[test] |
| 91 | + fn test_apu_mem_read_status() { |
| 92 | + let mut apu = Apu::new(); |
| 93 | + let data = apu.mem_read(0x4015); |
| 94 | + assert_eq!(data, 0); |
| 95 | + |
| 96 | + apu.status = 123; |
| 97 | + let data = apu.mem_read(0x4015); |
| 98 | + assert_eq!(data, 123); |
165 | 99 | }
|
166 | 100 | }
|
0 commit comments