--- /dev/null
+#!/usr/bin/env python3
+
+import math
+
+semitones = [
+ 'a',
+ 'a#',
+ 'b',
+ 'c',
+ 'c#',
+ 'd',
+ 'd#',
+ 'e',
+ 'f',
+ 'f#',
+ 'g',
+ 'g#',
+ 'rest'
+]
+
+# original music taken from lemonade.bas
+music = [
+ # welcome
+ [
+ (96, 180),
+ (128, 60),
+ (114, 60),
+ (128, 120),
+ (144, 60),
+ (152, 60),
+ (128, 255),
+ (128, 60),
+ (114, 60),
+ (85, 120),
+ (96, 60),
+ (102, 60),
+ (114, 120),
+ (102, 60),
+ (96, 255),
+ ],
+
+ # financial report
+ [
+ (152, 80),
+ (128, 160),
+ (152, 40),
+ (144, 80),
+ (128, 200),
+ ],
+
+ # SC=2: SUNNY
+ # played in a staccato style, duration_count always 10
+ # delay between repetitions needs to be calibrated
+ [
+ (96, 16 * 10),
+ (85, 4 * 10),
+ (128, 4 * 10),
+ (96, 4 * 10),
+ (76, 4 * 10),
+ (128, 4 * 10),
+ (96, 16 * 10),
+ ],
+
+ # SC=7: HOT AND DRY
+ # period_count of 1 means a rest (delay needs to be calibrated)
+ [
+ (114, 120),
+ (144, 60),
+ (114, 255),
+ (1, 120),
+ (128, 120),
+ (144, 60),
+ (128, 120),
+ (114, 60),
+ (144, 120),
+ (171, 255),
+ (228, 255),
+ ],
+
+ # SC=10: CLOUDY
+ # period_count of 1 means a rest (delay needs to be calibrated)
+ [
+ (152, 180),
+ (152, 120),
+ (152, 60),
+ (144, 120),
+ (152, 60),
+ (171, 120),
+ (192, 60),
+ (152, 255),
+ ],
+
+ # SC=5: THUNDERSTORMS
+ # period_count of 0 means 256
+ # period_count of 1 means a rest (delay needs to be calibrated)
+ [
+ (0, 160),
+ (128, 255),
+ (152, 40),
+ (171, 80),
+ (192, 40),
+ (228, 255),
+ (1, 40),
+ (0, 160),
+ (192, 255),
+ (192, 40),
+ (171, 80),
+ (152, 40),
+ (128, 255),
+ ],
+]
+
+for i in range(len(music)):
+ print('i', i)
+ for period_count, duration_count in music[i]:
+ a = duration_count
+ b = a / (((period_count - 1) & 0xff) + 1) # duration_count / period_count
+ cycles = 1.37788799e-02 - 4.21513128e-06 * a + 1.27999925e+02 * b
+ duration = 1.27361219e-05 + 2.50702246e-03 * a + 2.50310997e-03 * b
+ frequency = cycles / duration
+ note = math.log2(frequency / 110.) * 12.
+ semitone = round(note)
+ print(f'period_count {period_count:02x} duration_count {duration_count:02x} note {note:7.3f} semitone {semitones[semitone % 12]:>2s}{semitone // 12:d} duration {duration:7.3f}')
+
+# quantized music with some minor changes
+music = [
+ # welcome
+ [
+ ('c2', .45),
+ ('g1', .15),
+ ('a2', .15),
+ ('g1', .3),
+ ('f1', .15),
+ ('e1', .15),
+ ('g1', .75), # was: .644
+ ('g1', .15),
+ ('a2', .15),
+ ('d2', .3),
+ ('c2', .15),
+ ('b2', .15),
+ ('a2', .3),
+ ('b2', .15),
+ ('c2', .75), # was: .646
+ ],
+
+ # financial report
+ [
+ ('e1', .2),
+ ('g1', .4),
+ ('e1', .1),
+ ('f1', .2),
+ ('g1', .5),
+ ],
+
+ # SC=2: SUNNY
+ # played in a staccato style
+ # delay between repetitions needs to be calibrated
+ [('c2', .025), ('rest', .005)] * 16 +
+ [('d2', .025), ('rest', .005)] * 4 +
+ [('g1', .025), ('rest', .005)] * 4 +
+ [('c2', .025), ('rest', .005)] * 4 +
+ [('e2', .025), ('rest', .005)] * 4 +
+ [('g1', .025), ('rest', .005)] * 4 +
+ [('c2', .025), ('rest', .005)] * 16,
+
+ # SC=7: HOT AND DRY
+ [
+ ('a2', .3),
+ ('f1', .15),
+ ('a2', .75), # was: .645
+ ('rest', .6),
+ ('g1', .3),
+ ('f1', .15),
+ ('g1', .3),
+ ('a2', .15),
+ ('f1', .3),
+ ('d1', .75), # was: .643,
+ ('a1', .75), # was: .642,
+ ],
+
+ # SC=10: CLOUDY
+ [
+ ('e1', .45),
+ ('e1', .3),
+ ('e1', .15),
+ ('f1', .3),
+ ('e1', .15),
+ ('d1', .3),
+ ('c1', .15),
+ ('e1', .75), # was: .644
+ ],
+
+ # SC=5: THUNDERSTORMS
+ [
+ ('g0', .4),
+ ('g1', .8), # was: .644
+ ('e1', .1),
+ ('d1', .2),
+ ('c1', .1),
+ ('a1', .8), # was: .642
+ ('rest', .2),
+ ('g0', .4),
+ ('c1', .8), # was: .643
+ ('c1', .1),
+ ('d1', .2),
+ ('e1', .1),
+ ('g1', .8), # was: .644
+ ],
+]
+
+for i in range(len(music)):
+ print('i', i)
+ for note, duration in music[i]:
+ for j in range(12, -1, -1):
+ if note[:len(semitones[j])] == semitones[j]:
+ break
+ else:
+ assert False
+ if j == 12:
+ frequency_incr = 0
+ else:
+ semitone = j + int(note[len(semitones[j]):]) * 12
+ frequency = 2. ** (semitone / 12.) * 110.
+
+ # constants were calculated by lemonade_tone_nick.py:
+ frequency_incr = round(
+ 2.34821712e-05 / (1. / frequency / 0x1fffe - 4.47591203e-11)
+ )
+
+ # constants were calculated by lemonade_tone_nick.py:
+ duration_count = round(
+ (duration - 3.58309497e-10) /
+ (2.34821712e-05 + frequency_incr * 4.47591203e-11)
+ )
+
+ print(f'frequency_incr {frequency_incr:04x} duration_count {duration_count:04x} note {note:>4s} duration {duration:7.3f}')
+
+# converted music for lemonade_tone_nick.obj
+music = [
+ # welcome
+ [
+ (0x064f, 0x4aa1), # note c2 duration 0.450
+ (0x04b9, 0x18e5), # note g1 duration 0.150
+ (0x054e, 0x18e3), # note a2 duration 0.150
+ (0x04b9, 0x31ca), # note g1 duration 0.300
+ (0x0435, 0x18e7), # note f1 duration 0.150
+ (0x03f8, 0x18e7), # note e1 duration 0.150
+ (0x04b9, 0x7c7a), # note g1 duration 0.750
+ (0x04b9, 0x18e5), # note g1 duration 0.150
+ (0x054e, 0x18e3), # note a2 duration 0.150
+ (0x0716, 0x31bc), # note d2 duration 0.300
+ (0x064f, 0x18e0), # note c2 duration 0.150
+ (0x05f4, 0x18e1), # note b2 duration 0.150
+ (0x054e, 0x31c7), # note a2 duration 0.300
+ (0x05f4, 0x18e1), # note b2 duration 0.150
+ (0x064f, 0x7c61), # note c2 duration 0.750
+ ],
+
+ # financial report
+ [
+ (0x03f8, 0x2135), # note e1 duration 0.200
+ (0x04b9, 0x4263), # note g1 duration 0.400
+ (0x03f8, 0x109a), # note e1 duration 0.100
+ (0x0435, 0x2134), # note f1 duration 0.200
+ (0x04b9, 0x52fc), # note g1 duration 0.500
+ ],
+
+ # SC=2: SUNNY
+ # delay between repetitions needs to be calibrated
+ [
+ (0x064f, 0x0425), # note c2 duration 0.025
+ (0x0000, 0x00d5), # note rest duration 0.005
+ ] * 16 +
+ [
+ (0x0716, 0x0425), # note d2 duration 0.025
+ (0x0000, 0x00d5), # note rest duration 0.005
+ ] * 4 +
+ [
+ (0x04b9, 0x0426), # note g1 duration 0.025
+ (0x0000, 0x00d5), # note rest duration 0.005
+ ] * 4 +
+ [
+ (0x064f, 0x0425), # note c2 duration 0.025
+ (0x0000, 0x00d5), # note rest duration 0.005
+ ] * 4 +
+ [
+ (0x07f5, 0x0425), # note e2 duration 0.025
+ (0x0000, 0x00d5), # note rest duration 0.005
+ ] * 4 +
+ [
+ (0x04b9, 0x0426), # note g1 duration 0.025
+ (0x0000, 0x00d5), # note rest duration 0.005
+ ] * 4 +
+ [
+ (0x064f, 0x0425), # note c2 duration 0.025
+ (0x0000, 0x00d5), # note rest duration 0.005
+ ] * 16,
+
+ # SC=7: HOT AND DRY
+ [
+ (0x054e, 0x31c7), # note a2 duration 0.300
+ (0x0435, 0x18e7), # note f1 duration 0.150
+ (0x054e, 0x7c71), # note a2 duration 0.750
+ (0x0000, 0x63cf), # note rest duration 0.600
+ (0x04b9, 0x31ca), # note g1 duration 0.300
+ (0x0435, 0x18e7), # note f1 duration 0.150
+ (0x04b9, 0x31ca), # note g1 duration 0.300
+ (0x054e, 0x18e3), # note a2 duration 0.150
+ (0x0435, 0x31cd), # note f1 duration 0.300
+ (0x0389, 0x7c8c), # note d1 duration 0.750
+ (0x02a6, 0x7c9a), # note a1 duration 0.750
+ ],
+
+ # SC=10: CLOUDY
+ [
+ (0x03f8, 0x4ab6), # note e1 duration 0.450
+ (0x03f8, 0x31cf), # note e1 duration 0.300
+ (0x03f8, 0x18e7), # note e1 duration 0.150
+ (0x0435, 0x31cd), # note f1 duration 0.300
+ (0x03f8, 0x18e7), # note e1 duration 0.150
+ (0x0389, 0x31d2), # note d1 duration 0.300
+ (0x0326, 0x18ea), # note c1 duration 0.150
+ (0x03f8, 0x7c85), # note e1 duration 0.750
+ ],
+
+ # SC=5: THUNDERSTORMS
+ [
+ (0x025c, 0x4277), # note g0 duration 0.400
+ (0x04b9, 0x84c6), # note g1 duration 0.800
+ (0x03f8, 0x109a), # note e1 duration 0.100
+ (0x0389, 0x2136), # note d1 duration 0.200
+ (0x0326, 0x109c), # note c1 duration 0.100
+ (0x02a6, 0x84e8), # note a1 duration 0.800
+ (0x0000, 0x2145), # note rest duration 0.200
+ (0x025c, 0x4277), # note g0 duration 0.400
+ (0x0326, 0x84e0), # note c1 duration 0.800
+ (0x0326, 0x109c), # note c1 duration 0.100
+ (0x0389, 0x2136), # note d1 duration 0.200
+ (0x03f8, 0x109a), # note e1 duration 0.100
+ (0x04b9, 0x84c6), # note g1 duration 0.800
+ ],
+]
+
+# see ../lemonade/lemonade_tone_nick.lst
+REST = 0x300
+TONE = 0x308
+DURL = 0x309
+DURH = 0x30b
+FREQL = 0x30d
+FREQH = 0x314
+
+print(
+ f'''10PRINTCHR$(4);"BLOAD LEMONADE TONE NICK.OBJ"
+20RE={REST:d}
+30TN={TONE:d}
+40DL={DURL:d}
+50DH={DURH:d}
+60FL={FREQL:d}
+70FH={FREQH:d}
+80GETI$
+90ONVAL(I$)+1GOSUB10000,11000,12000,13000,14000,15000
+100GOTO80'''
+)
+for i in range(len(music)):
+ prev_fl = -1
+ prev_fh = -1
+ prev_dl = -1
+ prev_dh = -1
+ for j in range(len(music[i])):
+ frequency_incr, duration_count = music[i][j]
+ dl = -duration_count & 0xff
+ dh = (-duration_count >> 8) & 0xff
+ fl = frequency_incr & 0xff
+ fh = (frequency_incr >> 8) & 0xff
+ print(
+ '{0:d}{1:s}{2:s}{3:s}{4:s}CALLTN'.format(
+ 10000 + i * 1000 + j,
+ f'POKEDL,{dl:d}:' if dl != prev_dl else '',
+ f'POKEDH,{dh:d}:' if dh != prev_dh else '',
+ f'POKEFL,{fl:d}:' if fl != prev_fl else '',
+ f'POKEFH,{fh:d}:' if fh != prev_fh else ''
+ )
+ if frequency_incr else
+ '{0:d}{1:s}{2:s}CALLRE'.format(
+ 10000 + i * 1000 + j,
+ f'POKEDL,{dl:d}:' if dl != prev_dl else '',
+ f'POKEDH,{dh:d}:' if dh != prev_dh else '',
+ )
+ )
+ prev_dl = dl
+ prev_dh = dh
+ prev_fl = fl
+ prev_fh = fh
+ print(f'{10000 + i * 1000 + len(music[i]):d}RETURN')
--- /dev/null
+#!/usr/bin/env python3
+
+import numpy
+import py65.devices.mpu6502
+
+CPU_HZ = 3579545 * 4 / 14 # 1.02272714 MHz (from NTSC clock)
+
+mem = [0] * 0x10000
+cpu = py65.devices.mpu6502.MPU(memory = mem, pc = 0x400)
+
+def bload(bin_in):
+ with open(bin_in, 'rb') as fin:
+ bin = list(fin.read())
+ load_addr = bin[0] + (bin[1] << 8)
+ load_size = bin[2] + (bin[3] << 8)
+ assert len(bin) == load_size + 4
+ mem[load_addr:load_addr + load_size] = bin[4:]
+
+bload('../lemonade/lemonade_tone_nick.obj')
+
+# see ../lemonade/lemonade_tone_nick.lst
+TONE = 0x308
+DURL = 0x309
+DURH = 0x30b
+FREQL = 0x30d
+COUNTL = 0x30f
+FREQH = 0x314
+COUNTH = 0x316
+
+# jsr tone
+mem[0x400:0x403] = [0x20, TONE & 0xff, TONE >> 8]
+
+duration_count = 0x10000
+mem[DURL] = -duration_count & 0xff
+mem[DURH] = (-duration_count >> 8) & 0xff
+
+x = []
+y = []
+for i in range(17):
+ frequency_incr = (1 << i) - 1
+ mem[FREQL] = frequency_incr & 0xff
+ mem[FREQH] = (frequency_incr >> 8) & 0xff
+ mem[COUNTL] = 0
+ mem[COUNTH] = 0
+ cpu.reset()
+ toggles = 0
+ while cpu.pc != 0x403:
+ if cpu.pc == 0x31c:
+ toggles += 1
+ cpu.step()
+ duration = cpu.processorCycles / CPU_HZ
+ cycles = toggles / 2
+ print(f'frequency_incr {frequency_incr:04x} duration_count {duration_count:05x} cycles {cycles:10.3f} duration {duration:10.6f}')
+ x.append([1, duration_count, duration_count * frequency_incr])
+ y.append(duration)
+x = numpy.array(x, numpy.double)
+y = numpy.array(y, numpy.double)
+
+p, _, _, _ = numpy.linalg.lstsq(x, y, rcond = None)
+print('p', p)
+
+print(numpy.stack([x @ p, y], 1))
+
+# the output frequency can be predicted if we know the time per duration_count,
+# which we can calculate from duration count, frequency_incr, and p found above
+# (the dependency on frequency_incr is because branches are not equalized, so
+# the loop runs overall slower if there is more toggling of the speaker needed)
+# let a = duration_count, b = duration_count * frequency_incr
+# total_time = p[0] + duration_count (p[1] + frequency_incr p[2])
+# iteration_time = p[1] + frequency_incr p2
+# frequency_incr is added to "count" each iteration and speaker toggled when it
+# rolls over, so toggles per duration_count should be frequency_incr / 0x10000,
+# but it is actually frequency_incr / 0xffff because of the end-around carry:
+# toggles_per_iteration = frequency_incr / 0xffff
+# cycles_per_iteration = frequency_incr / 0x1fffe
+# period = iteration_time / cycles_per_iteration
+# = (p[1] + frequency_incr p[2]) / (frequency_incr / 0x1fffe)
+# = 0x1fffe (p[1] / frequency_incr + p[2])
+
+# using the above derivations, we can go backwards from total_time and period:
+# frequency_incr = p[1] / (period / 0x1fffe - p[2])
+# duration_count = (total_time - p[0]) / (p[1] + frequency_incr p[2])
+
+# example: generate 440 Hz for 0.125s (period 1 / 440 s, 55 cycles)
+frequency_incr = round(p[1] / (1. / 440. / 0x1fffe - p[2]))
+duration_count = round((.125 - p[0]) / (p[1] + frequency_incr * p[2]))
+
+mem[DURL] = -duration_count & 0xff
+mem[DURH] = (-duration_count >> 8) & 0xff
+mem[FREQL] = frequency_incr & 0xff
+mem[FREQH] = (frequency_incr >> 8) & 0xff
+mem[COUNTL] = 0
+mem[COUNTH] = 0
+cpu.reset()
+toggles = 0
+while cpu.pc != 0x403:
+ if cpu.pc == 0x31c:
+ toggles += 1
+ cpu.step()
+duration = cpu.processorCycles / CPU_HZ
+cycles = toggles / 2
+print(f'frequency_incr {frequency_incr:04x} duration_count {duration_count:04x} cycles {cycles:10.3f} duration {duration:10.6f}')