878 lines
17 KiB
Zig
878 lines
17 KiB
Zig
const std = @import("std");
|
|
|
|
pub const Dmc = @This();
|
|
|
|
pub const Region = @import("../common.zig").Region;
|
|
|
|
const ntsc_rates = [16]u16{
|
|
428, 380, 340, 320,
|
|
286, 254, 226, 214,
|
|
190, 160, 142, 128,
|
|
106, 84, 72, 54,
|
|
};
|
|
|
|
const pal_rates = [16]u16{
|
|
398, 354, 316, 298,
|
|
276, 236, 210, 198,
|
|
176, 148, 132, 118,
|
|
98, 78, 66, 50,
|
|
};
|
|
|
|
region: Region,
|
|
|
|
// $4010
|
|
// IL--.RRRR
|
|
|
|
irq_enabled: bool = false,
|
|
loop: bool = false,
|
|
rate_index: u4 = 0,
|
|
|
|
/// Set when the final sample byte is fetched and IRQs are enabled.
|
|
irq_flag: bool = false,
|
|
|
|
// $4011
|
|
// -DDD.DDDD
|
|
|
|
/// 7-bit DAC output level.
|
|
///
|
|
/// This is always sent to the mixer, even if DMC playback is disabled.
|
|
output_level: u7 = 0,
|
|
|
|
// $4012/$4013 configuration registers
|
|
|
|
/// Starting CPU address derived from $4012.
|
|
sample_address: u16 = 0xC000,
|
|
|
|
/// Number of bytes derived from $4013.
|
|
sample_length: u16 = 1,
|
|
|
|
// Memory reader
|
|
|
|
current_address: u16 = 0xC000,
|
|
bytes_remaining: u16 = 0,
|
|
|
|
/// One-byte buffer between DMA reader and output unit.
|
|
sample_buffer: ?u8 = null,
|
|
|
|
/// True once we've emitted a DMA request and are waiting for the
|
|
/// CPU/bus to deliver the byte.
|
|
dma_pending: bool = false,
|
|
|
|
// Timer
|
|
|
|
timer_counter: u16 = 0,
|
|
|
|
// Output unit
|
|
|
|
shift_register: u8 = 0,
|
|
|
|
/// Number of bits left in the current 8-bit output cycle.
|
|
bits_remaining: u4 = 8,
|
|
|
|
/// When true, timer clocks do not modify output_level.
|
|
///
|
|
/// The shifter and bit counter still continue to clock.
|
|
silence: bool = true,
|
|
|
|
pub fn init(region: Region) Dmc {
|
|
return .{
|
|
.region = region,
|
|
};
|
|
}
|
|
|
|
// Register writes
|
|
|
|
/// $4010
|
|
///
|
|
/// IL--.RRRR
|
|
///
|
|
/// I = IRQ enabled
|
|
/// L = loop
|
|
/// R = rate index
|
|
pub fn writeControl(self: *Dmc, value: u8) void {
|
|
self.irq_enabled = (value & 0x80) != 0;
|
|
self.loop = (value & 0x40) != 0;
|
|
self.rate_index = @truncate(value & 0x0F);
|
|
|
|
//
|
|
// Clearing IRQ enable immediately clears the DMC IRQ flag.
|
|
//
|
|
if (!self.irq_enabled) {
|
|
self.irq_flag = false;
|
|
}
|
|
|
|
//
|
|
// Do NOT reset timer_counter here.
|
|
//
|
|
}
|
|
|
|
/// $4011
|
|
///
|
|
/// Directly sets the 7-bit DAC.
|
|
pub fn writeDirectLoad(self: *Dmc, value: u8) void {
|
|
self.output_level = @truncate(value & 0x7F);
|
|
}
|
|
|
|
/// $4012
|
|
///
|
|
/// address = $C000 + value * 64
|
|
///
|
|
/// = %11AAAAAA.AA000000
|
|
pub fn writeSampleAddress(self: *Dmc, value: u8) void {
|
|
self.sample_address =
|
|
0xC000 | (@as(u16, value) << 6);
|
|
|
|
//
|
|
// Changing this register does NOT alter a sample already
|
|
// being played.
|
|
//
|
|
}
|
|
|
|
/// $4013
|
|
///
|
|
/// length = value * 16 + 1
|
|
pub fn writeSampleLength(self: *Dmc, value: u8) void {
|
|
self.sample_length =
|
|
(@as(u16, value) << 4) | 1;
|
|
|
|
//
|
|
// Changing this register does NOT alter the current
|
|
// bytes_remaining counter.
|
|
//
|
|
}
|
|
|
|
// $4015
|
|
|
|
/// Called when $4015 is written.
|
|
///
|
|
/// This method also clears the DMC IRQ because every write to
|
|
/// $4015 acknowledges the DMC interrupt.
|
|
pub fn writeEnabled(self: *Dmc, enabled: bool) void {
|
|
self.irq_flag = false;
|
|
|
|
if (!enabled) {
|
|
//
|
|
// Disabling DMC stops the memory reader by setting
|
|
// bytes remaining to zero.
|
|
//
|
|
// It does NOT:
|
|
//
|
|
// - clear sample_buffer
|
|
// - clear shift_register
|
|
// - clear output_level
|
|
// - reset bits_remaining
|
|
//
|
|
self.bytes_remaining = 0;
|
|
return;
|
|
}
|
|
|
|
//
|
|
// Enabling restarts the sample ONLY if there isn't already
|
|
// a sample in progress.
|
|
//
|
|
if (self.bytes_remaining == 0) {
|
|
self.restartSample();
|
|
}
|
|
}
|
|
|
|
/// Bit 4 returned by $4015.
|
|
///
|
|
/// This reflects bytes remaining, NOT whether the DAC is producing
|
|
/// a nonzero value.
|
|
pub fn active(self: *const Dmc) bool {
|
|
return self.bytes_remaining != 0;
|
|
}
|
|
|
|
pub fn irqAsserted(self: *const Dmc) bool {
|
|
return self.irq_flag;
|
|
}
|
|
|
|
// Sample restart
|
|
|
|
fn restartSample(self: *Dmc) void {
|
|
self.current_address = self.sample_address;
|
|
self.bytes_remaining = self.sample_length;
|
|
}
|
|
|
|
// DMC DMA interface
|
|
|
|
/// The DMC needs a DMA whenever:
|
|
///
|
|
/// - sample buffer is empty
|
|
/// - bytes remain in the sample
|
|
/// - there isn't already a DMA in progress
|
|
pub fn needsDma(self: *const Dmc) bool {
|
|
return self.sample_buffer == null and
|
|
self.bytes_remaining != 0 and
|
|
!self.dma_pending;
|
|
}
|
|
|
|
/// Start a DMA request.
|
|
///
|
|
/// Returns the CPU address that must be read.
|
|
///
|
|
/// The CPU/Bus layer should:
|
|
///
|
|
/// 1. stall the CPU appropriately
|
|
/// 2. read this address
|
|
/// 3. call completeDma(value)
|
|
///
|
|
pub fn beginDma(self: *Dmc) ?u16 {
|
|
if (!self.needsDma())
|
|
return null;
|
|
|
|
self.dma_pending = true;
|
|
|
|
return self.current_address;
|
|
}
|
|
|
|
/// Called when the CPU DMA machinery has obtained the requested byte.
|
|
pub fn completeDma(self: *Dmc, value: u8) void {
|
|
std.debug.assert(self.dma_pending);
|
|
|
|
self.dma_pending = false;
|
|
|
|
//
|
|
// Reader fills the one-byte sample buffer.
|
|
//
|
|
self.sample_buffer = value;
|
|
|
|
//
|
|
// Advance sample address.
|
|
//
|
|
// DMC has a special wrap:
|
|
//
|
|
// $FFFF -> $8000
|
|
//
|
|
// NOT:
|
|
//
|
|
// $FFFF -> $0000
|
|
//
|
|
if (self.current_address == 0xFFFF) {
|
|
self.current_address = 0x8000;
|
|
} else {
|
|
self.current_address += 1;
|
|
}
|
|
|
|
std.debug.assert(self.bytes_remaining != 0);
|
|
self.bytes_remaining -= 1;
|
|
|
|
if (self.bytes_remaining == 0) {
|
|
if (self.loop) {
|
|
//
|
|
// Looping immediately reloads address/length.
|
|
//
|
|
// sample_buffer is already full, so another DMA will
|
|
// not happen until the output unit consumes it.
|
|
//
|
|
self.restartSample();
|
|
} else if (self.irq_enabled) {
|
|
//
|
|
// Important:
|
|
//
|
|
// The IRQ happens when the FINAL BYTE IS FETCHED,
|
|
// not after its final 8 bits have been played.
|
|
//
|
|
self.irq_flag = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Timer
|
|
|
|
pub fn timerPeriod(self: *const Dmc) u16 {
|
|
const index: usize = @intCast(self.rate_index);
|
|
|
|
return switch (self.region) {
|
|
.ntsc => ntsc_rates[index],
|
|
.pal => pal_rates[index],
|
|
};
|
|
}
|
|
|
|
/// Clock once per CPU cycle.
|
|
pub fn clockTimer(self: *Dmc) void {
|
|
if (self.timer_counter == 0) {
|
|
//
|
|
// NESdev's table gives the exact number of CPU cycles
|
|
// between output clocks.
|
|
//
|
|
// Therefore reload with period - 1.
|
|
//
|
|
self.timer_counter = self.timerPeriod() - 1;
|
|
|
|
self.clockOutputUnit();
|
|
} else {
|
|
self.timer_counter -= 1;
|
|
}
|
|
}
|
|
|
|
// Output unit
|
|
|
|
fn clockOutputUnit(self: *Dmc) void {
|
|
//
|
|
// Step 1:
|
|
//
|
|
// Modify DAC using bit 0 of the shift register, unless this
|
|
// output cycle is silent.
|
|
//
|
|
if (!self.silence) {
|
|
if ((self.shift_register & 0x01) != 0) {
|
|
//
|
|
// Delta bit 1 => +2
|
|
//
|
|
// Don't wrap beyond 127.
|
|
//
|
|
if (self.output_level <= 125) {
|
|
self.output_level += 2;
|
|
}
|
|
} else {
|
|
//
|
|
// Delta bit 0 => -2
|
|
//
|
|
// Don't wrap below zero.
|
|
//
|
|
if (self.output_level >= 2) {
|
|
self.output_level -= 2;
|
|
}
|
|
}
|
|
}
|
|
|
|
//
|
|
// Step 2:
|
|
//
|
|
// Shift regardless of silence.
|
|
//
|
|
self.shift_register >>= 1;
|
|
|
|
//
|
|
// Step 3:
|
|
//
|
|
// Advance bit counter.
|
|
//
|
|
self.bits_remaining -= 1;
|
|
|
|
//
|
|
// An output cycle consists of exactly 8 timer clocks.
|
|
//
|
|
if (self.bits_remaining == 0) {
|
|
self.startOutputCycle();
|
|
}
|
|
}
|
|
|
|
fn startOutputCycle(self: *Dmc) void {
|
|
self.bits_remaining = 8;
|
|
|
|
if (self.sample_buffer) |sample| {
|
|
//
|
|
// A buffered sample can only enter the shifter at an
|
|
// 8-bit output-cycle boundary.
|
|
//
|
|
self.shift_register = sample;
|
|
self.sample_buffer = null;
|
|
self.silence = false;
|
|
|
|
//
|
|
// sample_buffer has just become empty.
|
|
//
|
|
// needsDma() will now become true if there are more bytes.
|
|
//
|
|
} else {
|
|
//
|
|
// No sample available.
|
|
//
|
|
// The output unit continues ticking, but it stops modifying
|
|
// the DAC.
|
|
//
|
|
self.silence = true;
|
|
}
|
|
}
|
|
|
|
// Mixer
|
|
|
|
/// Current DMC mixer input.
|
|
///
|
|
/// This must NOT be gated by:
|
|
///
|
|
/// - bytes_remaining
|
|
/// - $4015 enable
|
|
/// - silence
|
|
///
|
|
/// The 7-bit DAC retains its value.
|
|
pub fn output(self: *const Dmc) u7 {
|
|
return self.output_level;
|
|
}
|
|
|
|
// Tests
|
|
|
|
test "$4010 decodes IRQ loop and rate" {
|
|
var dmc = Dmc.init(.ntsc);
|
|
|
|
dmc.writeControl(0b1100_1010);
|
|
|
|
try std.testing.expect(dmc.irq_enabled);
|
|
try std.testing.expect(dmc.loop);
|
|
try std.testing.expectEqual(
|
|
@as(u4, 10),
|
|
dmc.rate_index,
|
|
);
|
|
}
|
|
|
|
test "clearing IRQ enable clears IRQ flag" {
|
|
var dmc = Dmc.init(.ntsc);
|
|
|
|
dmc.irq_flag = true;
|
|
|
|
dmc.writeControl(0x00);
|
|
|
|
try std.testing.expect(!dmc.irq_flag);
|
|
}
|
|
|
|
test "$4011 directly sets output level" {
|
|
var dmc = Dmc.init(.ntsc);
|
|
|
|
dmc.writeDirectLoad(0x55);
|
|
|
|
try std.testing.expectEqual(
|
|
@as(u7, 0x55),
|
|
dmc.output(),
|
|
);
|
|
}
|
|
|
|
test "$4011 ignores bit seven" {
|
|
var dmc = Dmc.init(.ntsc);
|
|
|
|
dmc.writeDirectLoad(0xFF);
|
|
|
|
try std.testing.expectEqual(
|
|
@as(u7, 127),
|
|
dmc.output(),
|
|
);
|
|
}
|
|
|
|
test "$4012 sample address calculation" {
|
|
var dmc = Dmc.init(.ntsc);
|
|
|
|
dmc.writeSampleAddress(0x00);
|
|
|
|
try std.testing.expectEqual(
|
|
@as(u16, 0xC000),
|
|
dmc.sample_address,
|
|
);
|
|
|
|
dmc.writeSampleAddress(0x01);
|
|
|
|
try std.testing.expectEqual(
|
|
@as(u16, 0xC040),
|
|
dmc.sample_address,
|
|
);
|
|
|
|
dmc.writeSampleAddress(0xFF);
|
|
|
|
try std.testing.expectEqual(
|
|
@as(u16, 0xFFC0),
|
|
dmc.sample_address,
|
|
);
|
|
}
|
|
|
|
test "$4013 sample length calculation" {
|
|
var dmc = Dmc.init(.ntsc);
|
|
|
|
dmc.writeSampleLength(0x00);
|
|
|
|
try std.testing.expectEqual(
|
|
@as(u16, 1),
|
|
dmc.sample_length,
|
|
);
|
|
|
|
dmc.writeSampleLength(0x01);
|
|
|
|
try std.testing.expectEqual(
|
|
@as(u16, 17),
|
|
dmc.sample_length,
|
|
);
|
|
|
|
dmc.writeSampleLength(0xFF);
|
|
|
|
try std.testing.expectEqual(
|
|
@as(u16, 4081),
|
|
dmc.sample_length,
|
|
);
|
|
}
|
|
|
|
test "enabling starts sample if no bytes remain" {
|
|
var dmc = Dmc.init(.ntsc);
|
|
|
|
dmc.writeSampleAddress(0x20);
|
|
dmc.writeSampleLength(0x02);
|
|
|
|
dmc.writeEnabled(true);
|
|
|
|
try std.testing.expectEqual(
|
|
dmc.sample_address,
|
|
dmc.current_address,
|
|
);
|
|
|
|
try std.testing.expectEqual(
|
|
dmc.sample_length,
|
|
dmc.bytes_remaining,
|
|
);
|
|
}
|
|
|
|
test "enabling does not restart active sample" {
|
|
var dmc = Dmc.init(.ntsc);
|
|
|
|
dmc.current_address = 0xD123;
|
|
dmc.bytes_remaining = 5;
|
|
|
|
dmc.writeEnabled(true);
|
|
|
|
try std.testing.expectEqual(
|
|
@as(u16, 0xD123),
|
|
dmc.current_address,
|
|
);
|
|
|
|
try std.testing.expectEqual(
|
|
@as(u16, 5),
|
|
dmc.bytes_remaining,
|
|
);
|
|
}
|
|
|
|
test "disabling clears bytes remaining" {
|
|
var dmc = Dmc.init(.ntsc);
|
|
|
|
dmc.bytes_remaining = 123;
|
|
|
|
dmc.writeEnabled(false);
|
|
|
|
try std.testing.expectEqual(
|
|
@as(u16, 0),
|
|
dmc.bytes_remaining,
|
|
);
|
|
}
|
|
|
|
test "disabling does not clear DAC output" {
|
|
var dmc = Dmc.init(.ntsc);
|
|
|
|
dmc.output_level = 70;
|
|
dmc.bytes_remaining = 10;
|
|
|
|
dmc.writeEnabled(false);
|
|
|
|
try std.testing.expectEqual(
|
|
@as(u7, 70),
|
|
dmc.output(),
|
|
);
|
|
}
|
|
|
|
test "$4015 write clears DMC IRQ" {
|
|
var dmc = Dmc.init(.ntsc);
|
|
|
|
dmc.irq_flag = true;
|
|
|
|
dmc.writeEnabled(true);
|
|
|
|
try std.testing.expect(!dmc.irq_flag);
|
|
}
|
|
|
|
test "active is based only on bytes remaining" {
|
|
var dmc = Dmc.init(.ntsc);
|
|
|
|
dmc.bytes_remaining = 0;
|
|
dmc.sample_buffer = 0xAA;
|
|
|
|
try std.testing.expect(!dmc.active());
|
|
|
|
dmc.bytes_remaining = 1;
|
|
|
|
try std.testing.expect(dmc.active());
|
|
}
|
|
|
|
test "DMC requests DMA when buffer is empty" {
|
|
var dmc = Dmc.init(.ntsc);
|
|
|
|
dmc.current_address = 0xC123;
|
|
dmc.bytes_remaining = 4;
|
|
|
|
try std.testing.expect(dmc.needsDma());
|
|
|
|
const address = dmc.beginDma();
|
|
|
|
try std.testing.expectEqual(
|
|
@as(?u16, 0xC123),
|
|
address,
|
|
);
|
|
|
|
try std.testing.expect(dmc.dma_pending);
|
|
try std.testing.expect(!dmc.needsDma());
|
|
}
|
|
|
|
test "DMA fills buffer and advances reader" {
|
|
var dmc = Dmc.init(.ntsc);
|
|
|
|
dmc.current_address = 0xC123;
|
|
dmc.bytes_remaining = 4;
|
|
|
|
_ = dmc.beginDma();
|
|
|
|
dmc.completeDma(0xAB);
|
|
|
|
try std.testing.expectEqual(
|
|
@as(?u8, 0xAB),
|
|
dmc.sample_buffer,
|
|
);
|
|
|
|
try std.testing.expectEqual(
|
|
@as(u16, 0xC124),
|
|
dmc.current_address,
|
|
);
|
|
|
|
try std.testing.expectEqual(
|
|
@as(u16, 3),
|
|
dmc.bytes_remaining,
|
|
);
|
|
|
|
try std.testing.expect(!dmc.dma_pending);
|
|
}
|
|
|
|
test "DMA address wraps $FFFF to $8000" {
|
|
var dmc = Dmc.init(.ntsc);
|
|
|
|
dmc.current_address = 0xFFFF;
|
|
dmc.bytes_remaining = 2;
|
|
|
|
_ = dmc.beginDma();
|
|
dmc.completeDma(0x00);
|
|
|
|
try std.testing.expectEqual(
|
|
@as(u16, 0x8000),
|
|
dmc.current_address,
|
|
);
|
|
}
|
|
|
|
test "final fetch raises IRQ" {
|
|
var dmc = Dmc.init(.ntsc);
|
|
|
|
dmc.irq_enabled = true;
|
|
dmc.loop = false;
|
|
|
|
dmc.current_address = 0xC000;
|
|
dmc.bytes_remaining = 1;
|
|
|
|
_ = dmc.beginDma();
|
|
dmc.completeDma(0xAA);
|
|
|
|
try std.testing.expectEqual(
|
|
@as(u16, 0),
|
|
dmc.bytes_remaining,
|
|
);
|
|
|
|
try std.testing.expect(dmc.irq_flag);
|
|
}
|
|
|
|
test "loop restarts sample instead of raising IRQ" {
|
|
var dmc = Dmc.init(.ntsc);
|
|
|
|
dmc.sample_address = 0xD000;
|
|
dmc.sample_length = 17;
|
|
|
|
dmc.current_address = 0xD010;
|
|
dmc.bytes_remaining = 1;
|
|
|
|
dmc.loop = true;
|
|
dmc.irq_enabled = true;
|
|
|
|
_ = dmc.beginDma();
|
|
dmc.completeDma(0xAA);
|
|
|
|
try std.testing.expectEqual(
|
|
@as(u16, 0xD000),
|
|
dmc.current_address,
|
|
);
|
|
|
|
try std.testing.expectEqual(
|
|
@as(u16, 17),
|
|
dmc.bytes_remaining,
|
|
);
|
|
|
|
try std.testing.expect(!dmc.irq_flag);
|
|
}
|
|
|
|
test "sample buffer prevents another DMA" {
|
|
var dmc = Dmc.init(.ntsc);
|
|
|
|
dmc.bytes_remaining = 10;
|
|
dmc.sample_buffer = 0xAA;
|
|
|
|
try std.testing.expect(!dmc.needsDma());
|
|
}
|
|
|
|
test "NTSC rate table" {
|
|
var dmc = Dmc.init(.ntsc);
|
|
|
|
dmc.rate_index = 0;
|
|
try std.testing.expectEqual(
|
|
@as(u16, 428),
|
|
dmc.timerPeriod(),
|
|
);
|
|
|
|
dmc.rate_index = 15;
|
|
try std.testing.expectEqual(
|
|
@as(u16, 54),
|
|
dmc.timerPeriod(),
|
|
);
|
|
}
|
|
|
|
test "PAL rate table" {
|
|
var dmc = Dmc.init(.pal);
|
|
|
|
dmc.rate_index = 0;
|
|
try std.testing.expectEqual(
|
|
@as(u16, 398),
|
|
dmc.timerPeriod(),
|
|
);
|
|
|
|
dmc.rate_index = 15;
|
|
try std.testing.expectEqual(
|
|
@as(u16, 50),
|
|
dmc.timerPeriod(),
|
|
);
|
|
}
|
|
|
|
test "delta one increases output by two" {
|
|
var dmc = Dmc.init(.ntsc);
|
|
|
|
dmc.output_level = 50;
|
|
dmc.shift_register = 0b0000_0001;
|
|
dmc.bits_remaining = 8;
|
|
dmc.silence = false;
|
|
|
|
dmc.clockOutputUnit();
|
|
|
|
try std.testing.expectEqual(
|
|
@as(u7, 52),
|
|
dmc.output_level,
|
|
);
|
|
}
|
|
|
|
test "delta zero decreases output by two" {
|
|
var dmc = Dmc.init(.ntsc);
|
|
|
|
dmc.output_level = 50;
|
|
dmc.shift_register = 0;
|
|
dmc.bits_remaining = 8;
|
|
dmc.silence = false;
|
|
|
|
dmc.clockOutputUnit();
|
|
|
|
try std.testing.expectEqual(
|
|
@as(u7, 48),
|
|
dmc.output_level,
|
|
);
|
|
}
|
|
|
|
test "delta increment does not overflow" {
|
|
var dmc = Dmc.init(.ntsc);
|
|
|
|
dmc.output_level = 126;
|
|
dmc.shift_register = 1;
|
|
dmc.bits_remaining = 8;
|
|
dmc.silence = false;
|
|
|
|
dmc.clockOutputUnit();
|
|
|
|
try std.testing.expectEqual(
|
|
@as(u7, 126),
|
|
dmc.output_level,
|
|
);
|
|
}
|
|
|
|
test "delta decrement does not underflow" {
|
|
var dmc = Dmc.init(.ntsc);
|
|
|
|
dmc.output_level = 1;
|
|
dmc.shift_register = 0;
|
|
dmc.bits_remaining = 8;
|
|
dmc.silence = false;
|
|
|
|
dmc.clockOutputUnit();
|
|
|
|
try std.testing.expectEqual(
|
|
@as(u7, 1),
|
|
dmc.output_level,
|
|
);
|
|
}
|
|
|
|
test "silent cycle does not change DAC" {
|
|
var dmc = Dmc.init(.ntsc);
|
|
|
|
dmc.output_level = 50;
|
|
dmc.shift_register = 1;
|
|
dmc.bits_remaining = 8;
|
|
dmc.silence = true;
|
|
|
|
dmc.clockOutputUnit();
|
|
|
|
try std.testing.expectEqual(
|
|
@as(u7, 50),
|
|
dmc.output_level,
|
|
);
|
|
}
|
|
|
|
test "output shifter shifts right" {
|
|
var dmc = Dmc.init(.ntsc);
|
|
|
|
dmc.shift_register = 0b1010_1011;
|
|
dmc.bits_remaining = 8;
|
|
dmc.silence = false;
|
|
|
|
dmc.clockOutputUnit();
|
|
|
|
try std.testing.expectEqual(
|
|
@as(u8, 0b0101_0101),
|
|
dmc.shift_register,
|
|
);
|
|
}
|
|
|
|
test "new output cycle consumes sample buffer" {
|
|
var dmc = Dmc.init(.ntsc);
|
|
|
|
dmc.bits_remaining = 1;
|
|
dmc.sample_buffer = 0b1010_1010;
|
|
|
|
dmc.clockOutputUnit();
|
|
|
|
try std.testing.expectEqual(
|
|
@as(u4, 8),
|
|
dmc.bits_remaining,
|
|
);
|
|
|
|
try std.testing.expectEqual(
|
|
@as(u8, 0b1010_1010),
|
|
dmc.shift_register,
|
|
);
|
|
|
|
try std.testing.expectEqual(
|
|
@as(?u8, null),
|
|
dmc.sample_buffer,
|
|
);
|
|
|
|
try std.testing.expect(!dmc.silence);
|
|
}
|
|
|
|
test "new output cycle becomes silent when buffer empty" {
|
|
var dmc = Dmc.init(.ntsc);
|
|
|
|
dmc.bits_remaining = 1;
|
|
dmc.sample_buffer = null;
|
|
dmc.silence = false;
|
|
|
|
dmc.clockOutputUnit();
|
|
|
|
try std.testing.expectEqual(
|
|
@as(u4, 8),
|
|
dmc.bits_remaining,
|
|
);
|
|
|
|
try std.testing.expect(dmc.silence);
|
|
}
|