673 lines
14 KiB
Zig
673 lines
14 KiB
Zig
const std = @import("std");
|
|
|
|
const LengthCounter = @import("length_counter.zig");
|
|
|
|
pub const Triangle = @This();
|
|
/// 32-step triangle DAC sequence.
|
|
///
|
|
/// Unlike pulse, the triangle directly sends one of these
|
|
/// 4-bit values to the mixer.
|
|
const sequence = [32]u4{
|
|
15, 14, 13, 12, 11, 10, 9, 8,
|
|
7, 6, 5, 4, 3, 2, 1, 0,
|
|
|
|
0, 1, 2, 3, 4, 5, 6, 7,
|
|
8, 9, 10, 11, 12, 13, 14, 15,
|
|
};
|
|
|
|
length_counter: LengthCounter = .{},
|
|
|
|
// $4008
|
|
// CRRR.RRRR
|
|
|
|
/// Bit 7 of $4008.
|
|
///
|
|
/// This has two functions:
|
|
///
|
|
/// - halts the length counter
|
|
/// - controls whether linear_counter_reload remains set
|
|
control: bool = false,
|
|
|
|
/// Bits 6..0 of $4008.
|
|
linear_reload_value: u7 = 0,
|
|
|
|
/// Current linear counter.
|
|
linear_counter: u7 = 0,
|
|
|
|
/// Set by every write to $400B.
|
|
linear_reload: bool = false,
|
|
|
|
// Timer
|
|
|
|
/// 11-bit timer period:
|
|
///
|
|
/// HHHLLLLLLLL
|
|
timer_period: u16 = 0,
|
|
|
|
/// Internal timer divider.
|
|
///
|
|
/// Triangle timer runs at the CPU clock, unlike pulse timers
|
|
/// which run at CPU / 2.
|
|
timer_counter: u16 = 0,
|
|
|
|
// Sequencer
|
|
|
|
/// Current position in the 32-step waveform.
|
|
sequence_position: u5 = 0,
|
|
|
|
pub fn init() Triangle {
|
|
return .{};
|
|
}
|
|
|
|
// $4015
|
|
|
|
/// Triangle is bit 2 of $4015.
|
|
///
|
|
/// Clearing it immediately clears the length counter.
|
|
///
|
|
/// Importantly, this does NOT reset the sequencer or linear counter.
|
|
pub fn setEnabled(self: *Triangle, enabled: bool) void {
|
|
self.length_counter.setEnabled(enabled);
|
|
}
|
|
|
|
/// Status returned as bit 2 when reading $4015.
|
|
///
|
|
/// $4015 status reports whether the length counter is non-zero.
|
|
pub fn active(self: *const Triangle) bool {
|
|
return self.length_counter.active();
|
|
}
|
|
|
|
// $4008
|
|
// CRRR.RRRR
|
|
//
|
|
// C = control / length-counter halt
|
|
// R = linear-counter reload value
|
|
|
|
pub fn writeLinear(self: *Triangle, value: u8) void {
|
|
self.control = (value & 0x80) != 0;
|
|
self.linear_reload_value = @truncate(value & 0x7f);
|
|
|
|
//
|
|
// IMPORTANT:
|
|
//
|
|
// Writing $4008 itself does NOT set linear_reload.
|
|
//
|
|
// $400B does that.
|
|
//
|
|
}
|
|
|
|
// $400A
|
|
// LLLL.LLLL
|
|
|
|
pub fn writeTimerLow(self: *Triangle, value: u8) void {
|
|
self.timer_period =
|
|
(self.timer_period & 0x0700) |
|
|
@as(u16, value);
|
|
}
|
|
|
|
// $400B
|
|
// LLLL.LHHH
|
|
//
|
|
// bits 7..3 = length counter index
|
|
// bits 2..0 = high timer bits
|
|
//
|
|
// Side effect:
|
|
// set linear-counter reload flag
|
|
|
|
pub fn writeTimerHigh(self: *Triangle, value: u8) void {
|
|
self.timer_period =
|
|
(self.timer_period & 0x00ff) |
|
|
(@as(u16, value & 0x07) << 8);
|
|
|
|
const length_index: u5 = @truncate(value >> 3);
|
|
self.length_counter.load(length_index);
|
|
|
|
//
|
|
// Every $400B write sets the reload flag.
|
|
//
|
|
self.linear_reload = true;
|
|
|
|
//
|
|
// Unlike pulse $4003/$4007:
|
|
//
|
|
// DO NOT reset sequence_position.
|
|
//
|
|
// DO NOT reset timer_counter.
|
|
//
|
|
}
|
|
|
|
// $4009
|
|
|
|
/// $4009 is unused.
|
|
pub fn writeUnused(_: *Triangle, _: u8) void {}
|
|
|
|
// Linear counter
|
|
|
|
/// Clocked on every quarter-frame.
|
|
///
|
|
/// NES behavior, in this exact order:
|
|
///
|
|
/// 1. If reload flag set:
|
|
/// counter = reload value
|
|
///
|
|
/// Otherwise, if counter > 0:
|
|
/// counter--
|
|
///
|
|
/// 2. If control flag clear:
|
|
/// reload flag = false
|
|
///
|
|
pub fn clockQuarterFrame(self: *Triangle) void {
|
|
if (self.linear_reload) {
|
|
self.linear_counter = self.linear_reload_value;
|
|
} else if (self.linear_counter != 0) {
|
|
self.linear_counter -= 1;
|
|
}
|
|
|
|
if (!self.control) {
|
|
self.linear_reload = false;
|
|
}
|
|
}
|
|
|
|
// Length counter
|
|
|
|
/// Clocked on every half-frame.
|
|
pub fn clockHalfFrame(self: *Triangle) void {
|
|
//
|
|
// $4008 bit 7 is also the length-counter halt flag.
|
|
//
|
|
self.length_counter.clock(self.control);
|
|
}
|
|
|
|
// Timer / waveform sequencer
|
|
|
|
/// Clock EVERY CPU cycle.
|
|
///
|
|
/// This differs from Pulse:
|
|
///
|
|
/// Pulse timer -> CPU / 2
|
|
/// Triangle timer -> CPU
|
|
///
|
|
pub fn clockTimer(self: *Triangle) void {
|
|
if (self.timer_counter == 0) {
|
|
self.timer_counter = self.timer_period;
|
|
|
|
//
|
|
// The waveform advances only when BOTH counters are non-zero.
|
|
//
|
|
if (self.linear_counter != 0 and
|
|
self.length_counter.active())
|
|
{
|
|
self.sequence_position +%= 1;
|
|
}
|
|
} else {
|
|
self.timer_counter -= 1;
|
|
}
|
|
}
|
|
|
|
// Output
|
|
|
|
/// Current 4-bit DAC output.
|
|
///
|
|
/// IMPORTANT:
|
|
///
|
|
/// Do NOT do this:
|
|
///
|
|
/// if (linear_counter == 0) return 0;
|
|
///
|
|
/// and do NOT do:
|
|
///
|
|
/// if (length_counter == 0) return 0;
|
|
///
|
|
/// Those counters stop the waveform sequencer. They do not force
|
|
/// the triangle DAC to zero.
|
|
///
|
|
/// When halted, the channel retains its current sequence value.
|
|
pub fn output(self: *const Triangle) u4 {
|
|
return sequence[@intCast(self.sequence_position)];
|
|
}
|
|
|
|
pub fn sequencerRunning(self: *const Triangle) bool {
|
|
return self.linear_counter != 0 and
|
|
self.length_counter.active();
|
|
}
|
|
|
|
// Inspection helpers
|
|
|
|
pub fn timerPeriod(self: *const Triangle) u16 {
|
|
return self.timer_period;
|
|
}
|
|
|
|
pub fn length(self: *const Triangle) u8 {
|
|
return self.length_counter.value;
|
|
}
|
|
|
|
pub fn linear(self: *const Triangle) u7 {
|
|
return self.linear_counter;
|
|
}
|
|
|
|
pub fn sequencePosition(self: *const Triangle) u5 {
|
|
return self.sequence_position;
|
|
}
|
|
|
|
// Tests
|
|
|
|
test "$4008 sets control and reload value" {
|
|
var triangle = Triangle.init();
|
|
|
|
triangle.writeLinear(0b1010_1010);
|
|
|
|
try std.testing.expect(triangle.control);
|
|
|
|
try std.testing.expectEqual(
|
|
@as(u7, 0x2a),
|
|
triangle.linear_reload_value,
|
|
);
|
|
}
|
|
|
|
test "$4008 does not set reload flag" {
|
|
var triangle = Triangle.init();
|
|
|
|
triangle.writeLinear(0xff);
|
|
|
|
try std.testing.expect(!triangle.linear_reload);
|
|
}
|
|
|
|
test "$400A sets timer low bits" {
|
|
var triangle = Triangle.init();
|
|
|
|
triangle.timer_period = 0x500;
|
|
|
|
triangle.writeTimerLow(0xab);
|
|
|
|
try std.testing.expectEqual(
|
|
@as(u16, 0x5ab),
|
|
triangle.timer_period,
|
|
);
|
|
}
|
|
|
|
test "$400B sets timer high bits" {
|
|
var triangle = Triangle.init();
|
|
|
|
triangle.timer_period = 0x0ab;
|
|
|
|
triangle.writeTimerHigh(0x05);
|
|
|
|
try std.testing.expectEqual(
|
|
@as(u16, 0x5ab),
|
|
triangle.timer_period,
|
|
);
|
|
}
|
|
|
|
test "$400B sets linear reload flag" {
|
|
var triangle = Triangle.init();
|
|
|
|
try std.testing.expect(!triangle.linear_reload);
|
|
|
|
triangle.writeTimerHigh(0);
|
|
|
|
try std.testing.expect(triangle.linear_reload);
|
|
}
|
|
|
|
test "$400B does not reset sequencer position" {
|
|
var triangle = Triangle.init();
|
|
|
|
triangle.sequence_position = 17;
|
|
|
|
triangle.writeTimerHigh(0);
|
|
|
|
try std.testing.expectEqual(
|
|
@as(u5, 17),
|
|
triangle.sequence_position,
|
|
);
|
|
}
|
|
|
|
test "$400B does not reset timer divider" {
|
|
var triangle = Triangle.init();
|
|
|
|
triangle.timer_counter = 123;
|
|
|
|
triangle.writeTimerHigh(0x05);
|
|
|
|
try std.testing.expectEqual(
|
|
@as(u16, 123),
|
|
triangle.timer_counter,
|
|
);
|
|
}
|
|
|
|
test "$400B loads length counter when enabled" {
|
|
var triangle = Triangle.init();
|
|
|
|
triangle.setEnabled(true);
|
|
|
|
// Length table index 0 = 10.
|
|
triangle.writeTimerHigh(0);
|
|
|
|
try std.testing.expectEqual(
|
|
@as(u8, 10),
|
|
triangle.length_counter.value,
|
|
);
|
|
}
|
|
|
|
test "$400B cannot load length while disabled" {
|
|
var triangle = Triangle.init();
|
|
|
|
triangle.setEnabled(false);
|
|
|
|
triangle.writeTimerHigh(0);
|
|
|
|
try std.testing.expectEqual(
|
|
@as(u8, 0),
|
|
triangle.length_counter.value,
|
|
);
|
|
}
|
|
|
|
test "$4015 disable clears length counter" {
|
|
var triangle = Triangle.init();
|
|
|
|
triangle.setEnabled(true);
|
|
triangle.writeTimerHigh(0);
|
|
|
|
try std.testing.expectEqual(
|
|
@as(u8, 10),
|
|
triangle.length_counter.value,
|
|
);
|
|
|
|
triangle.setEnabled(false);
|
|
|
|
try std.testing.expectEqual(
|
|
@as(u8, 0),
|
|
triangle.length_counter.value,
|
|
);
|
|
}
|
|
|
|
test "linear counter reloads when reload flag set" {
|
|
var triangle = Triangle.init();
|
|
|
|
triangle.linear_reload_value = 42;
|
|
triangle.linear_reload = true;
|
|
|
|
triangle.clockQuarterFrame();
|
|
|
|
try std.testing.expectEqual(
|
|
@as(u7, 42),
|
|
triangle.linear_counter,
|
|
);
|
|
}
|
|
|
|
test "linear counter decrements without reload" {
|
|
var triangle = Triangle.init();
|
|
|
|
triangle.linear_counter = 10;
|
|
triangle.linear_reload = false;
|
|
|
|
triangle.clockQuarterFrame();
|
|
|
|
try std.testing.expectEqual(
|
|
@as(u7, 9),
|
|
triangle.linear_counter,
|
|
);
|
|
}
|
|
|
|
test "linear counter stops at zero" {
|
|
var triangle = Triangle.init();
|
|
|
|
triangle.linear_counter = 0;
|
|
|
|
triangle.clockQuarterFrame();
|
|
|
|
try std.testing.expectEqual(
|
|
@as(u7, 0),
|
|
triangle.linear_counter,
|
|
);
|
|
}
|
|
|
|
test "control clear clears reload flag" {
|
|
var triangle = Triangle.init();
|
|
|
|
triangle.control = false;
|
|
triangle.linear_reload = true;
|
|
triangle.linear_reload_value = 10;
|
|
|
|
triangle.clockQuarterFrame();
|
|
|
|
try std.testing.expectEqual(
|
|
@as(u7, 10),
|
|
triangle.linear_counter,
|
|
);
|
|
|
|
try std.testing.expect(!triangle.linear_reload);
|
|
}
|
|
|
|
test "control set preserves reload flag" {
|
|
var triangle = Triangle.init();
|
|
|
|
triangle.control = true;
|
|
triangle.linear_reload = true;
|
|
triangle.linear_reload_value = 10;
|
|
|
|
triangle.clockQuarterFrame();
|
|
|
|
try std.testing.expectEqual(
|
|
@as(u7, 10),
|
|
triangle.linear_counter,
|
|
);
|
|
|
|
try std.testing.expect(triangle.linear_reload);
|
|
|
|
//
|
|
// Because reload remains set, every quarter frame reloads
|
|
// the linear counter back to 10.
|
|
//
|
|
triangle.linear_counter = 3;
|
|
|
|
triangle.clockQuarterFrame();
|
|
|
|
try std.testing.expectEqual(
|
|
@as(u7, 10),
|
|
triangle.linear_counter,
|
|
);
|
|
}
|
|
|
|
test "length counter decrements when control clear" {
|
|
var triangle = Triangle.init();
|
|
|
|
triangle.setEnabled(true);
|
|
triangle.writeTimerHigh(0);
|
|
|
|
try std.testing.expectEqual(
|
|
@as(u8, 10),
|
|
triangle.length_counter.value,
|
|
);
|
|
|
|
triangle.control = false;
|
|
triangle.clockHalfFrame();
|
|
|
|
try std.testing.expectEqual(
|
|
@as(u8, 9),
|
|
triangle.length_counter.value,
|
|
);
|
|
}
|
|
|
|
test "control flag halts length counter" {
|
|
var triangle = Triangle.init();
|
|
|
|
triangle.setEnabled(true);
|
|
triangle.writeTimerHigh(0);
|
|
|
|
triangle.control = true;
|
|
|
|
triangle.clockHalfFrame();
|
|
|
|
try std.testing.expectEqual(
|
|
@as(u8, 10),
|
|
triangle.length_counter.value,
|
|
);
|
|
}
|
|
|
|
test "timer runs for period plus one CPU clocks" {
|
|
var triangle = Triangle.init();
|
|
|
|
triangle.timer_period = 2;
|
|
triangle.timer_counter = 2;
|
|
|
|
triangle.linear_counter = 1;
|
|
triangle.length_counter.value = 1;
|
|
|
|
triangle.sequence_position = 0;
|
|
|
|
triangle.clockTimer();
|
|
|
|
try std.testing.expectEqual(
|
|
@as(u16, 1),
|
|
triangle.timer_counter,
|
|
);
|
|
|
|
try std.testing.expectEqual(
|
|
@as(u5, 0),
|
|
triangle.sequence_position,
|
|
);
|
|
|
|
triangle.clockTimer();
|
|
|
|
try std.testing.expectEqual(
|
|
@as(u16, 0),
|
|
triangle.timer_counter,
|
|
);
|
|
|
|
try std.testing.expectEqual(
|
|
@as(u5, 0),
|
|
triangle.sequence_position,
|
|
);
|
|
|
|
triangle.clockTimer();
|
|
|
|
try std.testing.expectEqual(
|
|
@as(u16, 2),
|
|
triangle.timer_counter,
|
|
);
|
|
|
|
try std.testing.expectEqual(
|
|
@as(u5, 1),
|
|
triangle.sequence_position,
|
|
);
|
|
}
|
|
|
|
test "sequencer does not advance when linear counter is zero" {
|
|
var triangle = Triangle.init();
|
|
|
|
triangle.timer_counter = 0;
|
|
triangle.timer_period = 10;
|
|
|
|
triangle.length_counter.value = 1;
|
|
triangle.linear_counter = 0;
|
|
|
|
triangle.sequence_position = 5;
|
|
|
|
triangle.clockTimer();
|
|
|
|
try std.testing.expectEqual(
|
|
@as(u5, 5),
|
|
triangle.sequence_position,
|
|
);
|
|
}
|
|
|
|
test "sequencer does not advance when length counter is zero" {
|
|
var triangle = Triangle.init();
|
|
|
|
triangle.timer_counter = 0;
|
|
triangle.timer_period = 10;
|
|
|
|
triangle.length_counter.value = 0;
|
|
triangle.linear_counter = 1;
|
|
|
|
triangle.sequence_position = 5;
|
|
|
|
triangle.clockTimer();
|
|
|
|
try std.testing.expectEqual(
|
|
@as(u5, 5),
|
|
triangle.sequence_position,
|
|
);
|
|
}
|
|
|
|
test "sequencer advances when both counters are nonzero" {
|
|
var triangle = Triangle.init();
|
|
|
|
triangle.timer_counter = 0;
|
|
triangle.timer_period = 10;
|
|
|
|
triangle.length_counter.value = 1;
|
|
triangle.linear_counter = 1;
|
|
|
|
triangle.sequence_position = 5;
|
|
|
|
triangle.clockTimer();
|
|
|
|
try std.testing.expectEqual(
|
|
@as(u5, 6),
|
|
triangle.sequence_position,
|
|
);
|
|
}
|
|
|
|
test "sequencer wraps after 31" {
|
|
var triangle = Triangle.init();
|
|
|
|
triangle.timer_counter = 0;
|
|
|
|
triangle.length_counter.value = 1;
|
|
triangle.linear_counter = 1;
|
|
|
|
triangle.sequence_position = 31;
|
|
|
|
triangle.clockTimer();
|
|
|
|
try std.testing.expectEqual(
|
|
@as(u5, 0),
|
|
triangle.sequence_position,
|
|
);
|
|
}
|
|
|
|
test "triangle waveform values" {
|
|
var triangle = Triangle.init();
|
|
|
|
triangle.sequence_position = 0;
|
|
try std.testing.expectEqual(@as(u4, 15), triangle.output());
|
|
|
|
triangle.sequence_position = 15;
|
|
try std.testing.expectEqual(@as(u4, 0), triangle.output());
|
|
|
|
triangle.sequence_position = 16;
|
|
try std.testing.expectEqual(@as(u4, 0), triangle.output());
|
|
|
|
triangle.sequence_position = 31;
|
|
try std.testing.expectEqual(@as(u4, 15), triangle.output());
|
|
}
|
|
|
|
test "halted triangle retains current DAC output" {
|
|
var triangle = Triangle.init();
|
|
|
|
triangle.sequence_position = 7;
|
|
|
|
//
|
|
// Sequence position 7 = DAC value 8.
|
|
//
|
|
try std.testing.expectEqual(
|
|
@as(u4, 8),
|
|
triangle.output(),
|
|
);
|
|
|
|
//
|
|
// Stop both gating counters.
|
|
//
|
|
triangle.linear_counter = 0;
|
|
triangle.length_counter.value = 0;
|
|
|
|
//
|
|
// The DAC is NOT forced to zero.
|
|
//
|
|
try std.testing.expectEqual(
|
|
@as(u4, 8),
|
|
triangle.output(),
|
|
);
|
|
}
|