Developer Notes
This page contains a few high-level implementation notes that may be helpful for anyone wanting to hack on Tuplet’s code. Or, more realistically, me in the future.
At a high level, there are two main components: the runtime and the syntax. Let’s start with the syntax, which can be found in "private/syntax.rkt".
Most of the core functionality of Tuplet centers around the runtime struct definitions for the various rhythmic forms, also known as "oneshots": notes, tuplets, patterns, and polyrhythms. Because of this, most of the jobs of the syntax macros consist of building instances of this structures based on the input. Syntax classes are used to define the syntactic forms of these structures, which makes the translation mostly a matter of simple recursive traversal.
Of note, there exists a potential syntactic ambiguity between patterns and regular runtime function calls. To mitigate this, the runtime functions included with Tuplet are described with the "prim" syntax class, which is used when compiling. This does mean that arbitrary Racket expressions cannot be used in the context of let or track. Though, adding an additional form as an escape-to-host may be something relatively simple that can be added in the future.
In addition, an attribute is added to each oneshot syntax class that stores a flat list of all identifiers found in the oneshot. This allows a set of identifiers to be defined with define-values, used for pattern binding. Otherwise, pattern binding uses the same compiliation as any other pattern, with the only difference being that a placeholder form is used instead of a note.
Each track compilation runs the runtime squeeze functions on the input, which means that the value defined by track is a track-assembly, containing a flat list with note in-points and (potential) out-points. This provides a decent segue to discussing the runtime, the top level of which can be found in "private/runtime.rkt".
The runtime’s most important functionality is its ability to squeeze a oneshot into a track-assembly. This is handled with calls to the squeeze functions in "private/squeeze.rkt". These functions must maintain a couple of invariants. The first relates to the input/output contracts on these functions. The input oneshot to these functions must contain, at the leaves, notes or placeholders, but never a combination of both. The second is that the number of notes or placeholders in the output list must match exactly with the number of notes or placeholders in the input oneshot.
There are also a handful of runtime functions that process and manipulate notes. It’s important to remember that rsound, the sound library Tuplet uses, stores audio in stereo, with a sample rate of 44.1kHz, at least by default, with the number of channels being hard-coded. This means that for functions like reverse, maintaining channels is important, especially considering that rsound stores audio in a vector of samples that alternate channels. In addition, a couple of the current runtime functions spawn instances of ffmpeg as a subprocess. This allows things like pitch-correction to be implemented without writing having to learn and write the algorithm for this codebase. Theoretically, this can allow a ton of additional features to be added to note manipulation easily by calling ffmpeg, though it does make this package less standalone. The code for running ffmpeg audio filters, and corresponding data structures, can be found in "private/ffmpeg-filter.rkt". In order to pass rsounds between ffmpeg and Racket, some code yoinked from rsound is used, which converts an rsound to/from a bytestring that can be read/written by ffmpeg (using the s16le format). This code can be found at "private/rsound-bytes.rkt".
There’s also a utility file at "private/util.rkt". Currently, it contains a couple of macros related to testing to reduce code copying.