From a9227af750cf8cd7fb760b1989609d4cf0a4c5ec Mon Sep 17 00:00:00 2001 From: Theron Spiegl Date: Tue, 26 Nov 2019 19:11:51 -0600 Subject: [PATCH] starting on audio --- src/apu/dmc.rs | 7 ++++ src/apu/mod.rs | 84 +++++++++++++++++++++++++++++++++++++++++++++ src/apu/noise.rs | 32 +++++++++++++++++ src/apu/square.rs | 13 +++++++ src/apu/triangle.rs | 9 +++++ src/cpu/mod.rs | 13 ++++--- src/main.rs | 28 ++++++++------- src/screen/mod.rs | 58 +++++++++++++++---------------- 8 files changed, 199 insertions(+), 45 deletions(-) create mode 100644 src/apu/dmc.rs create mode 100644 src/apu/mod.rs create mode 100644 src/apu/noise.rs create mode 100644 src/apu/square.rs create mode 100644 src/apu/triangle.rs diff --git a/src/apu/dmc.rs b/src/apu/dmc.rs new file mode 100644 index 0000000..e344902 --- /dev/null +++ b/src/apu/dmc.rs @@ -0,0 +1,7 @@ +impl super::DMC { + pub fn new() -> Self { + super::DMC { + + } + } +} \ No newline at end of file diff --git a/src/apu/mod.rs b/src/apu/mod.rs new file mode 100644 index 0000000..839d90a --- /dev/null +++ b/src/apu/mod.rs @@ -0,0 +1,84 @@ +mod noise; +mod square; +mod triangle; +mod dmc; + +pub struct Apu { + square1: Square, + square2: Square, + triangle: Triangle, + noise: Noise, + dmc: DMC, +} + +struct Square { + duty_cycle: usize, + length_counter_halt: bool, // (this bit is also the envelope's loop flag) + constant_volume_flag: bool, // (0: use volume from envelope; 1: use constant volume) + timer: usize, + length_counter: usize, + envelope: usize, + sweep: usize, +} + +// $4008 Hlll.llll Triangle channel length counter halt and linear counter load (write) +// bit 7 H--- ---- Halt length counter (this bit is also the linear counter's control flag) +struct Triangle { + timer: usize, + length_counter: usize, // (this bit is also the linear counter's control flag) + linear_counter: usize, +} + +// $400E M---.PPPP Mode and period (write) +// bit 7 M--- ---- Mode flag +struct Noise { + timer: usize, + length_counter: usize, + envelope: usize, + linear_feedback_sr: u16, + mode: bool, // also called loop noise, bit 7 of $400E +} + +struct DMC { + +} + +impl Apu { + fn new() -> Self { + Apu { + square1: Square::new(), + square2: Square::new(), + triangle: Triangle::new(), + noise: Noise::new(), + dmc: DMC::new(), + } + } + + fn write_reg(&mut self, address: usize, value: u8) { + match address { + 0x4000 => self.square1.duty(value), + 0x4001 => self.square1.sweep(value), + 0x4002 => self.square1.timer_low(value), + 0x4003 => self.square1.timer_high(value), + 0x4004 => self.square2.duty(value), + 0x4005 => self.square2.sweep(value), + 0x4006 => self.square2.timer_low(value), + 0x4007 => self.square2.timer_high(value), + 0x4008 => self.triangle.counter(value), + 0x400A => self.triangle.timer_low(value), + 0x400B => self.triangle.timer_high(value), + 0x400C => self.noise.envelope(value), + 0x400D => (), + 0x400E => self.noise.loop_noise(value), + 0x400F => self.noise.load_length_counter(value), + 0x4010 => self.dmc.control(value), + 0x4011 => self.dmc.direct_load(value), + 0x4012 => self.dmc.sample_address(value), + 0x4013 => self.dmc.sample_length(value), + 0x4014 => (), + 0x4015 => self.control(value), + 0x4016 => (), + 0x4017 => self.frame_counter(value), + } + } +} diff --git a/src/apu/noise.rs b/src/apu/noise.rs new file mode 100644 index 0000000..93b66b3 --- /dev/null +++ b/src/apu/noise.rs @@ -0,0 +1,32 @@ +impl super::Noise { + pub fn new() -> Self { + super::Noise { + timer: 0, + length_counter: 0, + envelope: 0, + linear_feedback_sr: 1, // On power-up, the shift register is loaded with the value 1. + mode: false, // also called loop noise, bit 7 of $400E + } + } + + pub fn clock(&mut self) { + let bit0 = self.linear_feedback_sr & (1 << 0); + let other_bit = match self.mode { + false => (self.linear_feedback_sr & (1 << 1)) >> 1, + true => (self.linear_feedback_sr & (1 << 6)) >> 6, + }; + let feedback = bit0 ^ other_bit; + self.linear_feedback_sr >>= 1; + self.linear_feedback_sr |= feedback << 14; + } +} + +// When the timer clocks the shift register, the following actions occur in order: + +// 1. Feedback is calculated as the exclusive-OR of bit 0 and one other bit: bit 6 if Mode flag is set, otherwise bit 1. +// 2. The shift register is shifted right by one bit. +// 3. Bit 14, the leftmost bit, is set to the feedback calculated earlier. + +// This results in a pseudo-random bit sequence, 32767 steps long when Mode flag is clear, +// and randomly 93 or 31 steps long otherwise. (The particular 31- or 93-step sequence depends +// on where in the 32767-step sequence the shift register was when Mode flag was set). diff --git a/src/apu/square.rs b/src/apu/square.rs new file mode 100644 index 0000000..fe6fbf2 --- /dev/null +++ b/src/apu/square.rs @@ -0,0 +1,13 @@ +impl super::Square { + pub fn new() -> Self { + super::Square { + duty_cycle: 0, + length_counter_halt: false, + constant_volume_flag: false, + timer: 0, + length_counter: 0, + envelope: 0, + sweep: 0, + } + } +} \ No newline at end of file diff --git a/src/apu/triangle.rs b/src/apu/triangle.rs new file mode 100644 index 0000000..15b50c2 --- /dev/null +++ b/src/apu/triangle.rs @@ -0,0 +1,9 @@ +impl super::Triangle { + pub fn new() -> Self { + super::Triangle { + timer: 0, + length_counter: 0, + linear_counter: 0, + } + } +} \ No newline at end of file diff --git a/src/cpu/mod.rs b/src/cpu/mod.rs index e48e180..289ad2a 100644 --- a/src/cpu/mod.rs +++ b/src/cpu/mod.rs @@ -76,6 +76,9 @@ pub struct Cpu { // ppu pub ppu: super::Ppu, + // apu + apu: super::Apu, + // controller pub strobe: u8, pub button_states: u8, // Player 1 controller @@ -83,7 +86,7 @@ pub struct Cpu { } impl Cpu { - pub fn new(cart: &super::Cartridge, ppu: super::Ppu) -> Self { + pub fn new(cart: &super::Cartridge, ppu: super::Ppu, apu: super::Apu) -> Self { let mut cpu = Cpu{ mem: vec![0; 0x2000], A: 0, X: 0, Y: 0, @@ -95,6 +98,7 @@ impl Cpu { prg_rom: cart.prg_rom.clone(), mapper_func: cart.cpu_mapper_func, ppu: ppu, + apu: apu, strobe: 0, button_states: 0, button_number: 0, @@ -187,11 +191,12 @@ impl Cpu { 0x0000...0x1FFF => self.mem[address % 0x0800], 0x2000...0x3FFF => self.read_ppu_reg(address % 8), 0x4014 => self.read_ppu_reg(8), + 0x4015 => self.apu.read_status(), 0x4016 => self.read_controller(), - 0x4000...0x4017 => 0, // APU stuff + 0x4000...0x4017 => 0, // can't read from these APU registers 0x4018...0x401F => 0, // APU and I/O functionality that is normally disabled. See CPU Test Mode. 0x4020...0xFFFF => { // Cartridge space: PRG ROM, PRG RAM, and mapper registers - *(self.mapper_func)(self, address, false).unwrap() // unwraping because mapper funcs won't return None for reads. + *(self.mapper_func)(self, address, false).unwrap() // unwrapping because mapper funcs won't return None for reads. }, _ => panic!("invalid read from 0x{:02x}", address), }; @@ -205,7 +210,7 @@ impl Cpu { 0x2000...0x3FFF => self.write_ppu_reg(address % 8, val), 0x4014 => self.write_ppu_reg(8, val), 0x4016 => self.write_controller(val), - 0x4000...0x4017 => (), // APU stuff + 0x4000...0x4017 => self.apu.write_reg(address, val), // APU stuff 0x4018...0x401F => (), // APU and I/O functionality that is normally disabled. See CPU Test Mode. 0x4020...0xFFFF => { // Cartridge space: PRG ROM, PRG RAM, and mapper registers match (self.mapper_func)(self, address, true) { diff --git a/src/main.rs b/src/main.rs index 27f5c26..15637ff 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,15 +2,17 @@ use std::time::{Instant, Duration}; mod cpu; mod ppu; +mod apu; mod cartridge; -mod screen; mod input; +mod screen; use cpu::Cpu; use ppu::Ppu; +use apu::Apu; use cartridge::Cartridge; -use screen::{init_window, draw_pixel, draw_to_window}; use input::poll_buttons; +use screen::{init_window, draw_pixel, draw_to_window}; use sdl2::keyboard::Keycode; use sdl2::event::Event; @@ -20,12 +22,12 @@ use sdl2::pixels::PixelFormatEnum; fn main() -> Result<(), String> { // Set up screen - let sdl_context = sdl2::init()?; - let mut event_pump = sdl_context.event_pump()?; + let sdl_context = sdl2::init()?; + let mut event_pump = sdl_context.event_pump()?; let (mut canvas, texture_creator) = init_window(&sdl_context).expect("Could not create window"); let mut texture = texture_creator.create_texture_streaming( PixelFormatEnum::RGB24, 256*screen::SCALE_FACTOR as u32, 240*screen::SCALE_FACTOR as u32) - .map_err(|e| e.to_string())?; + .map_err(|e| e.to_string())?; let byte_width = 256 * 3 * screen::SCALE_FACTOR; // 256 NES pixels, 3 bytes for each pixel (RGB 24-bit), and NES-to-SDL scale factor let byte_height = 240 * screen::SCALE_FACTOR; // NES image is 240 pixels tall, multiply by scale factor for total number of rows needed let mut screen_buffer = vec![0; byte_width * byte_height]; // contains raw RGB data for the screen @@ -33,7 +35,8 @@ fn main() -> Result<(), String> { // Initialize hardware components let cart = Cartridge::new(); let ppu = Ppu::new(&cart); - let mut cpu = Cpu::new(&cart, ppu); + let apu = Apu::new(); + let mut cpu = Cpu::new(&cart, ppu, apu); // For throttling to 60 FPS let mut timer = Instant::now(); @@ -45,25 +48,26 @@ fn main() -> Result<(), String> { // perform 1 cpu instruction, getting back number of clock cycles it took let num_cycles = cpu.step(); // maintain ratio of 3 ppu cycles for 1 cpu step - for _i in 0..num_cycles * 3 { + for _ in 0..num_cycles * 3 { let (pixel, end_of_frame) = cpu.ppu.step(); match pixel { Some((x, y, color)) => draw_pixel(&mut screen_buffer, x, y, color), None => (), }; if end_of_frame { - fps += 1; - draw_to_window(&mut texture, &mut canvas, &screen_buffer)?; + fps += 1; // keep track of how many frames we've rendered this second + draw_to_window(&mut texture, &mut canvas, &screen_buffer)?; // draw the buffer to the window with SDL let now = Instant::now(); + // if we're running faster than 60Hz, kill time if now < timer + Duration::from_millis(1000/60) { std::thread::sleep(timer + Duration::from_millis(1000/60) - now); } timer = Instant::now(); + // listen for Esc or window close. TODO: does this prevent keyboard events from being handled? for event in event_pump.poll_iter() { match event { - Event::Quit {..} | Event::KeyDown { keycode: Some(Keycode::Escape), .. } => { - break 'running - }, + Event::Quit {..} | Event::KeyDown { keycode: Some(Keycode::Escape), .. } + => { break 'running }, _ => (), } } diff --git a/src/screen/mod.rs b/src/screen/mod.rs index 06c7c2c..edf3696 100644 --- a/src/screen/mod.rs +++ b/src/screen/mod.rs @@ -12,40 +12,40 @@ const BYTES_IN_ROW: usize = BYTES_IN_COL * 256; // 256 = screen width in NES pix type RGBColor = (u8, u8, u8); pub fn init_window(context: &Sdl) -> Result<(Canvas, TextureCreator), String> { - let video_subsystem = context.video()?; - let window = video_subsystem.window("NESTUR", (256 * SCALE_FACTOR) as u32, (240 * SCALE_FACTOR) as u32) - .position_centered() - .opengl() - .build() - .map_err(|e| e.to_string())?; - let mut canvas = window.into_canvas().build().map_err(|e| e.to_string())?; - let texture_creator = canvas.texture_creator(); - canvas.set_draw_color(Color::RGB(0, 0, 0)); - canvas.clear(); - canvas.present(); - Ok((canvas, texture_creator)) + let video_subsystem = context.video()?; + let window = video_subsystem.window("NESTUR", (256 * SCALE_FACTOR) as u32, (240 * SCALE_FACTOR) as u32) + .position_centered() + .opengl() + .build() + .map_err(|e| e.to_string())?; + let mut canvas = window.into_canvas().build().map_err(|e| e.to_string())?; + let texture_creator = canvas.texture_creator(); + canvas.set_draw_color(Color::RGB(0, 0, 0)); + canvas.clear(); + canvas.present(); + Ok((canvas, texture_creator)) } pub fn draw_pixel(buffer: &mut Vec, x: usize, y: usize, color: RGBColor) { - let (r, g, b) = color; - let nes_y_offset = y * BYTES_IN_ROW * SCALE_FACTOR; // find offset for thick, SCALE_FACTOR-pixel tall row - for sdl_row_num in 0..SCALE_FACTOR { // looping over one-pixel tall rows up to SCALE_FACTOR - let row_offset = nes_y_offset + (sdl_row_num * BYTES_IN_ROW); // row_offset is the offset within buffer of the thin row we're on - let nes_x_offset = x * BYTES_IN_COL; // find horizontal offset within row (in byte terms) of NES x-coordinate - for sdl_col_num in 0..SCALE_FACTOR { // for pixels up to SCALE_FACTOR, moving horizontally - let col_offset = nes_x_offset + (sdl_col_num * 3); // skip 3 bytes at a time, R/G/B for each pixel - let offset = row_offset + col_offset; - buffer[offset + 0] = r; - buffer[offset + 1] = g; - buffer[offset + 2] = b; - } - } + let (r, g, b) = color; + let nes_y_offset = y * BYTES_IN_ROW * SCALE_FACTOR; // find offset for thick, SCALE_FACTOR-pixel tall row + for sdl_row_num in 0..SCALE_FACTOR { // looping over one-pixel tall rows up to SCALE_FACTOR + let row_offset = nes_y_offset + (sdl_row_num * BYTES_IN_ROW); // row_offset is the offset within buffer of the thin row we're on + let nes_x_offset = x * BYTES_IN_COL; // find horizontal offset within row (in byte terms) of NES x-coordinate + for sdl_col_num in 0..SCALE_FACTOR { // for pixels up to SCALE_FACTOR, moving horizontally + let col_offset = nes_x_offset + (sdl_col_num * 3); // skip 3 bytes at a time, R/G/B for each pixel + let offset = row_offset + col_offset; + buffer[offset + 0] = r; + buffer[offset + 1] = g; + buffer[offset + 2] = b; + } + } } pub fn draw_to_window(texture: &mut Texture, canvas: &mut Canvas, buffer: &Vec) -> Result<(), String> { - texture.update(None, buffer, 256*3*SCALE_FACTOR) - .map_err(|e| e.to_string())?; + texture.update(None, buffer, 256*3*SCALE_FACTOR) + .map_err(|e| e.to_string())?; canvas.copy(&texture, None, None)?; - canvas.present(); - Ok(()) + canvas.present(); + Ok(()) }