Skip to content

Instantly share code, notes, and snippets.

@nununoisy
Created May 3, 2023 06:35
Show Gist options
  • Save nununoisy/7c511b5f2532179eaa722c72e5873c53 to your computer and use it in GitHub Desktop.
Save nununoisy/7c511b5f2532179eaa722c72e5873c53 to your computer and use it in GitHub Desktop.
Hypothetical 2A03 triangle channel if it had volume control.
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