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(), ); }