#!/usr/bin/env python3 # Lays out a .tune by hand, byte by byte, so that Play's loader has something to read before # there is a compiler to write one. # # THIS IS NOT THE COMPILER. It is the hex editor: the sequences and the patches are written # out as literal bytes and the only thing computed is where each piece lands, because counting # offsets by hand is how you get a fixture that tests your arithmetic instead of the loader's. # # Written by Anachronaut import sys def patch(pairs): out = [len(pairs)] for parameter, value in pairs: out += [parameter, value] return out # Two instruments differing in one parameter, the octave, so that the same note number sounds # an octave apart depending on which is loaded. def voicePatch(octave): return patch([(0x00, 2), # saw (0x01, 0xFF), # at full gain (0x05, 1), # and on (0x20, 0), # no attack (0x21, 0), # no decay (0x22, 0xFF), # held at full (0x23, 5), # and gone quickly when the gate drops (0x04, octave)]) patches = [voicePatch(128), voicePatch(129)] # Note 60 for four ticks. The second sequence says it again, having changed instrument first - # so the octave between them is the tune's doing and not the note's. sequences = [[60, 4, 0xFF], [0x80, 1, 60, 4, 0xFF]] orders = [[0, 1, 0xFF], # voice 0 plays both, one after the other [0xFF], # and the other three have no part [0xFF], [0xFF]] starts = [0, 0, 0, 0] tick = 125000 # a sixteenth note at 120 beats a minute HEADER = 28 body, at = [], HEADER def place(blob): global at where = at body.extend(blob) at += len(blob) return where # The tables come first so that their own offsets are known before anything they point at. patchTableAt = at; at += 2 * len(patches) sequenceTableAt = at; at += 2 * len(sequences) patchAt = [place(p) for p in patches] sequenceAt = [place(s) for s in sequences] orderAt = [place(o) for o in orders] def word(n): return [(n >> 8) & 0xFF, n & 0xFF] tables = [] for p in patchAt: tables += word(p) for q in sequenceAt: tables += word(q) body[0:0] = tables header = [ord(c) for c in "SBTU"] + [1] header += [(tick >> 16) & 0xFF, (tick >> 8) & 0xFF, tick & 0xFF] header += [len(patches), len(sequences)] header += word(patchTableAt) + word(sequenceTableAt) for o in orderAt: header += word(o) header += starts header += [0, 0] assert len(header) == HEADER, "the header is %d bytes and the loader reads %d" % (len(header), HEADER) open(sys.argv[1], "wb").write(bytes(header + body)) print("%s: %d bytes, %d patches, %d sequences" % (sys.argv[1], len(header) + len(body), len(patches), len(sequences)))