Visual Click Track
A screen-based click track for worship teams: instead of (or as well as) an audio click in your ears, a display shows the current section, the bar and beat, and a countdown to the next section.
Songs are described in a small text format, .vct. Learn it from the guide,
docs/guide.md; the full specification is
docs/format.md, and examples/example-song.vct
uses most of it.
Layout
docs/architecture.md has the diagrams: what talks to the player, which package owns what, where time comes from, and the order things happen in.
| Path | What |
|---|---|
vct/ |
Core package: parses .vct, builds the beat timeline, and plays it with a runtime engine (engine.odin); lex.odin splits lines into coloured spans and rewrites them for the editor. No I/O, no globals; everything is allocated from the allocator you pass in. Public types are flat, C-shaped structs, so the package can later be exported with a C ABI or ported to C. |
cmd/vct/ |
vct command-line tool: check, lint, outline, dump and align. |
ug/, cmd/ug2vct/ |
ug2vct: makes a .vct from an Ultimate Guitar pro tab (see below). ug/ is the conversion, with no I/O. |
sheet/, cmd/sheet/ |
sheet: a chord sheet (chords above the words) to .vct or Guitar Pro (see below). sheet/ is the conversion, with no I/O; sheet/text.odin also reads lyrics sheets and writes both kinds, for the player's IMPORT and EXPORT. |
score/, cmd/score2vct/ |
score2vct: MusicXML, MuseScore and MIDI to .vct (see below). score/ is the conversion, with no I/O; the player's IMPORT uses it too. |
chordpro/, cmd/chordpro/ |
chordpro: ChordPro to .vct and back (see below). chordpro/ is the conversion, with no I/O. |
sections/ |
Names a song's sections from the words it sings (Verse 1, Chorus, Pre-Chorus), for imports that bring no names: a score marked A, B, C, or a sheet with no headings. Repetition does the work; no I/O. |
player/ |
Platform-independent part of the desktop player: runs the engine on a frame clock and keeps the screen state; the editor's model (text buffer with undo, structured edits, autocomplete). |
cmd/player/ |
vct-player, the desktop player built with raylib: the library screens, the pre-play screen, plays a song on screen, and edits songs and sets (edit.odin, edit_form.odin, set_edit.odin). macOS is the primary target. |
web/, scripts/build-web.sh |
The web version of the player: the page that loads it (web/index.html) and the script that compiles cmd/player to WebAssembly. |
stagesync/, cmd/vct-sync/ |
Stage sync: the server vct-player runs (on by default), so other screens on the network show the same beat at the same moment, with the song map and lyrics. JSON over UDP and WebSocket, pure Odin (core:net); the built-in web client is stagesync/web/index.html, and vct-sync is a terminal client. The player lists the screens following (H) and can give one host permission, which lets it control the player. The protocol is in docs/sync.md. |
lsp/, cmd/vct-lsp/, editors/ |
vct-lsp, a language server for .vct (diagnostics, semantic highlighting, completion, hover, outline, folding, formatting with vct.format), built on the editor's model; lsp/ handles messages with no I/O. Setup for Neovim and Zed in editors/README.md. |
qr/ |
QR code encoder (ISO/IEC 18004, byte mode, all versions), for the phone link on the home screen (Q). No I/O. |
packaging/macos/, scripts/package-macos.sh, scripts/notarize-macos.sh |
The macOS app: Info.plist template, and scripts that build, sign and notarise StageDisplay.app. |
layouts/ |
Stage display layouts (JSON5): which widgets the stage shows and where, and how the phone client arranges its panels. All are built in, and each stage display can show a different one; see below. |
themes/ |
Themes (JSON5): colours, line thicknesses, fonts and text sizes, for the stage and the phone client. All are built in; see below. |
schemas/ |
JSON Schemas for the stage sync messages (sync.schema.json, with an example of each message in examples/) and for the layout and theme files. mise run check:schemas checks the files in layouts/ and themes/ and the examples against them. |
assets/fonts/ |
Fonts built into the player: Barlow (Medium, SemiBold, Bold, ExtraBold) for text, Share Tech Mono for numbers. SIL Open Font Licence for each alongside. |
tests/ |
Unit tests, plus a language-neutral corpus: corpus/err/*.vct list the diagnostics they must produce in trailing #! lines; corpus/ok/*.vct must parse cleanly and match <name>.json when one exists. |
scripts/bless.sh |
Regenerates the golden files: the corpus timelines, and the importers' .vct output. |
docs/mobile.md |
No code yet: the research behind embedding the player in native mobile apps. What already builds for iOS, why the window and audio layer is the only blocker, how raylib, SDL3 and sokol compare, why the wasm build isn't the way in (and is worth keeping anyway), and a feature-by-feature look at what reaches a phone. Tracked as feature 28 in docs/PROJECT.md. |
Build and test
Requires the Odin compiler.
odin build cmd/vct -out:build/vct
odin test tests
build/vct check examples/example-song.vct
build/vct lint examples/example-song.vct # also warns about things that look unintended
build/vct outline examples/example-song.vct
build/vct dump examples/example-song.vct > timeline.json
build/vct reflow song.vct --write # join lyric lines that go by too fast to read
build/vct align song.vct --audio track.wav # the audio-offset that puts bar 1 on the music (--write saves it)
odin build cmd/player -out:build/vct-player
build/vct-player examples/example-song.vct
build/vct-player examples/example-song.vct --role drums # only the cues for drums (and everyone)
Two checks cover what the Odin tests can't, both optional and neither needed to build:
mise run check:schemas # layouts/, themes/ and the sync examples against schemas/
mise run check:diagrams # every Mermaid diagram in docs/ still draws
check:schemas needs uv (or python3 with
jsonschema and json5); check:diagrams needs
mermaid-cli, which it fetches
with npx if mmdc isn't on PATH.
Docs site
scripts/build-docs.py (mise run build:docs) turns this README,
docs/*.md and editors/README.md into a static site in build/docs
(plain HTML, one stylesheet, Mermaid diagrams drawn in the browser). It needs
uv, or python3 with markdown-it-py and
mdit-py-plugins. Any static server will do to read it locally:
mise run docs # builds, then serves build/docs at http://localhost:8000
To host it on Cloudflare Pages, upload the folder with Wrangler:
npx wrangler pages deploy build/docs --project-name stagedisplay-docs
or connect the repo in the Cloudflare dashboard with the build command
pip install markdown-it-py mdit-py-plugins && python3 scripts/build-docs.py
and the output directory build/docs.
Editor support (Neovim, Zed)
mise run build:vct-lsp builds build/vct-lsp, a language server for
.vct files. Put it on PATH, then follow
editors/README.md for Neovim (a plugin directory) or
Zed (a dev extension).
Web version
scripts/build-web.sh (mise run build:web) compiles the player to
WebAssembly and writes a static site to build/web: index.html,
index.js, index.wasm, index.data (the example songs), odin.js and the
icon. It needs Emscripten's
emcc on PATH (install emsdk, then source emsdk_env.sh) to link raylib
for the browser. Serve the folder over HTTP (a file:// page can't load the
wasm) and open it:
scripts/build-web.sh
mise run web # builds, then serves build/web at http://localhost:8000
The web version is the same app: library, stage and editors. Songs come in
by dropping files on the page or with Add song (the browser's file picker);
they are copied into the browser's storage (IndexedDB), so
they are still there after a reload. On the first visit the library starts
with the examples. Not on the web yet: MIDI, --render, the command-line
options, and saving songs back out of the browser.
Rendering a song to video
vct-player --render OUT song.vct plays the song from start to finish into a
video file, frame for frame as the stage shows it, with the song's audio:
file (any format ffmpeg reads) lined up by audio-offset. It needs ffmpeg
and ffprobe on PATH, and draws off screen as fast as it can (no real-time
wait; under Linux without a display, run it with xvfb-run).
build/vct-player --render song.mp4 examples/example-song.vct
mise run render -- song.mp4 examples/example-song.vct --fps 30 --size 1280x720
Options: --audio FILE and --audio-offset SEC override the song's header
(with no offset in either, beat detection finds it),
--fps N (60), --size WxH (1920x1080), --lead SEC (2: how long the
stopped stage shows before the song starts), --tail SEC (3: how long the
finished stage stays up, longer if the audio runs on). Loops and holds are
released as they come, so the song plays its written length and stays in
step with the audio; --next SEC (repeatable) presses Next at that second of
the video instead, as on stage. .mp4, .mov, .mkv and .m4v get H.264
and AAC; other extensions use ffmpeg's defaults for the format.
To render a set, give --set NAME (and --library DIR if needed) instead of
a song: OUT is then a folder, and each song is rendered to its own
NN Title.mp4 in it, in set order, as the set plays it (its arrangement, key
and tempo). Songs that can't be played are skipped and reported.
build/vct-player --render videos/ --set "Sunday"
Stage displays (more monitors)
With a projector or another monitor plugged in, press D on the home
screen (or click DISPLAYS) and turn on the displays that should show the
stage. Each one gets a full-screen window that can't be clicked: the stage
while a song is on it, and "Stage Display" otherwise, as the web client
shows. The display with the app's own window can't be picked, so the
controls stay where they are. The choice is remembered by display name;
--display N (repeatable, numbered as in the list) picks displays for one
run instead.
Each display can show a layout of its own, so one screen can be set up for
the singers and another for the band. In the list, Shift and the display's
number — or a click on the layout shown on its row — moves it on to the next
layout (anything the settings' layout takes, your own included); a display
with none set shows the stage's own. --display N:LAYOUT sets one for a
single run.
A set or a song can choose for every display at once with a
stage-layout: line (format §3 and §12): while it is open, each display
turned on shows that layout instead of its own, and layout: does the
same for the player's own screen. stage-layout[Name]: gives the display
of that name (as this list shows it) a layout of its own instead. The built-in click layout is made for
this — only the metronome, the count-in to each section and the cues — so a
band screen can stay that simple while the operator keeps the full stage:
set: Sunday Morning
stage-layout: click
stage-layout[EPSON PJ]: singers
The stage fills whatever screen it's on: on a 4:3 projector, a 16:10 laptop or an ultrawide monitor the layout spreads out to the screen's shape instead of leaving black bars, with text at the same size.
Sharing the stage with other screens
vct-player runs a sync server on port 47800 (--sync-port N changes
it). It's on by default: Shift+S on the home screen turns it off and on,
and --no-sync starts with it off. While it's off, the home screen's
footer and the QR code screen have an Enable Sync button instead. Screens on the same network show the same section, bar and
beat as the player, in time with it, along with the song map and the
lyrics. They don't need the song file.
- Any phone, tablet or browser: open the address in the set list
screen's footer,
http://<player's address>:47800/, or press Q (on the set list, or on stage while sync is on) for a QR code to scan. A machine on two networks at once (wired and Wi-Fi, or a VPN) has more than one address: the code screen lists the others underneath, and Left/Right shows one of those instead, which the footer and the code then use. The page follows the player over WebSocket.?offset=MSdelays it to match a slow screen, and?debugshows how close it is. - In a terminal:
vct-sync. With no address it finds the player by broadcast.
build/vct-player examples/example-song.vct
build/vct-sync # or: build/vct-sync 192.168.1.20
Connected screens, and letting one control the player. H (on the set list, the pre-play screen, or the stage while sync is on) lists the screens following, with the name each gives itself and where it is. Turning HOST on for one — its number key, or a click — lets that screen start and stop the song, press Next, jump to a section by tapping the song map, and pick a song from the set list, as if it were pressing the keys on the player. Nobody has it until the operator hands it out, it can be taken back the same way, and the player's footer says while anyone has it. A phone keeps its permission across a page reload.
To write a client of your own (a native phone app, say), see docs/sync.md.
Settings, themes and layouts
The player reads settings.json5 from its config folder
(~/Library/Application Support/vct-player/ on macOS,
~/.config/vct-player/ on Linux), writing a commented one on the first run:
{
theme: "default", // the stage's theme
layout: "stage", // the stage's layout
mobile: { // the phone client, if it should differ
theme: "daylight",
layout: "focus", // uses this layout's `mobile` section
},
}
Names are looked up in themes/ and layouts/ in the library
(Documents/StageDisplay/, where the editor saves) first, then in
themes/ and layouts/ beside the settings file (your own, as
NAME.json5), then among the built-in ones: themes
default, daylight (dark on white, for bright rooms) and contrast
(white and yellow on black, bigger type); layouts stage, focus,
singers (the words as large as the screen allows, with the key and the
count-in — made for a singers' monitor), click (only the metronome, the
count-in to each section and the cues — made for a band's stage display), metronome (the metronome
face filling the screen, with the count-in, cues and song map), lyrics (the whole song's
words beside the beat, with their chords), and strip and sheet for
players who follow the music rather than a counter: the song drawn like a
lead sheet, as bars of slashes with the chords over their beats, the cues
over theirs and the words underneath. strip is one row of bars sliding
past a playhead; sheet is lines of bars down the screen that turn a page
a bar before each section. Their chart widget takes view (strip or
sheet), flow (follow or page), bars and turn (bar, beat or
downbeat), so a layout of your own can mix them. A value
with a slash or ending in .json5 is a file path. Saving any of these files
(or a theme's font) while the player runs applies it straight away; a file
with a mistake leaves the default in its place and says why on screen for a
few seconds and on stderr.
mise run player -- --theme daylight --layout focus examples/example-song.vct
build/vct-player --theme ./my-theme.json5 --settings ./gig-settings.json5
T on the home screen (or its LAYOUTS + THEMES button) lists every layout
and theme, with a thumbnail of each. Built-in ones can only be copied
(Enter, or Ctrl/Cmd+D for any); a copy goes in the library's
layouts/ or themes/ folder, where it can be renamed (R) and deleted
(Delete). N starts an empty one. U (USE) makes one the player's
own: it is written into settings.json5 as layout: or theme:, leaving
the rest of the file, comments and all, as it was. Renaming one changes the
sets, displays and settings that name it, and the themes built on it;
deleting one puts those back to the player's own (the prompt lists them). A copied built-in theme is just
base: "NAME", so it follows the built-in until something is changed.
Enter (or EDIT) on one of your layouts opens the layout editor: the
stage as it will look, playing a built-in song, with every widget's box
over it. Drag a box to move it and its edges or corners to size it; it
snaps to the grid. The arrow keys move the selected widget a cell (with
Shift, its right or bottom edge), and the panel on the right lists the
widgets in drawing order and sets the selected one's scale, which songs it
shows for, its highlight and chords, and the song map's ticks (B). A adds a widget, Ctrl/Cmd+Z
undoes, Ctrl/Cmd+Shift+Z redoes, and Ctrl/Cmd+S saves. Below the
preview: pause it, skip to the next section, play the song with or without
its words, and try it in another theme. Saving rewrites the file from the
layout, so comments in it aren't kept; the mobile section is.
One of your themes opens in the theme editor the same way, or with E /
EDIT THEME from the layout editor (a built-in theme is copied first). Every
field a theme file sets is listed beside the preview, the ones the theme
sets itself marked; a colour is set with hue, saturation and brightness
sliders or typed as hex (Enter), a size on a slider (Left/Right step
it), a font by choosing or typing a file, or dropping one on the window.
Delete puts a field back to the base's. Saved, a theme is its base and
what differs from it.
A set can carry its own layout: and theme: (format §12, set with L and
H in the set editor), which stand in for these while the set is open, and
a song its own layout: (format §3), which stands in while the song is on
stage. L on the stage or the pre-play screen moves the stage on to the
next layout for this run only. The order, strongest first: L, --layout /
--theme, the song's, the set's, the settings file's. A set's or song's
stage-layout: is for the stage displays (above).
--theme and --layout override the settings for one run (the stage, and
any display with no layout of its own), and
--settings FILE reads another settings file. -define:LAYOUT=name (or
LAYOUT=name mise run build:player) picks the layout used when the settings
name none. mise run build:player builds optimised (-o:speed, 15–20 s), as the
packaged app is: unoptimised, beat detection and levelling make a song's
backing track load about 3× slower. OPT=none mise run build:player builds
in about 2 s, for when you are changing code rather than playing.
Themes. themes/default.json5 lists every key with its default. A theme
of your own only needs what it changes:
{
base: "default", // start from another theme
colors: { highlight: "#7CFC00", accent: "#2E5E3E" },
sections: { chorus: "#2E5E3E" }, // song map colours by section kind
sizes: { flash: 30, radius: 4, text_scale: 1.1, glow: 0 },
fonts: { sans: "fonts/Inter.ttf", bold: "fonts/Inter-Bold.ttf", mono: "fonts/JetBrainsMono.ttf" },
mobile: { flash: 1.6, radius: 4, text_scale: 1.2 },
}
colors are the palette (background, text, ink on coloured tags, accent,
highlight, alert, muted, next and more); sizes are line thicknesses (the
downbeat flash, beat column outlines and corners, the song map's boxes and
playhead) and the stage's text and number scale, on the 1920×1080 canvas;
fonts are TrueType/OpenType files relative to the theme file (positioned by
their own metrics); mobile sizes the phone client's flash, corners and
text. The phone client uses the theme's colours and fonts too: the player
serves the font files to it, so it needs no internet at the venue. The song
editor's code always uses Share Tech Mono.
Layouts. A layout lists widgets, each with a box [x, y, w, h] on the
1920×1080 canvas (boxes sit on a 20-unit grid, 96×54 cells, which is what
the layout editor snaps to; a file written by hand may use any number), an
optional scale, an optional when ("lyrics" or
"no-lyrics") and, on the lyrics, now-lyrics, next-lyrics and
all-lyrics widgets, highlight: true to show the current line bright (off by default;
the next line lights just after the last beat of the bar before it). The
karaoke widget always lights the current line and scales to its box. See layouts/stage.json5 and player/layout.odin for the
widget types.
A layout's mobile section arranges the phone client as a CSS grid of its
panels (header, map, state, section, beat, next, cues,
lyrics) for each orientation, with a text scale per panel; panels left out
are hidden:
mobile: {
portrait: { areas: ["header", "map", "section", "beat", "lyrics"],
rows: "auto auto auto auto minmax(0, 1fr)" },
landscape: { areas: ["header header", "section lyrics", "beat lyrics"],
columns: "1fr 1fr", rows: "auto 1fr 1fr" },
landscape_no_lyrics: { areas: ["header", "section", "beat"] },
scale: { section: 1.5, beat: 1.4 },
}
odin test tests checks every theme and layout file.
lyrics is the layout for songs with words: NOW and NEXT with their cues
down the left, the beat counter in the middle, and the whole song's words
on the right, scrolled to the line being sung, as the web client shows
them. Its all-lyrics widget takes chords: true to draw each line's
chords above the words they fall on.
The song-map widget fills its box. It takes ticks: true to draw a
progress bar under the current section, with a mark at the start of each
bar; without it (the default) nothing is drawn under the blocks.
Importing from Ultimate Guitar
ug2vct starts a .vct from an Ultimate Guitar "Official" (pro) tab. Pass
the tab id or its URL:
odin build cmd/ug2vct -out:build/ug2vct
build/ug2vct https://tabs.ultimate-guitar.com/tab/glen-hansard/falling-slowly-official-2459456
# wrote falling-slowly.vct (7 sections, 64 bars); bar counts are estimates, check them
It reads api-web.ultimate-guitar.com/v1/tab/pro/meta?id=N with libcurl
(vendor:curl). macOS has libcurl built in; on Linux, building ug2vct
needs the libcurl and mbedtls development packages (on Debian/Ubuntu,
libcurl4-openssl-dev libmbedtls-dev).
That endpoint needs no login, cookies or special headers. If Ultimate Guitar
ever starts refusing it, set UG_COOKIE to the Cookie header your browser
sends.
The tab gives the title, artist, tempo and the sections of its chord sheet. (The bars themselves are in the tab reader's score file, which is encrypted per download, so the tool doesn't use it.) It doesn't give bar counts, key or time signature, so:
- the tab page shows each section's name over the bar it starts on. Read
them off and pass them with
--sections, ending with the song's last bar:--sections "Intro:1, Verse 1:5, Chorus:21, end:70". The bar counts are then exact. - without
--sections, each section gets one bar per chord in the chord sheet, with the chords in a comment beside it.--bars-per-chord Nchanges the ratio, and--fitscales the counts to the length of the recording instead. A comment at the top compares the file's bar total with the recording. - the time signature is 4/4 unless you pass
--time 3/4, and the key is left out unless you pass--key.--tempooverrides the tab's tempo (its strumming tempo, which is what the tab page shows).
Check estimated bar counts against the recording before using the file on stage.
Only the structure is copied: never the words. Other options: -o FILE
(- for standard output), --count-in N and --json FILE (convert a saved
response instead of fetching it).
Reading section bars with Claude for Chrome
Instead of scrolling the tab and noting each section's bar by hand, you can ask Claude for Chrome to read them off the page you're viewing:
-
In Chrome, log in to Ultimate Guitar and open the tab, e.g.
https://tabs.ultimate-guitar.com/tab/glen-hansard/falling-slowly-official-2459456. Use the Tab view (not Chords), so bar numbers are shown. -
Open the Claude side panel (the Claude icon in the toolbar) and send:
On this Ultimate Guitar tab, list every section label shown above the staff (Intro, Verse 1, Chorus, ...) in order, with the number of the bar it starts on, then the number of the last bar of the song. Scroll through the whole tab to the end. Reply with one line only, in exactly this form and with nothing else: Intro:1, Verse 1:5, Chorus:21, end:70 Only names and bar numbers: no lyrics, chords or notes. -
Check a couple of entries against the page (the first section is usually at bar 1, and the numbers must increase), then paste the line into
--sections:build/ug2vct 2459456 --key C --sections "Intro:1, Verse 1:5, Chorus:21, end:70"
If a section label is repeated (two Choruses in a row), keep both entries:
ug2vct turns equal sections in a row into x2. If the reply doesn't parse,
ug2vct says so and writes nothing.
Chord sheets to .vct or Guitar Pro
sheet reads a chord sheet as copied from a song site ([Verse 1] headings,
chord names on the line above the words they fall on) and writes a .vct or a
Guitar Pro 5 file:
odin build cmd/sheet -out:build/sheet
build/sheet vct --tempo 68 --key C --artist "Glen Hansard" falling-slowly.txt
build/sheet gp5 --tempo 68 --key C --artist "Glen Hansard" falling-slowly.txt
A sheet has no bars, so as with ChordPro each chord gets one bar (a line of
words with no chords, one; --bars-per-chord N changes it). Check the bar
counts against the recording. A line is a chord line when every word on it is a
chord (Am, F#m7b5, Csus2, C/F); chord-only lines (an intro) are bars
with no words. Headings are [Name], or a known name ending in a colon
(Chorus:). --title (default: the file name), --artist, --key, --time
and, for vct, --count-in N set the song's details; vct needs --tempo
(gp5 uses 120 without it).
The .vct has the words and chords of each section, like a ChordPro import.
The Guitar Pro file has a track of one rest per bar, with the chord's name
above it (a chord diagram with no fingering), the words under that chord as
text, and a marker at each section. It has no notes, so it's a chart to read or
add to in Guitar Pro or TuxGuitar, not something to play back. The bar has to
be one rest long: 2/4, 3/4, 4/4, 6/8 and so on, not 5/4.
ChordPro import and export
chordpro import makes a .vct from a ChordPro file, and chordpro export
writes a .vct as ChordPro:
odin build cmd/chordpro -out:build/chordpro
build/chordpro import song.cho # wrote amazing-grace.vct; bar counts are estimates, check them
build/chordpro export examples/example-song.vct -o -
Import keeps the title, artist (or subtitle), key, capo, tempo, time,
copyright and CCLI number, the sections ({start_of_verse: Verse 1},
{soc}, {chorus} to repeat the last chorus, and {c: Bridge}-style
headings), the words and the chords. ChordPro has no bars, so each line of words gets one bar per
chord (a line with no chords, one bar), at the bar the line starts on, and its chords
become a > 1 | G | D | chord line. Other comments become markers, and
tab and grid blocks are skipped. --bars-per-chord N changes the ratio;
--tempo, --time, --key and --count-in N set those headers (a file with no
{tempo} needs --tempo). Check the bar counts against the recording.
Export writes the standard tags, then each section as an environment, with
each line's chords spread evenly over its words (.vct knows which bar a
chord is in, not which word). The rest (the exact section line, markers,
chord lines, lyric positions, other languages and headers ChordPro has no tag
for) goes into {x_vct_header: …}, {x_vct_section: …}, {x_vct_line: …}
and {x_vct_at: …} directives. Other apps ignore those, and import reads
them back, so exporting and importing again gives the same song. Songs that
use include: can't be exported yet.
Scores: MusicXML, MuseScore and MIDI
score2vct makes a .vct from a score. The player's IMPORT takes the same
files.
odin build cmd/score2vct -out:build/score2vct
build/score2vct song.mscz # wrote test-song.vct; check the section names and bar counts
build/score2vct song.mxl -o - # MusicXML (.musicxml, .xml or compressed .mxl)
build/score2vct song.mid
From MusicXML and MuseScore (.mscz, or an uncompressed .mscx) it keeps
the title, artist, key, tempo, time signatures and key changes. It starts a
section at each rehearsal mark. Where every mark is just a letter (A, B,
C), the sections are named from the words instead (Verse 1, Pre-Chorus,
Chorus, Bridge, Outro; package sections): the parts that come back
are the chorus and what leads into it, the rest are verses in order. The
names are guesses, so check them in the editor. Bars before the first mark
are an Intro, and a pickup bar is a Pickup 0+B. Repeats and first and
second endings are played out in full, and a section played twice in a row becomes x2. D.S.
and D.C. aren't followed. A change of tempo or time signature part-way
through a section splits it, and the second part is silent. The chord
symbols become a chord line for each section (a bar with no symbol keeps the
chord before it). The words become lyric lines, a new line after each rest,
from the first part with lyrics, using verse 2 on a repeat's second pass.
MIDI files have no chord symbols or rehearsal marks. Markers are used as section names, lyric events as words, and the tempo, time and key events as they are; a file without markers is one section. A score with no tempo gets 120 BPM, as in MuseScore, with a comment saying so.
MuseScore files are read directly, with no MuseScore install or account.
musescore.com's old developer API (keys by email) no longer seems to be
offered, and downloads there are limited by account and copyright, so the
importer works on files you already have. mscore -o song.musicxml song.mscz (the MuseScore app's command line) converts a score
that won't import into MusicXML.
Importing and exporting in the player
The SONGS tab's IMPORT button (or I) takes songs from files (the
native picker; several at once) or pasted text, in any of these formats:
.vct, ChordPro, a chord sheet (the "chords over lyrics" text Ultimate
Guitar shows, with [Verse 1] headings), a lyrics sheet (the same
without chords, as SongSelect gives it), or a score: MusicXML, MuseScore or
MIDI (files only; see above). The format is worked out from the
file name and the text; a file whose format can't be told asks for it, and
pasted text shows the format it looks like, which you can change. Chord and
lyrics sheets have no published spec; sheet/text.odin describes the rules
it follows (those of ChordSheetJS's Ultimate Guitar parser).
A sheet with no [Verse 1] headings at all (words pasted from a web search,
say) is split into sections and named from its words, the same way as a
score's lettered marks: the blank lines between the parts where there are
any, else the lines that come back later (a chorus) and what lies between
them.
Like a ChordPro import, bar counts are guesses (one bar per chord in a line, or per line of words), and a song with no tempo gets 100 BPM: the imported file says so in a comment at the top. Check both in the editor.
EXPORT, on a song's row (or X) and in the editor (Ctrl+E),
writes the song as a chord sheet, lyrics sheet, ChordPro or .vct, with the
native save panel (a download on the web). Sheets start with Title:,
Artist:, Key:, Tempo: lines, which the import reads back.
Player
vct-player uses vendor:raylib, which ships with the Odin compiler, so
there's nothing else to install. Songs and sets are plain files in
~/Documents/StageDisplay/songs and …/sets (--library DIR uses another
folder). The home screen has two tabs, SONGS and SETS (Tab switches).
Pass .vct files on the command line, drop them on the window or press A
to type a path: they are copied into the songs folder. Each song shows its
version (version: in the file, else "Original"), key, tempo, time and a
compact timeline. Two files with the same title and version are shown as
Title (file) with a warning; give one a version: to tell them apart.
R re-reads the folders (after editing files outside the app).
Enter (or double-click) on a song or set opens the pre-play screen: the
set's name (or the song's) over the list of songs with what the count-in
before each will be. Drag the grip (or Alt+↑/↓) to reorder them for this run;
the saved set doesn't change. Enter starts from the selected song, and the
stage moves on through the list. On the stage: Enter starts and stops,
Esc goes back to the pre-play screen, the set's name shows above the song's,
Space (or →, ↓, Page Down, which is what most page-turner pedals send) is
Next, R reloads the file after editing it, F toggles fullscreen,
L moves the stage on to the next layout for this run (also on the pre-play
screen; the footer shows the one in use).
Shift+←/→ puts the song before or after this one on the stage, stopped and
ready to start. Ctrl/Cmd+1…9 (0 for the tenth) jumps to that section, as
the controller's section pads do, and so does a click on a block of the song
map. Stopped, a jump only marks the section (outlined on the map); Enter then
starts the song there. While a song is playing Esc stops it only if a second Esc
follows within two seconds (the footer asks for it), so one stray key can't end
a song mid-service.
A song with several audio tracks (audio[Drums]: drums.wav, format §3)
plays them all together; 1–9 mute and unmute them, and Tab shows a
mixer with a volume and pan slider for each.
While a track plays the stage clock follows it rather than the other way
round — the sound card's clock isn't the screen's, and the band plays to what
it hears — so the two don't drift apart over a long song; the mixer panel
shows how far apart they are and the speed the clock is running at to keep
up (--drift prints the same).
Songs with lyrics (" lines, format §5.1) show the line being sung karaoke
style above the song map, with the next line dimmed beneath it (tagged ON 3
when it comes in on a beat other than 1).
Files with errors show their diagnostics and won't play.
Sets
A set is a file in the sets folder (format §12): a name and a list of songs.
N on the SETS tab (or E on a set) opens the set editor: add songs from
your library (always as the original), drag them into order, and for each song
click the arrangement (or Left/Right) to play one of the song's
arrangement:s, and type a key (K) or tempo (T) to change them for
this set only. Ctrl/Cmd+S saves.
The set also carries the look it is played in: the LAYOUT, STAGE DISPLAYS
and THEME chips beside its name (L, D and H, or a click on either
end of a chip) pick from the same layouts and themes the settings file takes,
and are saved as the set's layout: (this screen), stage-layout: lines and
theme: (format §12). The STAGE DISPLAYS chip (D) opens a list of the
displays connected and those the set names: a layout for every display, and
one for each display by name (Left/Right, or a click). Left as the
player's own — shown dimmed, the stage displays' as "per display" — the set
changes nothing. The choice applies from the moment
the set is opened: the lyrics layout for a wordy evening service, contrast
for a sunlit room.
Editing songs
Press E (or the row's EDIT button) to edit the selected song, E on the stage when it's stopped, or N / NEW SONG to start one from a template. The editor has two views of the same file, switched with Ctrl/Cmd+T:
- Text: syntax colours, problems underlined as you type and listed in the side panel, and suggestions as you type (Ctrl+Space for all): header keys and values, section names ("Verse 3" after "Verse 2"), modifiers, and marker and lyric snippets. Alt+↑/↓ moves the section under the caret, Ctrl+D duplicates it, Ctrl+/ comments lines.
- Structure: a form for the header and a table of sections with their markers and lyrics: click a field to change it, toggle loop/hold/quiet/silent, add, copy, delete and drag sections by their grip.
The backing track goes in the AUDIO field (on a Mac, CHOOSE… opens the file
picker; the audio suggestion in the text view does too). A file in the song's
folder is written as a relative path. FIND (or Ctrl/Cmd+B) finds
audio-offset by beat detection; leave it empty to detect it when the song plays.
Drag blocks on the song map to rearrange sections; the preview shows the song's
length, bars and tempo as you go. F5 (or Ctrl/Cmd+Enter, or PLAY) auditions
the song as it stands, saved or not, from the section at the caret with a bar of
lead-in (Shift+F5 from the top), with its backing track at full tempo; Esc
brings you back to the editor where you were.
While auditioning, the PRACTICE slider (or [ and ], 0 to reset) slows the
song down or speeds it up, from 40% to 150%, even mid-song. Ctrl/Cmd+S saves (a new song asks for a
path and joins the library), Ctrl/Cmd+Z undoes, and Esc goes back,
asking first if there are unsaved changes.
macOS app
On a Mac, scripts/package-macos.sh builds build/StageDisplay.app: a
binary (Apple Silicon) with the icon and a .vct file association, so double-clicking
a song (or dropping one on the Dock icon) opens it. It signs ad-hoc by default,
which is enough to run it on the Mac that built it.
To hand it to others, sign with your Developer ID and notarise:
xcrun notarytool store-credentials vct-notary --apple-id you@example.com --team-id TEAMID
scripts/package-macos.sh # picks your Developer ID identity automatically; SIGN_ID=... overrides
scripts/notarize-macos.sh # writes build/StageDisplay-<version>-macos.zip
VERSION, BUNDLE_ID and ARCHS can be set too; see the script headers.
Using the core package
import "core:mem/virtual"
import vct "path/to/vct"
arena: virtual.Arena
_ = virtual.arena_init_growing(&arena)
defer virtual.arena_destroy(&arena)
track := vct.parse(src, virtual.arena_allocator(&arena))
if !track.ok {
// track.diags has line/column/message for each problem
}
for beat in track.beats {
// beat.t (seconds), beat.bar, beat.beat, beat.accent,
// beat.say (voice cue, "" for none), beat.section, beat.marker, beat.lyric
}
Playing a track
The engine is the transport: it plays the timeline against your clock and
handles loop, hold, quiet, the operator's Next button and the
screen countdown. It does no I/O; you pass in your own monotonic time and
pull events with a short lookahead.
e: vct.Engine
vct.engine_init(&e, track)
vct.engine_start(&e, now())
events: [64]vct.Event
for e.state != .Finished {
n := vct.engine_poll(&e, now() + 0.05, events[:])
for ev in events[:n] {
// ev.kind: .Beat (ev.click, ev.accent, ev.say, ev.marker,
// ev.countdown, ev.bars_left), .Hold or .End, all at ev.time
}
if next_button_pressed {
vct.engine_next(&e, now()) // ends a loop, releases or skips a hold
}
}