Add a new assembly language tone generator, analyze and convert lemonade music
authorNick Downing <nick@ndcode.org>
Fri, 20 May 2022 15:10:34 +0000 (01:10 +1000)
committerNick Downing <nick@ndcode.org>
Fri, 20 May 2022 15:35:23 +0000 (01:35 +1000)
.gitignore
apple_io.py
lemonade/Makefile
lemonade/lemonade_tone_nick.asm [new file with mode: 0644]
lemonade/lemonade_tone_nick_asm.txt [new file with mode: 0644]
test/lemonade_music.py [new file with mode: 0755]
test/lemonade_tone_nick.py [new file with mode: 0755]
test/test.sh [new file with mode: 0755]

index cae1803..ea6a99b 100644 (file)
@@ -17,6 +17,8 @@ __pycache__
 /orig/Apple_DOS_v3.3_1980_Apple.do
 /orig/DOS_Tool_Kit_v1.0_1980_Apple.do
 /orig/Lemonade_Stand_1979_Apple.do
+/test/bootable.dsk
+/test/lemonade_music.bas
 /tok_to_bas.py
 /t_def.py
 /y_tab.py
index 802e8ff..2caf314 100755 (executable)
@@ -350,7 +350,7 @@ def call(addr):
   if addr == 0x302 or addr == 0x310:
     # for lemonade, see test/lemonade_tone_patched.py
     a = mem[0x301] # duration_count
-    b = a / mem[0x300] # duration_count / period_count
+    b = a / (((mem[0x300] - 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
     tone(round(cycles / duration), round(duration * 1e3))
index 1f1ab8e..8eb3ea1 100644 (file)
@@ -6,13 +6,13 @@ TOK_TO_BAS=../tok_to_bas.py
 BAS_TO_TOK=../bas_to_tok.py
 
 .PHONY: all
-all: lemonade_patched.dsk
+all: lemonade_patched.dsk lemonade_tone_nick.obj
 
 lemonade_patched.dsk: lemonade_patched.bin lemonade_tone_patched.obj lemonade_flash_patched.obj bootable.dsk
        cp bootable.dsk $@
        ${DOS33} $@ DELETE HELLO
        ${DOS33} $@ SAVE A lemonade_patched.bin "LEMONADE PATCHED"
-       ${DOS33} $@ SAVE B lemonade_tone_patched.obj "LEMONADE TONE PATCHED.OBJ"
+       ${DOS33} $@ SAVE B lemonade_tone_patched.obj "LEMONADE TONE patched.OBJ"
        ${DOS33} $@ SAVE B lemonade_flash_patched.obj "LEMONADE FLASH PATCHED.OBJ"
        ${DOS33} $@ HELLO "LEMONADE PATCHED"
 
@@ -35,6 +35,26 @@ lemonade.tok: lemonade.bin
 lemonade.bin: ../orig/Lemonade_Stand_1979_Apple.do
        ${DOS33} $< LOAD LEMONADE $@
 
+lemonade_tone_nick.obj: \
+lemonade_tone_nick.asm \
+lemonade_tone_nick_asm.txt \
+bootable.dsk \
+../orig/DOS_Tool_Kit_v1.0_1980_Apple.do
+       cp ../orig/DOS_Tool_Kit_v1.0_1980_Apple.do .
+       tr '\t\na-z' ' \rA-Z' <lemonade_tone_nick.asm |\
+LC_ALL=C tr '\000-\177' '\200-\377' >__temp__.asm
+       ${MKDOS33FS} __temp__.dsk
+       ${DOS33} __temp__.dsk SAVE T __temp__.asm "LEMONADE TONE NICK.ASM"
+       rm -f ../linapple-pie/Printer.txt
+       tr '\n' '\r' <lemonade_tone_nick_asm.txt |\
+( \
+  cd ../linapple-pie && \
+  ./linapple -b -1 ../lemonade/DOS_Tool_Kit_v1.0_1980_Apple.do -2 ../lemonade/__temp__.dsk \
+)
+       tr -d '\r' <../linapple-pie/Printer.txt >lemonade_tone_nick.lst
+       ${DOS33} __temp__.dsk LOAD "LEMONADE TONE NICK.OBJ" $@
+       rm DOS_Tool_Kit_v1.0_1980_Apple.do __temp__.asm __temp__.dsk
+
 lemonade_tone_patched.obj: \
 lemonade_tone_patched.asm \
 lemonade_tone_patched_asm.txt \
diff --git a/lemonade/lemonade_tone_nick.asm b/lemonade/lemonade_tone_nick.asm
new file mode 100644 (file)
index 0000000..ee672e8
--- /dev/null
@@ -0,0 +1,26 @@
+spkr   equ     $c030
+       org     $300
+rest   lda     #0
+       sta     freql
+       sta     freqh
+tone   ldx     #0
+durl   equ     *-1
+       ldy     #0
+durh   equ     *-1
+loop   lda     #0
+freql  equ     *-1
+       adc     #0
+countl equ     *-1
+       sta     countl
+       lda     #0
+freqh  equ     *-1
+       adc     #0
+counth equ     *-1
+       sta     counth
+       bcc     nospkr
+       lda     spkr
+nospkr inx
+       bne     loop
+       iny
+       bne     loop
+       rts
diff --git a/lemonade/lemonade_tone_nick_asm.txt b/lemonade/lemonade_tone_nick_asm.txt
new file mode 100644 (file)
index 0000000..4f546ce
--- /dev/null
@@ -0,0 +1,5 @@
+BRUN EDASM.OBJ
+PR#1
+DR 2
+ASM LEMONADE TONE NICK.ASM,LEMONADE TONE NICK.OBJ
+
diff --git a/test/lemonade_music.py b/test/lemonade_music.py
new file mode 100755 (executable)
index 0000000..d85184e
--- /dev/null
@@ -0,0 +1,394 @@
+#!/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')
diff --git a/test/lemonade_tone_nick.py b/test/lemonade_tone_nick.py
new file mode 100755 (executable)
index 0000000..b4af212
--- /dev/null
@@ -0,0 +1,102 @@
+#!/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}')
diff --git a/test/test.sh b/test/test.sh
new file mode 100755 (executable)
index 0000000..83e871a
--- /dev/null
@@ -0,0 +1,7 @@
+#!/bin/sh
+cp ../lemonade/bootable.dsk .
+../dos33fsprogs/utils/dos33fs-utils/dos33 bootable.dsk SAVE B ../lemonade/lemonade_tone_nick.obj "LEMONADE TONE NICK.OBJ"
+./lemonade_music.py |sed -ne '/^10PRINT/,$p' >lemonade_music.bas
+../bas_to_tok.py <lemonade_music.bas >lemonade_music.tok
+../tok_to_bin.py <lemonade_music.tok >lemonade_music.bin
+../dos33fsprogs/utils/dos33fs-utils/dos33 bootable.dsk SAVE A lemonade_music.bin "LEMONADE MUSIC"