pub mod dmc;
pub mod frame;
pub mod mixer;
pub mod noise;
pub mod pulse;
pub mod triangle;
pub mod units;
#[cfg(test)]
mod tests;
use alloc::boxed::Box;
use alloc::sync::Arc;
use alloc::vec::Vec;
use core::fmt;
use crate::core::clock::DomainId;
use crate::core::device::{Device, DeviceClass, PropertySpec, RealizeCtx, ResetKind};
use crate::core::error::{Error, Result};
use crate::core::props::{Props, ValueKind};
use crate::core::registry::Registry;
use crate::core::space::{AccessConstraints, MemAttrs, MemOps, MemResult, Region};
use crate::core::state::{ChunkReader, ChunkWriter};
use crate::core::sync::{AtomicU8, LockRank, Mutex, Ordering};
use crate::core::value::{Endian, Width};
use crate::core::wire::{Level, WireSource};
pub use dmc::{DmaKind, DmaRequest};
pub use frame::{Mode, Timing};
use dmc::Dmc;
use frame::FrameCounter;
use mixer::SampleRing;
use noise::Noise;
use pulse::Pulse;
use triangle::Triangle;
mod reg {
pub(super) const PULSE1_CTRL: u8 = 0x00;
pub(super) const PULSE1_SWEEP: u8 = 0x01;
pub(super) const PULSE1_LO: u8 = 0x02;
pub(super) const PULSE1_HI: u8 = 0x03;
pub(super) const PULSE2_CTRL: u8 = 0x04;
pub(super) const PULSE2_SWEEP: u8 = 0x05;
pub(super) const PULSE2_LO: u8 = 0x06;
pub(super) const PULSE2_HI: u8 = 0x07;
pub(super) const TRI_LINEAR: u8 = 0x08;
pub(super) const TRI_LO: u8 = 0x0A;
pub(super) const TRI_HI: u8 = 0x0B;
pub(super) const NOISE_CTRL: u8 = 0x0C;
pub(super) const NOISE_PERIOD: u8 = 0x0E;
pub(super) const NOISE_LEN: u8 = 0x0F;
pub(super) const DMC_CTRL: u8 = 0x10;
pub(super) const DMC_LOAD: u8 = 0x11;
pub(super) const DMC_ADDR: u8 = 0x12;
pub(super) const DMC_LEN: u8 = 0x13;
pub(super) const STATUS: u8 = 0x15;
pub(super) const FRAME: u8 = 0x17;
}
#[derive(Debug)]
struct Core {
frame: FrameCounter,
pulse1: Pulse,
pulse2: Pulse,
triangle: Triangle,
noise: Noise,
dmc: Dmc,
ticks: u64,
phase: u64,
samples: SampleRing,
}
impl Core {
fn new(timing: Timing, phase: u64, halt_ultrasonic: bool, capacity: usize) -> Core {
Core {
frame: FrameCounter::new(timing),
pulse1: Pulse::new(true),
pulse2: Pulse::new(false),
triangle: Triangle::new(halt_ultrasonic),
noise: Noise::new(timing),
dmc: Dmc::new(timing),
ticks: 0,
phase,
samples: SampleRing::with_capacity(capacity),
}
}
#[inline]
fn on_put_cycle(&self) -> bool {
(self.ticks.wrapping_sub(1).wrapping_add(self.phase)) & 1 == 1
}
fn tick(&mut self) {
self.ticks += 1;
let now = self.ticks;
let event = self.frame.tick(now);
if event.quarter {
self.clock_quarter_frame();
}
if event.half {
self.clock_half_frame();
}
self.triangle.tick_timer();
if !self.on_put_cycle() {
self.pulse1.tick_timer();
self.pulse2.tick_timer();
self.noise.tick_timer();
self.dmc.tick_timer();
let sample = self.mix();
self.samples.push(sample);
}
}
fn clock_quarter_frame(&mut self) {
self.pulse1.envelope.clock();
self.pulse2.envelope.clock();
self.noise.envelope.clock();
self.triangle.clock_linear();
}
fn clock_half_frame(&mut self) {
self.pulse1.length.clock();
self.pulse1.clock_sweep();
self.pulse2.length.clock();
self.pulse2.clock_sweep();
self.triangle.length.clock();
self.noise.length.clock();
}
fn mix(&self) -> u16 {
mixer::mix(
self.pulse1.output(),
self.pulse2.output(),
self.triangle.output(),
self.noise.output(),
self.dmc.output(),
)
}
#[inline]
fn irq_asserted(&self) -> bool {
self.frame.irq() || self.dmc.irq()
}
fn write(&mut self, index: u8, value: u8) {
match index {
reg::PULSE1_CTRL => self.pulse1.write_control(value),
reg::PULSE1_SWEEP => self.pulse1.write_sweep(value),
reg::PULSE1_LO => self.pulse1.write_period_low(value),
reg::PULSE1_HI => self.pulse1.write_period_high(value),
reg::PULSE2_CTRL => self.pulse2.write_control(value),
reg::PULSE2_SWEEP => self.pulse2.write_sweep(value),
reg::PULSE2_LO => self.pulse2.write_period_low(value),
reg::PULSE2_HI => self.pulse2.write_period_high(value),
reg::TRI_LINEAR => self.triangle.write_linear(value),
reg::TRI_LO => self.triangle.write_period_low(value),
reg::TRI_HI => self.triangle.write_period_high(value),
reg::NOISE_CTRL => self.noise.write_control(value),
reg::NOISE_PERIOD => self.noise.write_period(value),
reg::NOISE_LEN => self.noise.write_length(value),
reg::DMC_CTRL => self.dmc.write_control(value),
reg::DMC_LOAD => self.dmc.write_output(value),
reg::DMC_ADDR => self.dmc.write_address(value),
reg::DMC_LEN => self.dmc.write_length(value),
reg::STATUS => self.write_status(value),
reg::FRAME => self.frame.write(value, self.on_put_cycle()),
_ => {}
}
}
fn write_status(&mut self, value: u8) {
self.pulse1.length.set_enabled(value & 0x01 != 0);
self.pulse2.length.set_enabled(value & 0x02 != 0);
self.triangle.length.set_enabled(value & 0x04 != 0);
self.noise.length.set_enabled(value & 0x08 != 0);
self.dmc.clear_irq();
self.dmc.set_enabled(value & 0x10 != 0);
}
fn read_status(&mut self, open_bus: u8, peek: bool) -> u8 {
let mut value = open_bus & 0x20;
if self.pulse1.length.active() {
value |= 0x01;
}
if self.pulse2.length.active() {
value |= 0x02;
}
if self.triangle.length.active() {
value |= 0x04;
}
if self.noise.length.active() {
value |= 0x08;
}
if self.dmc.active() {
value |= 0x10;
}
if self.frame.read_irq(self.ticks, peek) {
value |= 0x40;
}
if self.dmc.irq() {
value |= 0x80;
}
value
}
fn reset(&mut self, kind: ResetKind, timing: Timing, halt_ultrasonic: bool) {
match kind {
ResetKind::Cold => {
let phase = self.phase;
let capacity = self.samples.capacity();
*self = Core::new(timing, phase, halt_ultrasonic, capacity);
}
ResetKind::Warm | ResetKind::Bus => {
self.write_status(0x00);
self.frame.reset_warm();
self.dmc.reset_warm();
self.triangle.reset_phase();
self.samples.clear();
}
}
}
fn save(&self, w: &mut dyn crate::core::state::Sink) -> Result<()> {
self.frame.save(w)?;
self.pulse1.save(w)?;
self.pulse2.save(w)?;
self.triangle.save(w)?;
self.noise.save(w)?;
self.dmc.save(w)?;
w.write_u64(self.ticks)?;
w.write_u64(self.phase)
}
fn load<'a>(&mut self, r: &mut dyn crate::core::state::Source<'a>) -> Result<()> {
self.frame.load(r)?;
self.pulse1.load(r)?;
self.pulse2.load(r)?;
self.triangle.load(r)?;
self.noise.load(r)?;
self.dmc.load(r)?;
self.ticks = r.read_u64()?;
self.phase = r.read_u64()? & 1;
self.samples.clear();
Ok(())
}
}
struct ApuState {
core: Mutex<Core>,
irq: Mutex<Option<WireSource>>,
open_bus: AtomicU8,
domain: Mutex<Option<DomainId>>,
timing: Timing,
halt_ultrasonic: bool,
}
impl fmt::Debug for ApuState {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ApuState")
.field("timing", &self.timing)
.field("open_bus", &self.open_bus.load(Ordering::Relaxed))
.finish_non_exhaustive()
}
}
impl ApuState {
fn irq_level(&self) -> Level {
if self.core.lock().irq_asserted() {
Level::High
} else {
Level::Low
}
}
fn refresh_irq(&self) {
let level = self.irq_level();
let port = self.irq.lock().clone();
if let Some(port) = port {
port.set(level);
}
}
fn write(&self, index: u8, value: u8) {
self.core.lock().write(index, value);
self.refresh_irq();
}
fn read_with(&self, index: u8, peek: bool) -> u8 {
let open_bus = self.open_bus.load(Ordering::Relaxed);
if index != reg::STATUS {
return open_bus;
}
let value = self.core.lock().read_status(open_bus, peek);
if !peek {
self.refresh_irq();
}
value
}
}
#[derive(Debug)]
pub struct Apu {
state: Arc<ApuState>,
}
impl Apu {
pub fn new(props: &Props) -> Result<Apu> {
let mut reader = props.reader();
let timing_name = reader.or_str("timing", "ntsc")?;
let timing = Timing::from_name(timing_name).ok_or_else(|| {
Error::Property(alloc::format!(
"property `timing` must be `ntsc` or `pal`, not `{timing_name}`"
))
})?;
let capacity = reader.or_range::<u64>("sample-buffer", 8192, 0..=1 << 24)?;
let halt_ultrasonic = reader.or("halt-ultrasonic", false)?;
let phase = reader.or_range::<u64>("put-phase", 0, 0..=1)?;
reader.finish()?;
Ok(Apu {
state: Arc::new(ApuState {
core: Mutex::with_rank(
LockRank::DEVICE,
Core::new(timing, phase, halt_ultrasonic, capacity as usize),
),
irq: Mutex::with_rank(LockRank::WIRE, None),
open_bus: AtomicU8::new(0),
domain: Mutex::with_rank(LockRank::LEAF, None),
timing,
halt_ultrasonic,
}),
})
}
pub fn timing(&self) -> Timing {
self.state.timing
}
pub fn connect_irq(&self, source: WireSource) {
*self.state.irq.lock() = Some(source);
self.refresh_irq();
}
pub fn irq_level(&self) -> Level {
self.state.irq_level()
}
fn refresh_irq(&self) {
self.state.refresh_irq();
}
pub fn attach_clock(&self, domain: DomainId) {
*self.state.domain.lock() = Some(domain);
}
pub fn clock_domain(&self) -> Option<DomainId> {
*self.state.domain.lock()
}
pub fn ticks(&self) -> u64 {
self.state.core.lock().ticks
}
pub fn frame_mode(&self) -> Mode {
self.state.core.lock().frame.mode()
}
pub fn frame_cycle(&self) -> u32 {
self.state.core.lock().frame.cycle()
}
pub fn dmc_output(&self) -> u8 {
self.state.core.lock().dmc.output()
}
pub fn advance(&self, cycles: u64) {
{
let mut core = self.state.core.lock();
for _ in 0..cycles {
core.tick();
}
}
self.refresh_irq();
}
pub fn advance_to(&self, tick: u64) {
{
let mut core = self.state.core.lock();
while core.ticks < tick {
core.tick();
}
}
self.refresh_irq();
}
pub fn write(&self, index: u8, value: u8) {
self.state.write(index, value);
}
pub fn read(&self, index: u8) -> u8 {
self.read_with(index, false)
}
pub fn peek(&self, index: u8) -> u8 {
self.read_with(index, true)
}
fn read_with(&self, index: u8, peek: bool) -> u8 {
self.state.read_with(index, peek)
}
pub fn set_open_bus(&self, value: u8) {
self.state.open_bus.store(value, Ordering::Relaxed);
}
pub fn open_bus(&self) -> u8 {
self.state.open_bus.load(Ordering::Relaxed)
}
pub fn dma_request(&self) -> Option<DmaRequest> {
self.state.core.lock().dmc.dma_request()
}
pub fn dma_is_pending(&self, serial: u64) -> bool {
self.state.core.lock().dmc.dma_is_pending(serial)
}
pub fn dma_complete(&self, serial: u64, byte: u8) -> bool {
let accepted = self.state.core.lock().dmc.dma_complete(serial, byte);
if accepted {
self.refresh_irq();
}
accepted
}
pub fn output(&self) -> u16 {
self.state.core.lock().mix()
}
pub fn take_samples(&self, out: &mut Vec<u16>) {
self.state.core.lock().samples.drain_into(out);
}
pub fn samples_dropped(&self) -> u64 {
self.state.core.lock().samples.dropped()
}
pub fn regions(&self) -> Vec<(u64, Region)> {
alloc::vec![
(
0x00,
Region::io("apu.channels", 0x14, self.port(reg::PULSE1_CTRL)),
),
(0x15, Region::io("apu.status", 1, self.port(reg::STATUS))),
(0x17, Region::io("apu.frame", 1, self.port(reg::FRAME))),
]
}
fn port(&self, first: u8) -> Arc<dyn MemOps> {
Arc::new(ApuPort {
state: Arc::clone(&self.state),
first,
})
}
}
struct ApuPort {
state: Arc<ApuState>,
first: u8,
}
impl fmt::Debug for ApuPort {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ApuPort")
.field("first", &self.first)
.finish_non_exhaustive()
}
}
impl ApuPort {
fn index(&self, offset: u64) -> Option<u8> {
u8::try_from(offset)
.ok()
.and_then(|o| self.first.checked_add(o))
}
}
impl MemOps for ApuPort {
fn read(&self, offset: u64, dst: &mut [u8], attrs: MemAttrs) -> MemResult {
let [byte] = dst else {
return Err(crate::core::error::BusError::BadAccess);
};
let index = self
.index(offset)
.ok_or(crate::core::error::BusError::BadAccess)?;
*byte = self.state.read_with(index, attrs.debug);
Ok(())
}
fn write(&self, offset: u64, src: &[u8], attrs: MemAttrs) -> MemResult {
let [value] = src else {
return Err(crate::core::error::BusError::BadAccess);
};
let index = self
.index(offset)
.ok_or(crate::core::error::BusError::BadAccess)?;
if attrs.debug {
return Ok(());
}
self.state.write(index, *value);
Ok(())
}
fn constraints(&self) -> AccessConstraints {
AccessConstraints::word(Width::U8, Endian::Little)
}
}
impl Device for Apu {
fn class(&self) -> &'static DeviceClass {
&APU_CLASS
}
fn realize(&self, _ctx: &mut RealizeCtx<'_>) -> Result<()> {
Ok(())
}
fn reset(&self, kind: ResetKind) {
{
let mut core = self.state.core.lock();
let timing = self.state.timing;
let halt = self.state.halt_ultrasonic;
core.reset(kind, timing, halt);
}
self.refresh_irq();
}
fn save(&self, w: &mut ChunkWriter<'_>) -> Result<()> {
self.state.core.lock().save(w)
}
fn load(&self, r: &mut ChunkReader<'_>) -> Result<()> {
self.state.core.lock().load(r)?;
self.refresh_irq();
Ok(())
}
}
static APU_PROPERTIES: &[PropertySpec] = &[
PropertySpec {
name: "timing",
kind: ValueKind::Str,
required: false,
summary: "console variant: `ntsc` (RP2A03) or `pal` (RP2A07)",
},
PropertySpec {
name: "sample-buffer",
kind: ValueKind::Uint,
required: false,
summary: "audio ring capacity in samples; 0 produces no audio at all",
},
PropertySpec {
name: "halt-ultrasonic",
kind: ValueKind::Bool,
required: false,
summary: "halt the triangle when its period is below 2, trading accuracy for less popping",
},
PropertySpec {
name: "put-phase",
kind: ValueKind::Uint,
required: false,
summary: "CPU/APU cycle alignment at power-on (0 or 1); random on hardware",
},
];
pub static APU_CLASS: DeviceClass = DeviceClass {
name: "nes.apu",
version: 1,
summary: "NES APU (RP2A03 audio): two pulse, triangle, noise and DMC channels",
properties: APU_PROPERTIES,
construct: |props| Ok(Box::new(Apu::new(props)?) as Box<dyn Device>),
};
pub fn register(registry: &mut Registry) -> Result<()> {
registry.add(&APU_CLASS)
}