Created
May 3, 2023 06:35
-
-
Save nununoisy/7c511b5f2532179eaa722c72e5873c53 to your computer and use it in GitHub Desktop.
Hypothetical 2A03 triangle channel if it had volume control.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import wave | |
import matplotlib.pyplot as plt | |
CPU_CLK = 1789773 | |
SAMPLE_RATE = 48000 | |
LENGTH_COUNTER_LENGTHS = [ | |
10, 254, 20, 2, 40, 4, 80, 6, 160, 8, 60, 10, 14, 12, 26, 14, | |
12, 16, 24, 18, 48, 20, 96, 22, 192, 24, 72, 26, 16, 28, 32, 30 | |
] | |
class LengthCounter: | |
length: int | |
halt_flag: bool | |
enabled: bool | |
def __init__(self): | |
self.length = 0 | |
self.halt_flag = False | |
self.enabled = False | |
def clock(self): | |
if self.enabled: | |
if self.length > 0 and not self.halt_flag: | |
self.length -= 1 | |
else: | |
self.length = 0 | |
def set_length(self, idx: int): | |
if self.enabled: | |
self.length = LENGTH_COUNTER_LENGTHS[idx] | |
class Triangle: | |
length_counter: LengthCounter | |
control_flag: bool | |
linear_reload_flag: bool | |
linear_counter_init: int | |
linear_counter_cur: int | |
seq_counter: int | |
period_init: int | |
period_cur: int | |
length: int | |
def __init__(self): | |
self.length_counter = LengthCounter() | |
self.control_flag = False | |
self.linear_reload_flag = False | |
self.linear_counter_init = 0 | |
self.linear_counter_cur = 0 | |
self.seq_counter = 0 | |
self.period_init = 0 | |
self.period_cur = 0 | |
self.length = 0 | |
def update_linear_counter(self): | |
if self.linear_reload_flag: | |
self.linear_counter_cur = self.linear_counter_init | |
elif self.linear_counter_cur > 0: | |
self.linear_counter_cur -= 1 | |
if not self.control_flag: | |
self.linear_reload_flag = False | |
def clock(self): | |
if self.linear_counter_cur and self.length_counter.length: | |
if self.period_cur == 0: | |
self.period_cur = self.period_init | |
self.seq_counter += 1 | |
self.seq_counter &= 0x1F | |
else: | |
self.period_cur -= 1 | |
def output(self) -> int: | |
if self.period_init <= 2: | |
return 7 | |
elif self.seq_counter <= 15: | |
return self.seq_counter | |
else: | |
return 31 - self.seq_counter | |
def write_reg(self, addr: int, val: int): | |
if addr == 0x4008: | |
self.control_flag = bool(val & 0x80) | |
self.length_counter.halt_flag = self.control_flag | |
self.linear_counter_init = val & 0x7F | |
elif addr == 0x400A: | |
pl = val & 0xFF | |
self.period_init &= 0x700 | |
self.period_init |= pl | |
elif addr == 0x400B: | |
ph = (val & 7) << 8 | |
li = (val & 0xF8) >> 3 | |
self.period_init &= 0xFF | |
self.period_init |= ph | |
self.length_counter.set_length(li) | |
self.linear_reload_flag = True | |
elif addr == 0x4015: | |
self.length_counter.enabled = bool(val & 4) | |
if not self.length_counter.enabled: | |
self.length_counter.length = 0 | |
class SuperTriangle(Triangle): | |
length_counter: LengthCounter | |
control_flag: bool | |
linear_reload_flag: bool | |
linear_counter_init: int | |
linear_counter_cur: int | |
seq_counter: int | |
seq_increasing: bool | |
period_init: int | |
period_cur: int | |
length: int | |
volume: int | |
def __init__(self): | |
self.length_counter = LengthCounter() | |
self.control_flag = False | |
self.linear_reload_flag = False | |
self.linear_counter_init = 0 | |
self.linear_counter_cur = 0 | |
self.seq_counter = 0 | |
self.period_init = 0 | |
self.period_cur = 0 | |
self.length = 0 | |
self.volume = 0 | |
def clock(self): | |
if self.linear_counter_cur and self.length_counter.length: | |
if self.period_cur == 0: | |
self.period_cur = 16 * self.period_init // (self.volume + 1) | |
self.seq_counter += 1 | |
self.seq_counter %= 2 * (1 + self.volume) | |
else: | |
self.period_cur -= 1 | |
def output(self) -> int: | |
seq_max = 2 * self.volume + 1 | |
if self.period_init <= 2: | |
return 7 | |
elif self.seq_counter <= self.volume: | |
return self.seq_counter | |
else: | |
return seq_max - self.seq_counter | |
def write_reg(self, addr: int, val: int): | |
if addr == 0x4008: | |
self.control_flag = bool(val & 0x80) | |
self.length_counter.halt_flag = self.control_flag | |
self.linear_counter_init = val & 0x7F | |
elif addr == 0x4009: | |
self.volume = val & 0xF | |
elif addr == 0x400A: | |
pl = val & 0xFF | |
self.period_init &= 0x700 | |
self.period_init |= pl | |
elif addr == 0x400B: | |
ph = (val & 7) << 8 | |
li = (val & 0xF8) >> 3 | |
self.period_init &= 0xFF | |
self.period_init |= ph | |
self.length_counter.set_length(li) | |
self.linear_reload_flag = True | |
elif addr == 0x4015: | |
self.length_counter.enabled = bool(val & 4) | |
if not self.length_counter.enabled: | |
self.length_counter.length = 0 | |
def freq_to_period(f: float) -> int: | |
return round(CPU_CLK / (f * 16 * 2) - 1) | |
def sg(file: str, tri: Triangle, pitch: float, t: int, v: int): | |
period = freq_to_period(pitch) | |
pl = period & 0xFF | |
ph = period & 0x700 | |
tri.write_reg(0x4015, 4) | |
tri.write_reg(0x4009, v) | |
tri.write_reg(0x400A, pl) | |
tri.write_reg(0x400B, ph) | |
tri.write_reg(0x4008, 0xC0) | |
samples = bytearray() | |
for _ in range(t): | |
last_sample = 0 | |
frame_counter = 0 | |
for clk in range(CPU_CLK): | |
frame_counter += 1 | |
if frame_counter in [7457, 14913, 22371, 29829]: | |
# quarter-frame | |
tri.update_linear_counter() | |
if frame_counter in [14913, 29829]: | |
# half-frame | |
tri.length_counter.clock() | |
if frame_counter == 29830: | |
# frame | |
frame_counter = 0 | |
tri.clock() | |
clk_t = clk / CPU_CLK | |
sr_t = int(clk_t * SAMPLE_RATE) | |
if sr_t > last_sample: | |
last_sample = sr_t | |
out = tri.output() | |
samples.append(out << 3) | |
fig, ax = plt.subplots(nrows=1, ncols=1) | |
ax.set_title(f'{tri.__class__.__name__} volume={v}') | |
ax.plot([i for i in range(period * 2)], [(samples[i + 200] >> 3) for i in range(period * 2)]) | |
ax.set_ylim([-1, 16]) | |
fig.savefig(f'{file}.png') | |
plt.close(fig) | |
with wave.open(file, 'wb') as w: | |
w.setnchannels(1) | |
w.setsampwidth(1) | |
w.setframerate(SAMPLE_RATE) | |
w.writeframesraw(samples) | |
if __name__ == '__main__': | |
note_f = 261.6 # C-4 | |
print('stock triangle') | |
triangle = Triangle() | |
sg('out.wav', triangle, note_f, 1, 0) | |
for v in range(16): | |
print(f'super triangle vol={v}') | |
super_triangle = SuperTriangle() | |
sg(f'out-super-{v}.wav', super_triangle, note_f, 1, v) | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment