quasimusic studio turns a symbolic substitution into self-similar music.
You define a substitution rule on a finite alphabet, the app computes the Perron-Frobenius geometry of that rule, lays out the corresponding one-dimensional substitution tiling in several layers, and plays each tile as a note. You can shape the sound, export a MIDI file, and share a setup by link.
It is one self-contained HTML file. It needs no installation, no account, and no internet
connection once loaded. Open it by double-clicking, or visit the hosted version. The geometry
it produces is identical to the quasimusic Python package, so a setup here and the
same setup in the package generate the same tilings and the same note timings. The underlying
ideas are from R. Trevino, "Quasimusic: tilings and metre", Journal of Mathematics and the
Arts, 2022.
A substitution replaces each symbol with a finite word over the same alphabet. Iterating it builds longer and longer words with rigid self-similar structure. The substitution matrix records how many of each symbol appears in each image; its leading (Perron-Frobenius) eigenvalue and eigenvector set a natural length for each symbol's tile so that applying the substitution scales the whole tiling by a single factor. Reading those tiles left to right in time, and assigning a pitch or chord to each symbol, turns the tiling into a rhythm and a melody. Stacking several substitution depths gives layers that share the same total length but subdivide it at different rates, which is where the music gets its nested, quasi-periodic feel.
0 maps to
0 1, and 1 maps to 0).If you hear nothing, click anywhere in the page once and press Play again. Browsers block audio until you interact with the page; pressing Play counts, so this only ever bites on the very first try.
This panel sets the structure of the piece.
A practical note on iterations: the finest layer has as many tiles as the length of SN, which grows quickly. If that exceeds 5000 tiles the app stops drawing and asks you to lower iterations, so the browser stays responsive. Rules with a larger Perron-Frobenius value reach that ceiling at lower iteration counts.
One row per symbol. Each row shows the symbol's colour and number, and a text box for its
image. Type the image as symbols separated by spaces, for example 0 1. Commas
work too. The image must be non-empty and use only symbols that exist in the current alphabet.
If you type something invalid the box turns red and the app keeps the last valid rule until
you fix it.
The readout strip across the top reports the geometry.
The matrix M where the entry in row i, column k counts how many times symbol k appears in the image of symbol i. Positive entries are highlighted. This is the object whose spectral data drives everything else.
The polynomial det(λI − M), shown with exact integer coefficients (for example λ2 − λ − 1 for Fibonacci). The Perron-Frobenius value in the readout is its largest real root.
This is where you control sound per layer. A Lite / Full synth switch sits at the top.
In both modes, each layer has a channel strip on the left with a volume slider (live; move it while the piece plays and hear the balance change) and a waveform selector (triangle, sine, saw, or square).
In Full synth mode each strip also gets an envelope:
Envelope edits take effect the next time you press Play, because notes are scheduled with their envelopes at the moment playback starts. Volume changes, by contrast, are immediate.
The strip layout adapts to whether you are editing pitches. In Auto mode each strip shows that layer's pitch map, listing which note each symbol plays. In Custom chord mode the editable chord cells appear to the right of each strip, so a row is one layer's volume, timbre, envelope, and chords together. With many symbols the chord area scrolls sideways while the channel strip stays pinned on the left.
This panel decides where pitches come from.
60 is middle C) or note names (for example
C4 E4 G4). An empty cell is a rest. The Fill chords from scale
button seeds the whole grid from the current root and scale so you have a musical starting
point to edit rather than a blank grid.Master Volume sits at the bottom and scales everything.
quasimusic.mid.The stacked strips are the heart of the tool. The top strip is the finest layer, SN. Each strip below it is one substitution coarser, and every strip spans the same total width because all layers have the same length. Tiles are coloured by symbol; the legend maps colours to symbols. Hover a tile to read its symbol, its onset in seconds, and its duration. During playback a vertical playhead sweeps across all layers and the tile currently sounding in each layer is highlighted.
A warning bar appears above the matrix when the rule is not primitive, or when it has no positive Perron-Frobenius eigenvector. In those cases the durations may not be well defined or unique, and you should read the tiling with caution. Primitive rules are the well-behaved case and are what the construction assumes.
Download MIDI writes a standard MIDI file built from the current setup:
What does not travel into MIDI: the waveform and the ADSR envelope. MIDI carries notes and a
few controllers, not synthesis. In a DAW you would choose an instrument for each track instead,
which is the natural equivalent. This is the same setup as the quasimusic Python
package, so exports from the app and the package are consistent.
Copy share link encodes the whole setup, the rule, every slider, the sound mode, the per-layer mix, the envelopes, and any custom chords, into the page address and copies it to your clipboard. Open that link and the app rebuilds exactly that setup. This is the simplest way to save a piece or send one to someone, and it needs no files. The link can get long if you have hand-edited a large chord grid, which is expected; it still pastes fine.
Alphabet 2, rule 0 maps to 0 1 and 1 maps to
0, seed 0. The characteristic polynomial reads
λ2 − λ − 1 and the
Perron-Frobenius value is the golden ratio.
Alphabet 3, with 0 maps to 0 1, 1 maps to
0 2, 2 maps to 0. The Perron-Frobenius value is the
tribonacci constant, about 1.8393.
Pick an alphabet size, give every symbol an image, and watch the Primitive badge. If it stays red, add cross-references between symbols until every symbol can eventually reach every other; that is what primitivity means in practice.
Switch the mixer to Full synth and give the bass (coarse) layers slow attacks and long releases, and the finest layer a short, percussive envelope. Set different waveforms per layer so the lines separate by timbre as well as register.
No sound. Click the page once, then press Play. If still silent, raise Master Volume and the per-layer volumes, and confirm your system output is not muted.
The tiling area says there are too many tiles. Lower Iterations. Rules with a large Perron-Frobenius value reach the tile ceiling quickly.
Playback is declined for too many notes. Lower Layers or Iterations, or use smaller chords in Custom mode.
A red warning bar appears. The rule is not primitive (or has no positive eigenvector). The piece may still play, but the durations are not on firm footing. Adjust the rule so every symbol can reach every other.
A chord cell or rule box is red. The text is not valid. For rules, use only
existing symbol numbers separated by spaces. For chords, use MIDI numbers 0 to 127 or note
names like C4; an empty cell is a rest.
The chord grid runs off the side. That is the honest shape of a layers-by-symbols grid at large alphabets. Scroll it sideways; the channel strips stay pinned.
The substitution matrix M is non-negative. When the substitution is primitive, some power of M is strictly positive, and the Perron-Frobenius theorem guarantees a simple largest eigenvalue λ > 0 with a strictly positive eigenvector. The app checks primitivity by taking boolean reachability powers of M up to the Wielandt bound (A − 1)2 + 1, which is exactly how far you must look before concluding a rule is not primitive. It uses boolean powers rather than the literal matrix powers so the test never overflows, even for large alphabets.
The right Perron-Frobenius eigenvector gives each symbol a base tile length, normalised so that the whole construction has a chosen total duration at the chosen tempo. Applying the substitution once multiplies the tiling's scale by λ, which is why each coarser layer is a factor of λ longer per tile yet the layers share a total length. For the full development, see the paper cited at the top.