ave / track

Tracker

This is a continuation of the synth, maybe read that page first because a lot of this ties back to that.

This started from a dissatisfaction with the synth. It was great at “signal processing”, it was fun to mess around with, but for practical purposes, it was very cumbersome to actually compose with it, as the tracks tended to get very long, and it was very clumsy at handling notes of different lengths, had no support for note triggers (so no adsr envelopes either - I had to manually create fades and sync them), and a dozen other little tidbits that made it unpleasant for larger projects where I needed content. So, I decided to abandon the intricate commands, and instead spend more effort on track management - and inadverently ended up implementing half an operating system.

Overview

(if this makes no sense, just read the commands list - it’s probably more helpful anyway)

The tracker produces sounds by executing commands from a file one after another, and generating a waveform - same way the synth did. Where it differs is that it does not do much calculation - if you want to play a certain note, that’s one command, and you can’t modify it much afterwards. Instead, it excels in program flow - the track can split into multiple and warp across the program. In practice, the tracker behaves a lot like a non-preemptive operating system scheduler - it supports multiple concurrent tracks, which each have internal variables used for sound synthesis (note length, adsr envelopes, etc), can spawn more tracks, and even have their own call stacks. They each execute commands to modify their state (or that of the tracker), and are preemptied once they play a note (incurring a delay).

For extra confusion, I decided to write the tracks in an assembly-like language, necessitating a compiler. Overall this was quite simple, yet it did involve intermediary bytecode and a symbol lookup table for track labels, and some twiddling around with binary instructions.

Specifications

The tracker runs at most 8 tracks (processes), each with a callstack at most 7 recursions deep. It has 4 registers for adsr envelopes (at the moment unused).

The commands are as follows (given first in “assembly” and then in binary expansion):

The “labeling” used for jumps/calls/forks requires the compilation to be a two-step process. The first one does most of the compilation, yet it does not know where labels are. Instead, the commands refer to the labels by the names they are listed as in the source, and the positions of the labels (as set by the . command) are kept in a separate table. On the second run, it outputs the codes generated before, substituting in the positions of the labels from the table.