Plainsong
A keyboard-first music editor that writes plain-text .track files — composed as single notes on a pitch×time sheet, played back at runtime by game-audio. Built for game leitmotifs, and it can sing.
▶ Open in your browser · Windows · Linux · Trackyard · Discord · Source
The browser version runs with no install — the piano, recording, playback, file open/save (as downloads, or drag a .track onto the window), and WAV export (whole song and per-layer stems) all work. Share a song as a link: …/plainsong-web/#url=<a .track URL> opens it directly, and File → Open Link loads a .track from any URL (on the web it also rewrites the address bar to that shareable link). A shared track's @imports are fetched too — relative ones resolve against the link, so a song and its instrument packs can live side by side on any web host. MP3 export and microphone recording are desktop-only.
The whole document is a text file. Every visual edit reserializes it; the Source view lets you edit the text directly and an AI can edit either side. Nothing is hidden in a binary blob.
The Sheet
There is one canvas for everything. Each note is a horizontal line: Y = pitch (rounded to semitones), length = duration, thickness = volume (quiet notes are thinner; toggle vol lane in the sheet header for a strip at the bottom with a draggable bar per note), color = instrument (hashed to a hue), and a sloped line means glide. The left pitch gutter is a mini piano keyboard — black keys shaded, every C labelled (C4, C5…). The ruler numbers each bar (centered) and alternate bars are faintly shaded for easier reading.
Placing notes
| Action | Result |
|---|---|
| Ctrl + click empty grid | Places a note at the snapped position, and auditions it |
| Ctrl + drag on empty grid | Places a note and sets its length live as you drag (draws it as a stroke) |
| Drag on empty grid | Rubber-band multiselect — selects every note the box touches |
| Click empty grid | Clears the selection |
| Drag a note body | Moves it — a plain click never nudges it (a small drag is ignored); dragging past the view edge auto-scrolls so you can move notes across the whole song |
| Drag a note's right edge | Resizes its duration |
| Ctrl + drag a note | Duplicates the selection and drags the copy |
| Click overlapping notes | Cycles the selection through the stacked notes under the cursor |
| Shift + click a note | Toggles it in/out of the selection |
| Drag a selected note's end handle | On a flat note: pulls a glide to the target pitch |
| Drag a curve point | Bends the pitch curve — sideways moves it in time (0..1 of the note), up/down changes the pitch offset |
| Double-click a selected note | Adds a curve point where you click |
| Right-click a curve point | Deletes that point |
Pitch curves. A note's pitch is not just one line — it follows a multi-point curve over the note's length, exactly like the wave editor. A flat note has none; drag its end handle to pull a glide, then double-click to add more points and drag each one (a small handle on the selected note) to shape the trajectory. This expresses complex bends and per-note vibrato. The sheet draws the same curve the engine plays. Motif instances keep a single glide value, so their notes show only the end handle.
Snap & stacking
The Snap combo in the toolbar quantizes placement: Off, 1, 1/2, 1/3 (triplet), 1/4 (default), 1/6 (triplet), 1/8. Hold Shift during any click or drag to override snap for free positioning — Shift also disables pitch rounding, so you can place microtonal pitches between the semitone lanes.
Notes on the same pitch that overlap in time are drawn in parallel sub-lanes so nothing hides behind anything else. Draw order is inactive → active → selected.
The toolbar's Quantize button snaps the selected notes' start and length onto the current snap grid after the fact; recorded notes already snap their start to it as you play. To grab every note of one instrument at once, right-click a sound in the Sounds panel → Select notes using this sound.
Keyboard reference
Everything is reachable from the keyboard. Shortcuts are suppressed while a text field is focused.
| Keys | Action |
|---|---|
| Space · Shift+Space · Ctrl+Space | Play / Pause · Stop · Stop and return to the very start |
| Home / End | Move the playhead to the start / end of the song |
| Ctrl+Z · Ctrl+Shift+Z / Ctrl+Y | Undo / Redo |
| Ctrl+C · Ctrl+V | Copy · Paste (at the playhead, keeping relative offsets) |
| Ctrl+D | Duplicate selection (offset +0.5 beat) |
| Ctrl+A · Ctrl+Shift+A | Select all notes (active layers) · select all notes in the current layer |
| Delete / Backspace | Delete selection |
| Esc | Exit motif/instance editing, else clear the selection |
| ← / → | With a single note selected: jump to the previous / next note in that layer. Otherwise: seek one beat back / forward (Shift: one bar) — also while playing |
| Tab / Shift+Tab | While editing a lyric: jump to the next / previous note, keeping the lyric field focused (fast lyric writing) |
| ↑ / ↓ | Raise / lower the volume of the selected notes (Shift: coarse steps) |
| Alt+←/→ · Alt+↑/↓ | Nudge the selection — in time by a snap step · in pitch by a semitone |
| Ctrl+↑ / Ctrl+↓ | Transpose the selection by an octave |
| Ctrl+L | Set the loop region to span the selection |
| Ctrl+R | Toggle record |
| F1 / Ctrl+H | Show / hide the shortcut list |
| PageUp / PageDown | Focus previous / next layer (wraps) |
| Ctrl++ · Ctrl+- · Ctrl+0 | Zoom in · out · reset (both axes together) |
| Ctrl+wheel · Shift+wheel | Zoom · swap scroll axis |
| Ctrl+N/O/S · Ctrl+Shift+S/O | New · Open · Save · Save As · Reload |
| Ctrl+T · Ctrl+W | New document tab · Close tab (asks before discarding the last unsaved one) |
| Ctrl+PageUp / PageDown | Previous / next document tab |
| Ctrl+Q · Ctrl+Shift+Q | Quit · Force-quit |
Real zoom. Ctrl+wheel scales both pixels-per-beat and semitone height, so notes get physically bigger — not just horizontally stretched, and it anchors on the point under the cursor (Ctrl++/− anchor on the view center). Ctrl+0 resets height and re-fits the song horizontally.
Piano mode
Piano (always on) plays the active sound from the computer keyboard. The layout is isomorphic and uses physical key positions, so it is independent of your keyboard layout. Two rules:
Up one QWERTY row = +1 semitone. Right one key = +3 semitones.
The F key is the base pitch (0). The grid below shows the semitone offset of every key before the octave shift:
The toolbar shows − Oct +N + to shift the whole keyboard by octaves.
Record-driven composition
Rec is off by default — the piano just plays, so you can try out sounds and hunt for the next note freely. Turn Rec on in the toolbar and every key you press also writes a note at the insert line. This is the fast way to compose:
- Without playback, a record clock starts with the first held key: the insert line moves in real time, so keys pressed later start later. Press keys together for a chord; release together and the notes end together.
- A held key's note grows with the line; releasing snaps its duration to the grid.
- When the last key is released the line stops at the snapped note end, ready for the next tone.
- If transport is already playing, recording rides the live playhead instead of its own clock.
Singing voices phoneme-synth
Any sound can become a voice that sings its notes' lyrics. In the sound designer, pick the Voice preset: none (synth), male, female, child, robot, or ghost. A voice sound ignores the harmonics/envelope below it — it is synthesized by phoneme-synth instead.
Four voice knobs fine-tune the timbre, each seeded from the preset with a ↺ reset:
| Knob | Range | What it does |
|---|---|---|
| Formant | 0.5 – 2.0 | Shifts the vocal tract size (smaller = more childlike/chipmunk) |
| Tilt | 0.0 – 1.0 | Spectral tilt — brightness vs. darkness of the voice |
| Breath | 0.0 – 1.0 | Breathiness mixed into the tone |
| Vibrato | 0.0 – 1.0 | Pitch vibrato depth |
Lyrics & phonemes
Each note carries a Lyric (in the note inspector), written in phonetic symbols. New notes on a voice sound default their lyric to la so they always sing something. The lyric is drawn right on the note in the sheet.
Fast lyric entry. With Rec off, select a note and just start typing — the text jumps straight into that note's lyric field, no clicking. Then Tab / Shift+Tab moves to the next / previous note with the field still focused, so you can type a whole line syllable by syllable. (With Rec on, letters record/play notes instead — turn Rec off to write lyrics.)
The phonetic alphabet:
Vowels
aeiou@äöüVoiced
mnlrjq (ng)Noise
sc (sh) fx (ach) zw (v)Plosives
ptkbdgA lyric can hold more than one syllable: spaces separate words and ' separates syllables within a word — hal'lo du sings a two-syllable word and then another word, all sharing the note's duration. A leading ' ties the note to the previous note on the same sound: the run is sung as one legato phrase (syllables flow into each other), drawn as a small tie arc between the notes in the sheet. Without it every note starts a fresh phrase.
Any note (sung or not) also carries an articulation in its inspector — separate or legato. Legato ties it to the previous note on the same sound: a slur for instruments, one flowing word when sung. This is the structured equivalent of the leading '.
Because lyrics live on ordinary notes, a sung line inherits everything else notes have: glide, volume ramps, layer effects, motif reuse. Auditions sing too — placing or pitch-dragging a voice note sings its lyric instead of falling back to the synth.
Subtitles
A subtitle links a timestamp to a line of text, shown as an overlay synced to the playhead — for karaoke lyrics, spoken lines, or cutscene captions. Unlike note lyrics, subtitles are standalone: a subtitle is just (beat → text), independent of any note.
Edit them in the Subtitles section of the left library panel. + at playhead adds an entry at the current position; each row has a → to seek there, its beat, its text, and ✕ to delete. During playback — or while you scrub the insert line — the active subtitle (the last one whose beat has passed) is drawn at the bottom of the sheet. An entry with empty text acts as an end marker: it clears the overlay from that beat on.
Subtitles are saved in the .track file as a # Subtitles section (one <beat> <text> line each), which round-trips through the format, so any tool that reads the file can pick them up. Export .srt writes a timed SubRip file (timing derived from the song tempo) for videos and cutscenes; in-game display synced to the music is planned.
Leitmotifs
A motif is a named, reusable group of notes — the core feature for game music. Compose it once, drop transposable instances wherever it recurs, and vary it at runtime in the game. Each motif has its own color (hashed from its name), so every occurrence of the same leitmotif is recognisable at a glance across the whole piece.
- The Motifs library shows each motif as a small pitch×time thumbnail. + Motif creates one and drops you into edit mode: only that motif is shown (its coordinates are its own, not the song's), and you place its notes in the sheet like normal notes.
- With a motif selected, Space auditions it on its own with the active instrument — even before it is placed anywhere. Esc, clicking the motif again, or clicking a layer tab leaves edit mode.
- Place on layer drops an instance at the playhead. Instances render as a slim colored handle above their notes (the
♪ namelabel shows on hover or when selected) — drag the handle to reposition and transpose, double-click to edit that instance's overrides. The instance inspector's sound picker has a → button to jump straight to that sound in the inspector. - The instance inspector sets its sound, start, transpose, and tuning. If the instance's sound is a voice, empty motif-note lyrics fill with
la— so a leitmotif can sing.
Imported motifs are read-only. A motif brought in from another file (via @import) can't be edited here — its inspector shows an Open in original file button that opens the source as a document tab so you can edit it at the source.
from/count) to keep only a contiguous range of the motif's notes; trimmed and skipped notes render faded while editing.Custom tunings
Beyond 12-TET, define your own scales. In the Tunings library: + Tuning creates one; the inspector sets:
- period — the repeat ratio (2.0 = octave), range 1.1 – 4.0.
- Degrees (ratios) — the scale steps as frequency ratios. Degree 0 is pinned to
1.0 (root); add degrees with + Degree (inserted at the midpoint to the period) and delete with ×.
Assign a tuning per layer with the Tun combo on each layer card, and set that layer's Root frequency in Hz — the nearest note name is shown next to it, and ▶ plays it as a reference tone. Motif instances can override tuning too. Deleting or renaming a tuning cascades to everything that referenced it.
Switching a tuning does not move notes. A note stores a scale-degree number, not an absolute pitch; the tuning turns that number into a frequency. Switching to a tuning with the same number of steps per octave keeps each note on its degree (a real key/mode change). Switching to one with a different step count makes the same numbers land on different octaves, so the music reflows — plainsong warns you and offers to either keep the numbers or remap each note to the new scale degree closest in frequency.
The left pitch gutter follows the current layer's tuning: a piano keyboard for 12-TET, and for a custom tuning a scale ruler — one line per scale degree, a bold line labelled at each period (octave) boundary, the root marked, and the degree index shown when there is room. So the ruler always matches the scale you are composing in.
Layers & effects
Layers are mix groups, not composition containers — you compose in the sheet with the active sound, and which layer a note belongs to is just an attribute. New documents start with one layer; the concept only surfaces when you need it.
Each layer card (above the grid) has: name (click to focus, Shift/Ctrl+click to toggle active for editing), M mute, S solo, Vol, Pan, Tun, Root. Inactive layers render faded and non-interactive.
The layer inspector adds the runtime semantics that the game switches:
- Loop — a per-layer
@loopwithlenandskip, for looping ambience under non-looping music. - Effects (in order) — an ordered
@effectchain the game can toggle live: Lowpass, Highpass, Echo, Tremolo, Bitcrusher. These are the environment effects (cave, underwater), distinct from a sound's own musical character.
Move a note to another layer with the Layer combo at the top of the note inspector.
Playback, mixing & audition
- Space toggles play/pause (the audio holds its exact position); Shift+Space stops.
- Live mixing: layer Vol sliders take effect immediately while playing; mute/solo/pan/tuning changes restart playback seamlessly at the current beat. The toolbar has a master volume slider, and every layer card plus the master shows a level meter (bar = RMS, line = peak; yellow above 0.7, red at clipping — a soft-clip limiter protects the output).
- Mute / Solo (M / S on each layer card) affect both what you hear and what you see — an inaudible layer is hidden from the sheet, so it shows exactly what plays. A plain click on S solos that layer exclusively; Shift+click adds to the solo set. Clear M/S resets them on every layer. Mute/solo are editor-only: they persist in the file but a game ignores them at runtime (use
@volume paramfor runtime layer control). - Tempo in the toolbar sets the starting BPM; the → end-tempo field next to it makes the song accelerate or decelerate smoothly from start to end (set it equal to the start for a steady tempo).
- Drag in the ruler (the thin band above the grid) to set a loop region — playback jumps back to its start when it reaches the end. Double-click the ruler clears it. A single click sets the play position. This is separate from the toolbar Loop checkbox, which loops the whole song.
- Playhead follow: during playback the view locks the playhead to the center and scrolls the song under it (manual scrolling is off while it plays). Paused or stopped, the view scrolls freely. When playback ends it resets to the song start (or the loop-region start).
- Audition: notes sound when placed, and a moved note sounds once at its new pitch on release — every piano key previews the active sound. The sound designer's Preview plays it for one second (so an N-Hz vibrato/tremolo shows N cycles). Selecting a motif and pressing Play auditions just that motif with the active instrument, even before it is placed.
- Microphone recording: the ● Mic toolbar button records from the default input; pressing it again stops, saves a WAV next to the song (named by the text field beside the button; if the file exists you choose Overwrite, Keep both or Discard), adds it as a sampler sound and drops a note for it at the playhead.
Adaptive music
Game music reacts: combat layers fade in, the mix ducks in menus, a fanfare lands on the beat. In the .track format all of that is data — and Plainsong lets you rehearse it live, without starting your game.
- Parameters (
@param) are the interface between game and music: named numbers the game sets at runtime (combat,depth,tension). Create and rename them in the Params tab of the left panel; each has a saved default — the value before the game touches anything. - Bind a layer's volume to a parameter in the layer inspector (Vol param row, written as
@volume param <name> [scale]). The layer then fades with the parameter — that is vertical layering: compose the full arrangement once, the game reveals layers as intensity rises. - The Params tab is a live sandbox: every parameter is a slider that takes effect while the song plays — volume bindings fade click-free, and conditional
@jumps see the new value. Drag thecombatslider and hear the brass layer rise, exactly like the game callingset_parameter. - Snapshots are named parameter mixes ("calm", "combat", "underwater"). + Snapshot captures the current values, ▶ fades every parameter to the stored mix over the fade time, 📸 overwrites a snapshot with the current state. They are saved in the file as a
# Snapshotssection. - Stingers: the ⚡ button on each motif fires it once on top of the running music, quantized to the next bar — test how a pickup fanfare or a hit accent sits over the groove before wiring it into the game.
- Conditional jumps (
@jump <target> [if <param> <op> <value>]) chain songs into a graph — verse loops untilcombat > 0.5, then the fight track takes over. Edit them in the Jumps list of the song inspector (click empty sheet space): target file, skipped beats, and the condition as dropdowns. The Graph toggle opens a window drawing the open songs as nodes and their jumps as labeled arrows — click a node to switch to that song's tab. Set a parameter in the sandbox and let the song end to watch a jump resolve.
On the game side the same file plays through game-audio: load the .track, call set_parameter from your game loop, done — what you rehearsed in the sandbox is what ships.
Sound designer
Selecting a sound opens a full synth editor (non-voice sounds):
- Harmonics — build the timbre partial by partial. Each row picks a waveform from one dropdown that lists the built-ins (
sine, triangle, square, saw, soft_square, hill) and any waves you defined — then a frequency multiplier, a weight, and a duty (pulse width,0= off; NES-style values0.125,0.25,0.5override the waveform with a pulse). - distortion, volume, sustain, noise.
- attack / decay envelopes, each with an ease-in / ease-out curve editor.
- vibrato and tremolo modulation (depth, rate in Hz, and a waveform that can also be a wave) and a pitch slide (time + amount).
- arpeggio — a chiptune-style list of semitone offsets cycled over any held note at a set rate, for fast broken chords from a single note.
- detune and vol jitter — per-note randomization: each note gets a small random pitch offset (±semitones) and volume scale, so repeated hits (footsteps, gunshots) never sound mechanically identical.
0= off. - Samples — add an audio file (WAV/MP3/OGG/FLAC) with + Sample and the sound becomes a sampler. A note's pitch sets playback speed (
0= original,+12= one octave up / double speed) and the file plays to its natural end, ignoring note length. Each file is a pitch zone: leave it at all pitches, or limit it to a range so one sound can map different samples across the keyboard. Samples with harmonics below them layer on top of the synth. - steps — lo-fi wave quantization: each waveform period is rendered as N stairs (Game Boy / C64 wave-channel character).
0= continuous, full quality. - Waves — define your own waveforms in the Waves panel; they then appear in every waveform dropdown. Each is a chain of points in phase×amplitude (
x0..1,y−1..1) with a per-point ease curve, and the wave wraps (the last point connects back to the first) so it is always one seamless cycle. The preview is a direct editor: drag a point, double-click to add one, right-click a point to delete it. Two points at the samexmake an instant jump (a square wave); the ▶ Preview button plays the wave. The number fields below remain for fine-tuning. - Preview (plays the sound for one second), Duplicate, Delete. Imported sounds are read-only — duplicate to make a local copy. When a wave is chosen for a harmonic, the plain waveform selector greys out.
Source view & cross-file playback
Toggle Source to open a live text editor beside the sheet. Every visual edit reserializes it; every text edit reparses live (errors show in red and block playback). It is the only place to reach the source-only features:
@jump— jump between songs, including across files. A song with jumps plays as a graph: referenced files auto-open as tabs and the active tab follows the currently playing song.- Time signature (
beats_per_bar) — read for drawing bar lines.
Files open as document tabs in the top bar (Ctrl+PageUp/PageDown to cycle). Drop a .track file to open it; drop an audio file (WAV/MP3/OGG/FLAC) to add it as a sampler sound you can place like any other; drop a .mid to import it (below). External changes auto-reload unless you have unsaved edits.
Sharing parts. A .track can hold any mix of sounds, waves, motifs and tunings, and @import (a local path or a URL) pulls another file's definitions into yours — an imported instrument makes no sound until you use it, so sharing a part is just sharing a link. To publish a clean single-purpose pack, File → Export pack writes a .track containing only the sounds, only the waves, only the motifs, or only the tunings of the current song.
Open vs. Import. Two ways to bring in a URL, for two intentions — both open a new document, differing only in what it contains. File → Open Link fetches the file itself and opens it — for looking at or editing someone's song. File → Import Link opens a new, empty song whose only content is @import <url>: the linked file's sounds, motifs and tunings are available to compose with, but you write your own piece and the original stays untouched (and updates with it). Share a song with Open, a sound pack with Import. In the browser the address bar carries both — #url=<track-url> opens, #import=<track-url> starts a fresh song that imports the pack.
Song metadata. With nothing selected, the inspector shows the song's title, author, description and tags. They are saved as a # Meta section in the .track, so shared songs and packs stay identifiable in file listings and future song browsers.
MIDI import
Drop a .mid / .midi file onto the window — or use Import MIDI in the toolbar — to convert a Standard MIDI File into a new song in its own tab. The conversion (from game-audio) carries over:
- one layer per MIDI track, named after the track;
- General MIDI programs and the channel-10 drum map mapped to the bundled sound-bank names;
- note pitch (relative to A4), timing in beats, and velocity as note volume.
The first tempo wins — later tempo changes are dropped with a warning in the status bar. The imported song pulls its instruments from the shared bank via @import sounds/all.track, so provide that bank next to where you save (or remap the sounds) for playback; until then the sheet still shows every note, colored by instrument. Save as .track like any song.
Audio export
Export Audio in the toolbar renders the whole song to a .wav or .mp3 file — pick the extension in the save dialog. Export Stems writes one WAV per audible layer instead, for external mixing. Everything is synthesized offline exactly as it plays, so the export is shareable and usable anywhere, not just in a game running game-audio. The .track file stays the editable source; the audio file is the baked result. Both work in the browser too (as downloads, WAV only).
Export Stems renders one .wav per audible layer into a folder you pick — for mixing the parts separately in a DAW.
Plainsong is free software. Source: gitlab.com/porky11/plainsong. The runtime is game-audio.