Code for the Sunsoft 5A / 5B / FME-7 boards (iNES mapper 69).
11#[allow(clippy::upper_case_acronyms)] 12#[derive(Debug, Clone, Copy, PartialEq, Eq, Encode, Decode)] 13enum PrgType { 14 ROM, 15 RAM, 16} 17 18#[derive(Debug, Clone, Encode, Decode)] 19struct Sunsoft5bChannel { 20 wave_step: bool, 21 divider: u8, 22 timer: u16, 23 period: u16, 24 tone_enabled: bool, 25 volume: u8, 26} 27 28const AUDIO_DIVIDER: u8 = 16; 29 30impl Sunsoft5bChannel { 31 fn new() -> Self { 32 Self { 33 wave_step: false, 34 divider: AUDIO_DIVIDER, 35 timer: 0, 36 period: 0, 37 tone_enabled: false, 38 volume: 0, 39 } 40 } 41 42 fn handle_period_low_update(&mut self, value: u8) { 43 self.period = (self.period & 0xFF00) | u16::from(value); 44 } 45 46 fn handle_period_high_update(&mut self, value: u8) { 47 self.period = (self.period & 0x00FF) | (u16::from(value & 0x0F) << 8); 48 } 49 50 fn handle_volume_update(&mut self, value: u8) { 51 self.volume = value & 0x0F; 52 } 53 54 fn sample(&self) -> u8 { 55 if !self.tone_enabled { self.volume } else { u8::from(self.wave_step) * self.volume } 56 } 57 58 fn sample_analog(&self) -> f64 { 59 static DAC_LOOKUP_TABLE: LazyLock<[f64; 16]> = LazyLock::new(|| { 60 let mut lookup_table = [0.0; 16]; 61 62 // Arbitrary value, will get normalized later 63 lookup_table[1] = 1.0; 64 65 // https://en.wikipedia.org/wiki/Decibel 66 // 67 // Each step produces an output difference of 3dB which gives this equation, where L0 is the 68 // previous output value on a linear scale and L1 is the current value: 69 // 3dB = 20 * log10(L1/L0) 70 // Simple algebra brings this to: 71 // L1 = L0 * 10^(3/20) 72 for i in 2..16 { 73 lookup_table[i] = lookup_table[i - 1] * 10.0_f64.powf(3.0 / 20.0); 74 } 75 76 // Hack: the 5B has some sort of compressor that appears to make the difference between 77 // volume 14 and volume 15 much smaller than it should be. 78 // 79 // Without doing anything, volume 14 will end up around (0.707 * volume 15), so modify 80 // volume 15 to be a bit higher than that. 81 // 82 // This also has the effect of making all of the lower volumes a little louder, which is 83 // desired because otherwise they will sound too soft. 84 lookup_table[15] *= 0.72; 85 86 // Normalize the values so that the max is 1.0 87 let max = lookup_table[15]; 88 for value in &mut lookup_table[1..] { 89 *value /= max; 90 } 91 92 lookup_table 93 }); 94 95 DAC_LOOKUP_TABLE[self.sample() as usize] 96 } 97 98 fn clock(&mut self) { 99 self.timer += 1; 100 if self.timer >= self.period { 101 self.timer = 0; 102 self.wave_step = !self.wave_step; 103 } 104 } 105 106 fn tick_cpu(&mut self) { 107 self.divider -= 1; 108 if self.divider == 0 { 109 self.divider = AUDIO_DIVIDER; 110 self.clock(); 111 } 112 } 113} 114 115#[derive(Debug, Clone, Encode, Decode)] 116struct Sunsoft5bAudioUnit { 117 register_select: u8, 118 register_writes_enabled: bool, 119 channel_1: Sunsoft5bChannel, 120 channel_2: Sunsoft5bChannel, 121 channel_3: Sunsoft5bChannel, 122} 123 124impl Sunsoft5bAudioUnit { 125 fn new() -> Self { 126 Self { 127 register_select: 0, 128 register_writes_enabled: false, 129 channel_1: Sunsoft5bChannel::new(), 130 channel_2: Sunsoft5bChannel::new(), 131 channel_3: Sunsoft5bChannel::new(), 132 } 133 } 134 135 fn handle_select_update(&mut self, value: u8) { 136 self.register_select = value & 0x0F; 137 self.register_writes_enabled = value & 0xF0 == 0; 138 } 139 140 fn handle_write(&mut self, value: u8) { 141 if !self.register_writes_enabled { 142 return; 143 } 144 145 match self.register_select { 146 0x00 => { 147 self.channel_1.handle_period_low_update(value); 148 } 149 0x01 => { 150 self.channel_1.handle_period_high_update(value); 151 } 152 0x02 => { 153 self.channel_2.handle_period_low_update(value); 154 } 155 0x03 => { 156 self.channel_2.handle_period_high_update(value); 157 } 158 0x04 => { 159 self.channel_3.handle_period_low_update(value); 160 } 161 0x05 => { 162 self.channel_3.handle_period_high_update(value); 163 } 164 0x07 => { 165 self.channel_3.tone_enabled = !value.bit(2); 166 self.channel_2.tone_enabled = !value.bit(1); 167 self.channel_1.tone_enabled = !value.bit(0); 168 } 169 0x08 => { 170 self.channel_1.handle_volume_update(value); 171 } 172 0x09 => { 173 self.channel_2.handle_volume_update(value); 174 } 175 0x0A => { 176 self.channel_3.handle_volume_update(value); 177 } 178 _ => {} 179 } 180 } 181 182 fn tick_cpu(&mut self) { 183 self.channel_1.tick_cpu(); 184 self.channel_2.tick_cpu(); 185 self.channel_3.tick_cpu(); 186 } 187 188 fn sample(&self) -> f64 { 189 (self.channel_1.sample_analog() 190 + self.channel_2.sample_analog() 191 + self.channel_3.sample_analog()) 192 / 3.0 193 } 194 195 fn enabled(&self) -> bool { 196 self.channel_1.tone_enabled 197 || self.channel_2.tone_enabled 198 || self.channel_3.tone_enabled 199 || self.channel_1.volume != 0 200 || self.channel_2.volume != 0 201 || self.channel_3.volume != 0 202 } 203} 204 205#[derive(Debug, Clone, Encode, Decode)] 206pub(crate) struct Sunsoft { 207 prg_banks: [u8; 4], 208 prg_bank_0_type: PrgType, 209 prg_ram_enabled: bool, 210 chr_type: ChrType, 211 chr_banks: [u8; 8], 212 nametable_mirroring: NametableMirroring, 213 command_register: u8, 214 irq_enabled: bool, 215 irq_counter_enabled: bool, 216 irq_counter: u16, 217 irq_triggered: bool, 218 audio: Sunsoft5bAudioUnit, 219} 220 221impl Sunsoft { 222 pub(crate) fn new(chr_type: ChrType) -> Self { 223 Self { 224 prg_banks: [0; 4], 225 prg_bank_0_type: PrgType::ROM, 226 prg_ram_enabled: false, 227 chr_type, 228 chr_banks: [0; 8], 229 nametable_mirroring: NametableMirroring::Vertical, 230 command_register: 0, 231 irq_enabled: false, 232 irq_counter_enabled: false, 233 irq_counter: 0, 234 irq_triggered: false, 235 audio: Sunsoft5bAudioUnit::new(), 236 } 237 } 238} 239 240impl MapperImpl<Sunsoft> { 241 fn map_cpu_address(&self, address: u16) -> CpuMapResult { 242 match address { 243 0x0000..=0x401F => panic!("invalid CPU map address: {address:04X}"), 244 0x4020..=0x5FFF => CpuMapResult::None, 245 0x6000..=0x7FFF => match self.data.prg_bank_0_type { 246 PrgType::ROM => { 247 let prg_rom_addr = 248 BankSizeKb::Eight.to_absolute_address(self.data.prg_banks[0], address); 249 CpuMapResult::PrgROM(prg_rom_addr) 250 } 251 PrgType::RAM => { 252 if self.data.prg_ram_enabled && !self.cartridge.prg_ram.is_empty() { 253 let prg_ram_addr = 254 BankSizeKb::Eight.to_absolute_address(self.data.prg_banks[0], address); 255 CpuMapResult::PrgRAM(prg_ram_addr) 256 } else { 257 CpuMapResult::None 258 } 259 } 260 }, 261 0x8000..=0xDFFF => { 262 // 0x8000..=0x9FFF to bank index 1 263 // 0xA000..=0xBFFF to bank index 2 264 // 0xC000..=0xDFFF to bank index 3 265 let bank_index = (address - 0x6000) / 0x2000; 266 let prg_rom_addr = BankSizeKb::Eight 267 .to_absolute_address(self.data.prg_banks[bank_index as usize], address); 268 CpuMapResult::PrgROM(prg_rom_addr) 269 } 270 0xE000..=0xFFFF => { 271 // Fixed to last bank 272 let prg_rom_addr = BankSizeKb::Eight 273 .to_absolute_address_last_bank(self.cartridge.prg_rom.len() as u32, address); 274 CpuMapResult::PrgROM(prg_rom_addr) 275 } 276 } 277 } 278 279 pub(crate) fn read_cpu_address(&self, address: u16, cpu_open_bus: u8) -> u8 { 280 self.map_cpu_address(address).read(&self.cartridge).unwrap_or(cpu_open_bus) 281 } 282 283 pub(crate) fn write_cpu_address(&mut self, address: u16, value: u8) { 284 match address { 285 0x0000..=0x401F => panic!("invalid CPU map address: {address:04X}"), 286 0x4020..=0x5FFF => {} 287 0x6000..=0x7FFF => { 288 self.map_cpu_address(address).write(value, &mut self.cartridge); 289 } 290 0x8000..=0x9FFF => { 291 self.data.command_register = value & 0x0F; 292 } 293 0xA000..=0xBFFF => match self.data.command_register { 294 0x00..=0x07 => { 295 self.data.chr_banks[self.data.command_register as usize] = value; 296 } 297 0x08 => { 298 self.data.prg_banks[0] = value & 0x3F; 299 self.data.prg_bank_0_type = 300 if value.bit(6) { PrgType::RAM } else { PrgType::ROM }; 301 self.data.prg_ram_enabled = value.bit(7); 302 } 303 0x09..=0x0B => { 304 let prg_bank_index = self.data.command_register - 0x08; 305 self.data.prg_banks[prg_bank_index as usize] = value & 0x3F; 306 } 307 0x0C => { 308 self.data.nametable_mirroring = match value & 0x03 { 309 0x00 => NametableMirroring::Vertical, 310 0x01 => NametableMirroring::Horizontal, 311 0x02 => NametableMirroring::SingleScreenBank0, 312 0x03 => NametableMirroring::SingleScreenBank1, 313 _ => unreachable!("value & 0x03 should always be 0x00/0x01/0x02/0x03"), 314 }; 315 } 316 0x0D => { 317 self.data.irq_enabled = value.bit(0); 318 self.data.irq_counter_enabled = value.bit(7); 319 self.data.irq_triggered = false; 320 } 321 0x0E => { 322 self.data.irq_counter = (self.data.irq_counter & 0xFF00) | u16::from(value); 323 } 324 0x0F => { 325 self.data.irq_counter = 326 (self.data.irq_counter & 0x00FF) | (u16::from(value) << 8); 327 } 328 _ => panic!("command register should always contain 0-15"), 329 }, 330 0xC000..=0xDFFF => { 331 self.data.audio.handle_select_update(value); 332 } 333 0xE000..=0xFFFF => { 334 self.data.audio.handle_write(value); 335 } 336 } 337 } 338 339 pub(crate) fn interrupt_flag(&self) -> bool { 340 self.data.irq_triggered 341 } 342 343 pub(crate) fn tick_cpu(&mut self) { 344 self.data.audio.tick_cpu(); 345 346 if !self.data.irq_counter_enabled { 347 return; 348 } 349 350 if self.data.irq_enabled && self.data.irq_counter == 0 { 351 self.data.irq_triggered = true; 352 } 353 self.data.irq_counter = self.data.irq_counter.wrapping_sub(1); 354 } 355 356 pub(crate) fn sample_audio(&self, mixed_apu_sample: f64) -> f64 { 357 if !self.data.audio.enabled() { 358 return mixed_apu_sample; 359 } 360 361 let sunsoft_5b_sample = self.data.audio.sample(); 362 363 // This audio chip appears to slightly decrease APU channel volume 364 0.7 * mixed_apu_sample - sunsoft_5b_sample 365 } 366} 367 368impl HasBasicPpuMapping for MapperImpl<Sunsoft> { 369 fn map_ppu_address(&self, address: u16) -> PpuMapResult { 370 match address { 371 0x0000..=0x1FFF => { 372 let chr_bank_index = address / 0x0400; 373 let chr_addr = BankSizeKb::One 374 .to_absolute_address(self.data.chr_banks[chr_bank_index as usize], address); 375 self.data.chr_type.to_map_result(chr_addr) 376 } 377 0x2000..=0x3EFF => { 378 PpuMapResult::Vram(self.data.nametable_mirroring.map_to_vram(address)) 379 } 380 0x3F00..=0xFFFF => panic!("invalid PPU map address: {address:04X}"), 381 } 382 } 383}