MyGo 云音乐
用 Go 与 MyGo 写成的第三方网易云音乐桌面客户端。原生界面,没有 WebView;边下边播,无损与 Hi-Res,逐字歌词与「律动」,刘海播放器。
Translation pending
Das Regiebuch, das die Show fährt: Cues für Ton, Mikrofone, Licht und Timecode direkt im Textbuch – gesteuert mit Go und Back. Kostenlos für Mac, Windows und Linux.
Translation pending
MediaRust · HTMLGitHub @julius-b-muellerOnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
Markdown based Show Control Software
Translation pending
Taken from the repository README
Loading the full README.
Read the full README ↗Stage manager app for live shows and theatre productions — a native app written in Rust, for macOS (Apple Silicon and Intel), Windows and Linux (x64 and ARM64 each). The script is stored as a Markdown file; cues are embedded directly as YAML blocks. The app provides audio playback, MIDI control, timecode output, OSC output, and a live view — all from a single file.
However, there is no need to open a text editor. Roletext, stage directions, cues and everything else can be added and edited via the graphical user interface.
To turn the PDF of a play into a show file, see docs/pdf-zu-skript-prompt.md (a prompt for Claude, in German).
The script is a plain .md file. On startup the app reopens the last used file, or prompts for one. File → New File… (Cmd/Ctrl+N) creates a template.
| Syntax | Meaning |
|---|---|
# Scene, ## Subscene | Section headings, appear in the sidebar |
*Stage direction* | Italic stage direction (shown in grey) |
**Role name** | A role name (coloured according to the role editor) |
**Role name**\nText | Role with dialogue (displayed directly below) |
**Role1/Role2**\nText | Multiple roles speaking the same line (slash-separated) |
```yaml … ``` | Cue block (trigger) |
The first YAML block is always the config block (config:). All subsequent YAML blocks are cues.
config:
roles:
Anna:
color: blue
ch: 3
Ben:
color: green
ch: 5
groups:
Ensemble:
color: purple
roles: [Anna, Ben]
emLightNote: {ch: 1, note: 64}
# Virtual-channel NAMES are show-level (shared, config top-level). Cue audio patches
# reference them by name; the name→physical-output routing is machine-local.
virtualChannels:
- {name: L}
- {name: R}
settings:
MacBookPro:
# Output-device DECLARATIONS only — name/type/colour. Addresses (MIDI port /
# OSC host:port / HTTP url), the mixer connection, audio device and vChannel
# routing are NOT stored here; they live in each machine's local prefs.
outputDevices:
- {name: "Licht", type: midi, color: yellow}
- {name: "QLab", type: osc}
midiGoNote: {ch: 1, note: 36}
midiBackNote: {ch: 1, note: 37}
The script can be edited directly inside the app — no external editor required.
<br>, so that any Markdown reader breaks there too; the app itself never shows the <br>. Line ends without <br> (the formatter puts one after every sentence) flow on as in any Markdown reader.(quietly). It is saved as *(quietly)* and shown in grey italics.What you type is sorted for you: text without any marking becomes a stage direction (the asterisks around it belong to the file and are not shown), text under a line of role names becomes that role's dialogue. A block that is left empty — or whose text is deleted — is removed.
Role names complete as you type, in the first line and in the lines below it (to add further roles). A role's name can only be removed or replaced as a whole, so that no stray ** is left behind.
Vim keys (Settings → Vim keys in the text editor): normal and insert mode in the inline editor. Movements (h j k l w b e E 0 ^ $ f F t T gg G, with counts), operators with movements (dfx, ct,, ye, dE …), single characters (x, r), lines (dd, yy, p, o, O, J, u; Shift+Enter is a new line in the block in either mode), new blocks (go, gO, Enter) and whole blocks (gK/gJ move, yab/dab copy/cut, gp/gP paste). j/k leave a block at its last/first line into the next/previous one. Escape only returns to normal mode; the editor is left with :w, :q or a click outside. The Info button beside the setting lists every supported key.
Right-clicking a block (if an editor command is set in the settings) offers to open the corresponding line of the show file in that editor.
Several roles can share the same line of dialogue. In the script file this is stored as **Role1/Role2/Role3** followed by the dialogue text.
Creating a multi-role block:
Backspace at the start (no typed text) removes the last confirmed role chip.
Editing confirmed role chips (Shift+hover):
While the new-block editor is open, hold Shift and hover over a role chip to reveal two buttons:
| Button | Action |
|---|---|
| × | Remove this role from the group |
| + | Place the cursor at the end of the chip row so you can type and confirm an additional role |
When an existing block is open for editing (via Shift+Click), clicking on any role name opens a dropdown:
? placeholder appears, and a sub-dropdown opens immediately to choose the new role. Only roles not already in the block are listed. Clicking outside without selecting cancels the addition.The dialogue text always keeps the colour of the first role in the block, regardless of which role is changed or added.
Every YAML block (except the config block) is a cue (trigger). It is displayed as a control panel in the script, named after its trigger note.
A cue is called after its trigger note: trigger_note: {ch: 1, note: 47} is Cue 1.47 (channel.note). The name appears on the cue's card, in references from other cues, on auto-cue markers in a waveform (just 1.47), in the live view and in exports. It does not change when cues are added, removed or moved.
Every cue needs a trigger note of its own; only the variants of a cue share its note. A show file in which two separate cues carry the same note is reported as an error when it is opened, and the cue dialog does not save a note another cue already has. Changing a cue's note updates every reference to it (chain_end, loop_outro, auto_trigger, music.adjust) and the notes of its variants.
| Field | Description |
|---|---|
trigger_note: {ch: 1, note: 42} | MIDI note sent when the cue fires. Assigned automatically if not set. |
mic: "Anna" | Role name(s) or group name(s) whose microphone channels are opened. All others are muted. muteall mutes everyone. Group names expand to all member roles. Not used when Auto-Mic is active. |
auto_mic: true | Enables Auto-Mic for this cue (see Auto-Mic). |
music: file.wav | Audio file from the audio/ subfolder. Short form or object (see below). |
note: "Note" | Internal note displayed on the trigger panel. |
start_tc: "01:00:00:00" | Start timecode (HH:MM:SS:FF, or HH:MM:SS;FF for drop-frame; frames counted at the show's frame rate) sent as MTC / Art-Net timecode when the cue fires. |
osc: "/path/{ch}" | OSC message path sent when the cue fires (legacy — see OSC Output). |
cue_midi: [...] | List of MIDI messages sent when the cue fires (see MIDI). |
cue_osc: [...] | List of OSC messages sent when the cue fires (see OSC Output). |
cue_http: [...] | List of HTTP requests sent when the cue fires (see HTTP Output). |
cue_display: [...] | List of {device, markdown} entries shown on Display web interfaces when the cue fires (see Per-Cue Display Markdown). |
music:
file: loop.wav
volume: 0.75
start: 2.5
end: 120.0
fadein: 1.0
fadeout: 2.0
fading_point: 96.0
loop: true
start, end, fadein, fadeout, fading_point and loop define the shared transport
for the whole cue.
A cue can contain several audios that play together as one multichannel audio. Each
audio's channel count is detected automatically; every channel is patched by name to one
or more virtual channels (defined in settings). Use the audios list instead of file:
music:
loop: true
audios:
- file: band.wav # 4-channel file
patch: [Saal L, Saal R, Sub L, Sub R]
- file: click.wav
mono: true # downmix to mono (e.g. stereo click track)
volume: 0.7 # per-audio level
patch: [Click] # an entry may be a list of names for fan-out
patch has one entry per source channel (one entry when mono: true). An entry is a
virtual-channel name or a list of names (fan-out to several vChannels).Every cue's card has an edit button (a pencil) in its bottom right corner that opens the cue dialog. Everything a cue can do is set there — the YAML never has to be touched. The other buttons of a card (Auto-Cue, Adjust, S/L/F, Auto-Mic, variant, Live) appear while the pointer is on it.
New cues: Shift+Click on the + between two blocks adds an empty cue at once; Insert cue before/after… in a block's context menu opens the dialog first. Dragging audio files from the file manager into the script copies them into audio/ and makes a cue for each at the place where they are dropped; while dragging, a green line shows where that is (macOS and Windows).
What a card shows: microphones as labels, the note, audio files, timecode, what the cue sends — one line per device with the message itself (MIDI in words such as Ch 1 · CC 7 = 100, OSC address and values, HTTP method and path, announcement text, the first line of display content) — and its connections to other cues.
The bar at a card's left edge says where the cue stands in the show, in the colour of the word beside its name:
| Colour | Meaning |
|---|---|
| Green | the current cue, or a cue that has just fired by itself ("fired") |
| Yellow | the next Go, or an outro waiting for its loop to end |
| Blue | its audio is playing |
| Faint | done in this pass through the script |
| Grey | still to come |
Connections between cues are drawn as lines in the left margin, from the cue that acts (a dot) to the cue it leads to or changes (an arrow): purple for Start/Loop/Finish, orange for auto-cues, green for Adjust. An auto-cue at an audio position starts at its marker on the source's waveform. The lines are faint until the pointer is on one of their two cues. In a card, every reference to another cue ("Cue 1.47") is underlined; a click goes there.
In a card, a reference to another cue names the start of its audio file as well: "Cue 1.3 (Rain at nigh…)".
Copy / paste: Copy in a cue's context menu — or Cmd/Ctrl+C with the pointer on a cue — puts the whole cue (all of its fields) on an in-app clipboard, and its YAML block on the system clipboard as text. A paste button then appears next to every + insert zone, and Cmd/Ctrl+V with the pointer on a cue pastes it after that cue; the pasted copy is standalone (it gets a fresh trigger note and is never a variant). The clipboard is session-only.
Copying messages: The messages a cue sends (MIDI, OSC, HTTP, display) can be copied one by one (Copy beside each in the cue dialog) or all at once (Copy all messages in the dialog, Copy messages in the cue's context menu). Paste N messages — in the dialog of another cue or its context menu — adds them, each to the end of its list.
Live (arm in live view): Opens the live view with this cue armed as the next Go — handy for jumping straight to a specific point during a rehearsal or top-of-scene. While the show is locked the button is hidden.
Show lock: With the lock on, the script takes one interaction only: a click on a cue. With the live view closed it fires the cue, with the live view open it arms it as the next Go. Waveforms cannot be scrolled, zoomed or dragged, the transport row and the cue buttons are off, and nothing can be edited. While the live view is open the main view is locked by itself (it can be unlocked by hand); when the live view closes, the lock is as it was before.
⊕ duplicates a cue as a variant (sibling: true). Variants are alternatives for one place in the show — another key for another cast, a shorter version of a song. Only one of them fires.
chain_end) into a group. (As a loop's outro a group works: the operator chooses, then presses Go.) A show file that breaks this is reported as an error; the auto-cue or transition then has no effect.auto_trigger.group: true — it counts from whichever variant is fired, and its line begins at the frame) or on one variant only; the dialog asks. One at a place in the audio always belongs to the variant whose waveform it lies in.group_volume on the first cue). Each variant's own volume counts times it, so variants of different loudness keep their balance. An adjust with a volume aimed at the group sets this share anew: "to 50 %" means half of each variant's own volume.The Auto-Mic button in the cue's button row enables automatic microphone assignment. Instead of selecting microphone channels manually, the app scans the script text and opens exactly the microphones whose roles have dialogue in the section that follows.
auto_mic: true is saved in the YAML block).The buttons of a card that stand for something the cue has (Auto-Mic, Auto-Cue, Adjust, S/L/F) are coloured, and stay coloured when clicked: a click sets the connection up anew (the button gets a bright outline while the other cue is being picked), only Shift+Click removes it.
Once any Auto-Mic cue exists in the script, manual microphone selection is disabled on all cues (the edit dialog shows an informational note instead of checkboxes). This prevents conflicts between auto and manual assignments.
The computed microphone list is derived at runtime from the script text — editing role dialogue updates the mic list automatically. The auto_mic: true flag itself is stored in the YAML block, but the resulting channel list is not. Existing manual mic entries are preserved in their YAML blocks even while Auto-Mic is active.
Audio files (WAV, AIFF, FLAC, MP3, AAC/M4A, OGG) are located in the audio/ subfolder next to the .md file.
Audio is streamed from disk, never loaded whole: a show with hours of audio needs only a few megabytes of memory, and opening it is immediate. If the audio device disappears (unplugged interface), the show carries on and the device is taken up again by itself when it is back.
The audio picker in the cue dialog has a ✏ button next to the selected file. It renames the
file inside audio/ and rewrites every reference to it in the whole script — other cues using
the same file follow along automatically. The new name must be a plain file name (no path) with an
audio extension; renaming to an existing name is rejected.
Move unused audio files… (same menu as Clean up YAML) collects every file in audio/ that no
cue references and moves it into the fixed subfolder audio/_unused/, which is created if
needed. The folder name is not configurable, so the same files are recognised again on the next
run. Nothing is deleted — files can be moved back at any time. If the script contains
YAML blocks that cannot be parsed, the action aborts, because unreadable blocks may hold
references.
Every cue with an audio file shows an interactive waveform:
Drag along the waveform (press and move) to move the playhead; the audio follows when the button is released. A plain click does nothing, so the audio is not moved by accident.
Named markers: a right click on the waveform sets a marker at that place and asks for its name (optional). A right click on a marker renames or deletes it; Shift+Drag moves it. Markers are saved with the cue (music.markers, each with at in seconds and name). In the live view they cut the audio's progress bar into pieces, the way the show's bar is cut into scenes; where an auto-cue fires at a place in that audio, the bar carries an orange dot.
Shift+Drag on the waveform markers:
| Marker | Function |
|---|---|
| Lower start marker | Move playback start point |
| Lower end marker | Move playback end point |
| Upper start marker | Move fade-in end point |
| Upper end marker | Move fade-out start point |
All changes (volume, start/end, fades, loop) are saved automatically to the YAML file.
Audio routing uses a virtual channel abstraction between cue audio and the soundcard:
Saal L, Saal R, Sub, Click) — independent of how many channels the selected
device actually has. The default is two channels, L and R.This decouples the production design (named buses) from the venue's soundcard: re-routing for a different interface only touches the virtual-channel routing, not the cues.
Names are shared, routing is local. The virtual-channel names are stored in the show file (so cue patches resolve everywhere); the name→physical-output mapping is stored locally on each machine (unrouted channels default positionally: 1st→output 1, 2nd→output 2, …). The audio output device and the mixer-remote connection are likewise machine-local — the same show file runs at different venues without editing.
To test a virtual channel's physical output directly from the settings window, use the ▶ button in its row.
If a cue references a virtual channel that no longer exists (e.g. it was removed in settings), a warning is shown in the red banner when the file is opened. Clean up YAML removes audios that are routed to non-existent virtual channels.
MP3 and AAC files contain encoder padding (silent frames at the beginning/end) that prevents seamless transitions. Use WAV files for all S/L/F structures and loop transitions.
Seamless, sample-accurate transitions between audio clips without silence or clicks.
In musical theatre terms: the Loop is the Vamp, and the Finish (Outro) is the Devamp.
chain_end)# Start cue
music: intro.wav
chain_end: {ch: 1, note: 5} # note of the follow-on cue
---
# Follow-on cue (started automatically)
trigger_note: {ch: 1, note: 5}
music: loop.wav
loop: true
The start cue plays through and at the end automatically and seamlessly starts the follow-on cue.
loop_outro)# Loop cue (Vamp)
music: loop.wav
loop_outro: {ch: 1, note: 6} # note of the outro cue
---
# Outro cue (Devamp)
trigger_note: {ch: 1, note: 6}
music: outro.wav
The loop cue (Vamp) loops indefinitely. Clicking the outro cue (Devamp) queues it — it then starts exactly at the next loop end, seamlessly. A second click cancels the queue.
fading_point)fading_point (seconds) defines where a loop (Vamp) stops cycling. Without it, the transition happens at end (or the file boundary). With it:
start to fading_point.fading_point to end plays as a tail. It rings out over whatever comes next: over the loop's own next pass every time it wraps, and over the next cue (Devamp) once that starts. On the waveform a ghost cursor runs through the tail while the main cursor is back at the start.fading_point before starting the transition.This allows the Vamp or Devamp to ring out naturally — for example, if a cymbal is struck just before the loop boundary, the tail carries it through into the Devamp without cutting it off.
music:
file: loop.wav
end: 64.0
fading_point: 32.0 # loop cycles 0→32 s; tail 32→64 s plays on outro
loop_outro: {ch: 1, note: 7}
The waveform editor shows an orange marker at fading_point. The progress bar in the live view cycles between start and fading_point.
crossfade)For background sound that has to run on endlessly without a gap — rain, a crowd, room tone. Instead of playing to the end and starting over, the loop overlaps itself: the last seconds of each pass fade out while the next pass already fades in from the start.
music:
file: rain.wav
loop: true
start: 2.0
end: 62.0
crossfade: 4.0 # seconds of overlap; one pass is 56 s long
crossfade replaces fadein, fadeout and fading_point on such a loop: the fade at the start and the fade at the end are the same crossfade and always the same length. On the waveform, dragging either fade grip (Shift) moves both; the two zones are tinted and crossed by a dashed slope.start and end. The two curves keep the loudness even (equal power).Crossfade 2.4 s, 2.4 s or 2.4 (plain fades: Fade-In / Fade-Out).start_tc is not possible on it, and no timecode runs on from it.music_seq).music_seq)A loop (Vamp) can span multiple audio files that play in sequence. The primary file is defined in music:, additional files in music_seq:. Each file transitions seamlessly to the next at its fading_point (or end), and the sequence loops back to the first file. This allows a long, varied Vamp that still devamps quickly — the Devamp fires at the next fading_point in the currently active file, not at the end of the full sequence.
music:
file: loop-a.wav
fading_point: 32.0
loop_outro: {ch: 1, note: 7}
music_seq:
- file: loop-b.wav
fading_point: 28.0
- file: loop-c.wav
fading_point: 30.0
Each music_seq entry supports file, volume, start, end, fadein, fadeout, fading_point. With more than 2 virtual channels, a slot can also hold its own audios list (multiple files patched to virtual channels), exactly like the primary music block:
music_seq:
- audios:
- file: loop-b-band.wav
patch: [Saal L, Saal R, Sub L, Sub R]
- file: loop-b-click.wav
mono: true
patch: [Click]
fading_point: 28.0
Behavior:
fading_point of the current file (sample-accurate; audio between fading_point and end plays as an overlap tail into the next file).fading_point in the currently active file.start → fading_point) and resets on each transition.Use WAV files for all files in the sequence to avoid encoder padding artefacts at transition points.
The S/L/F button on a trigger opens a menu to configure chain_end or loop_outro. Shift+Click removes an existing connection. The button shows the type (Start, Loop, Finish, Bridge).
A cue can be triggered automatically by another cue (the source): together with it, when the source's audio reaches a position, after a delay once the source fires, or when the source's audio has played to its end.
Setup:
Click Auto-Cue on the target cue
Click the source cue (pick mode — any cue with a trigger note can be picked)
A dialog offers every kind:
With the cue: fires together with the source (stored as delay: 0).
After the end of its audio: fires when the source's audio has played to its end position by itself. Audio that is stopped or faded out does not count; a loop ends once its outro has taken over and it has rung out; a source without audio counts as ended at once. The line leaves the source at the foot of its left edge.
At the playhead (x s): offered when the source's audio is paused or playing somewhere other than its very start or end — the auto-cue gets its marker there.
After a time: a time in seconds. If that time lies within the source's audio, you choose:
If the source has no audio, or the time lies past its end, it is always a delay auto-cue. A delay auto-cue's line starts at the left edge of the source's card, with its seconds written where it leaves. Several of them leave in the order of their delays.
The cue dialog offers the same kinds (except the playhead).
Auto-cues stand in the order they fire in. The auto-cues of one source have to fire in the order they stand in the script, so that what is read along is what happens:
Compared are places with places and delays with delays; a place with a delay only where the source's audio plays once (in a loop the place comes round on every pass, the delay only once). Auto-cues of different sources are not compared.
Seconds are shown with two decimals at most.
For audio-position auto-cues the marker appears on the source cue's waveform, with the
auto-cue's name (1.47) above it; Shift+Drag repositions it. A line runs from the marker
to the auto-cue's card. When an auto-cue fires, the view goes to it and its card is marked
"fired"; the operator's own cue stays the current one, and Go carries on from there. For delay auto-cues, a progress bar fills on the target's Auto-Cue button while
counting down. Shift+Click on the Auto-Cue button deletes the auto-cue.
A delay auto-cue fires its target the configured number of seconds after the source cue is triggered. Stopping the source or pressing Back before it fires cancels the pending auto-cue.
auto_trigger: # audio-position form
trigger_note: {ch: 1, note: 3}
at: 45.2
auto_trigger: # delay form (fires 8 s after the source cue; 0 = with it)
trigger_note: {ch: 1, note: 3}
delay: 8.0
auto_trigger: # after the end of the source's audio
trigger_note: {ch: 1, note: 3}
at: end
A cue can affect another cue when it fires (e.g. fade out music when dialogue starts).
Setup: ⇢ Adjust button on the trigger → click the target trigger → choose action:
The cue that does the adjusting needs no audio of its own: a cue that only sets microphones or sends a light cue can fade out the music that is playing. (The adjust is stored under music.adjust; for a cue without audio music then holds nothing else.)
music:
file: dialog.wav
adjust:
trigger_note: {ch: 1, note: 3}
fadeout: true
fadetime: 2.0
MIDI (and OSC) output devices are configured in Settings as a unified list of named output devices. Each device has a name, type (midi or osc), an optional colour (shown as a badge on the trigger panel), and a sendTriggerNote flag.
Every MIDI device with sendTriggerNote: true sends the cue's trigger_note (Note On/Off) when a cue fires. Additional per-cue MIDI messages are defined via cue_midi (see below).
cue_midi)A cue can send arbitrary MIDI messages to any configured MIDI device when it fires. Supported message types: Note, CC, Program Change, SysEx.
cue_midi:
- device: "Licht"
type: note
ch: 1
note: 42
vel: 100
- device: "Licht"
type: cc
ch: 2
cc: 7
value: 64
- device: "Licht"
type: pc
ch: 1
program: 5
- device: "Licht"
type: sysex
bytes: "F0 41 F7"
If device is omitted, the first MIDI device is used. The Back function restores device states — see Cue History (Back).
Microphone muting is configured separately under Mixer Remote Control in Settings (not part of the output device list). See Settings.
Each MIDI output device has a Timecode (MTC) checkbox in Settings, and beside it its frame rate; MTC is sent to every device with it enabled. See Timecode (MTC / Art-Net).
Configurable MIDI notes for Go, Back, ▲ and ▼ (the live view's buttons: arm the cue before / after the next Go, without firing anything) — received on all MIDI inputs, or on the one chosen. MIDI devices are looked for all the time: one plugged in after the program started turns up in the settings and is listened to. Back acts when its note is let go of (the controller has to send a note-off, or a note-on with velocity 0); holding it for one second stops all audio instead, without going back. Notes can be assigned using MIDI Learn mode in the settings window.
External software (lighting desks, QLab, Companion, …) can fire individual cues by their own note. Configure the input under Settings → Live → Remote Cue Trigger:
trigger_note fires that cue (independent of the current position)./cue/<ch>/<note> (no arguments). For example /cue/1/42 fires the cue with trigger_note: {ch: 1, note: 42}.In both cases the cue is triggered exactly as if it were the next Go on that cue. Because every cue has a trigger_note (assigned automatically if not set), no extra per-cue configuration is needed.
For hardware or software that sends OSC (Companion, TouchOSC, a button box …): Settings → Remote control → Go/Back/▲/▼ by OSC. MDstage listens on a UDP port (by default only from this computer; tick accept from the network for other machines) for four addresses, by default /mdstage/go, /mdstage/back, /mdstage/up and /mdstage/down; each can be changed or left empty. A message to one of them acts; a message whose first argument is the number 0 does nothing, so a button that sends 1 when pressed and 0 when let go of acts once. Back acts at once (holding is a MIDI matter). The port may be the same as that of the OSC cue trigger.
OSC (Open Sound Control) output devices are part of the same unified device list as MIDI devices (see MIDI). Each OSC device has a name, host, port, and optional colour.
cue_osc)A cue can send OSC messages to any configured OSC device when it fires:
cue_osc:
- device: "QLab"
path: /cue/42/start
- device: "QLab"
path: /volume/set
arg: 0.8
arg_type: float # int | float | string
If device is omitted, the first OSC device is used. The Back function restores device states — see Cue History (Back).
osc fieldThe older single-path format is still supported:
osc: /show/cue/go
osc_arg: 0.8
osc_arg_type: float
The {ch} placeholder in the path is replaced with the role's channel number (two digits, 1-based). The OSC badge ⌁ /path … is shown on the trigger panel. This field does not participate in Back's device state restoration.
HTTP output devices are part of the same unified device list as MIDI and OSC devices (see MIDI). Each HTTP device has a name, a base URL (e.g. http://192.168.1.50:8080), and an optional colour. HTTP is available for per-cue messages only (not for the mixer remote control).
cue_http)A cue can send HTTP requests to any configured HTTP device when it fires:
cue_http:
- device: "Lichtpult"
method: POST # GET | POST | PUT | DELETE | PATCH
path: /api/scene/3
body: '{"fade": 2.0}'
content_type: application/json
The request is sent to <device base URL> + path. body and content_type are optional (a body is only sent for non-GET/HEAD methods). If device is omitted, the first HTTP device is used. Like cue_midi/cue_osc, HTTP messages participate in Back's device state restoration (see Cue History (Back)).
cue_display)A Display device exposes a web interface that shows per-cue markdown, reachable at http://<address>:<port>/<slug> — the slug is derived from the device name (lowercased, umlauts → ae/oe/ue/ss, everything else → hyphens; e.g. Bühne Links → buehne-links). The settings screen shows the exact URL. The bind interface (all networks / localhost / a specific adapter) and the port are single machine-local settings shared by all displays (default port 7590); the settings screen shows the resulting address. A cue sets what a display shows:
cue_display:
- device: "Stage Left"
markdown: |
# Scene 3
Cue in 4 bars
announce: "Scene three, standby" # optional text-to-speech, spoken on Go
The markdown is rendered (headings, lists, emphasis, links, images) and pushed live to any connected browser via Server-Sent Events. Browser zoom (pinch / Ctrl-scroll) works, and if the content is taller than the viewport the page auto-scrolls page-by-page and back to the top every N seconds (configurable per device, 0 = off).
What a display shows per cue — each cue_display message controls one display device:
| Message content | Effect on that display |
|---|---|
markdown set | Show the new markdown (replaces the previous content). |
only announce (no markdown) | Keep the current content and speak the announcement (a "hold"). |
neither markdown nor announce (an empty message) | Clear the display. |
| no message for the device | Leave the display untouched — its content persists across cues, like MIDI/OSC device state. |
On Back, the effective content is recomputed from the remaining cue history, so the previous state (including holds and clears) is restored exactly. Back never re-speaks announcements.
Each display device chooses a stylesheet: a built-in dark (default), light, or subtitle (white text on black, centered), or a custom .css file dropped into a css/ folder next to the show .md.
Announcements (announce): When a cue fires (Go / auto), every browser client of that display device plays a theatrical front-of-house chime and reads the announce text aloud via the browser's text-to-speech. Back does not re-announce. The announcement is fired only on the trigger, independent of the markdown restoration. Per display device you can enable repeat, which speaks the announcement, then "I repeat" (translated to the device's language), then the announcement again.
Client presence & naming: Each browser tab that opens a display URL is remembered and shown in the app's live view as a coloured dot — green while connected, red when the connection drops (the client retries automatically and turns green again on reconnect). Several clients can share one configured device. Each client has a small settings panel (⚙) to set a name, a TTS voice and a zoom level; the voice list only offers voices matching the device's configured language (set in MDstage's display-device settings). name and voice are also read from URL query params (?name=Foyer&voice=Anna&zoom=1.5) for direct kiosk setup and are stored as cookies (query param wins over cookie). Renaming a client updates its label in the live view live. The name, voice and zoom appear back in the URL so a configured client's URL can be reused as-is.
Landing page: Opening the server root (http://<address>:<port>/ with no slug) shows an index that lists every configured display device with a link to its page. Point a backstage browser at the bare address and pick the screen you need — no URL typing. The list updates as devices are added or removed.
Kiosk-friendly client: On a display page the settings gear (⚙) stays hidden and only fades in while the mouse moves, then fades out again after a couple of seconds — so a permanent stage/foyer screen shows nothing but content. Because browsers block audio until the user interacts, the client probes on load whether audio is already permitted; if it is not (a normal browser), a one-tap “🔊 Ton aktivieren / Enable audio” button appears so announcements can play. On a kiosk configured to allow autoplay the audio starts unlocked and the button never appears.
Network binding & security: The display server binds to 127.0.0.1 (this computer only) by default; to reach it from tablets or stage monitors over the LAN, switch the bind interface to All networks (0.0.0.0) or a specific adapter in the settings. The server is read-only for clients (they only receive content) and serves no files outside the built-in assets and the show's css/ folder.
The app generates timecode synchronised to audio playback, as MIDI Timecode (to MIDI output devices with Timecode (MTC) ticked) and as Art-Net timecode (ArtTimeCode, to Art-Net devices: + Art-Net in the device list, with an address — a device or a broadcast address such as 2.255.255.255 — and port 6454). Art-Net timecode is sent once a frame. Art-Net devices take no cue messages and do not appear in the show file.
Frame rates: every show has a frame rate — 24, 25, 29.97 drop-frame or 30 fps — set under Settings → This show's frame rate while the show is open. It is kept on this computer (by show file), not in the show file; a show without one runs at 25 fps. The show's rate is what the frames of start_tc count, what the timecode in the header, the live view and the cards is shown in, and what every device set to as the show gets. A device can have a rate of its own instead; it then gets the same moment in its own frames. A start_tc with more frames than a second has at the show's rate (:27 at 25 fps) is reported when the show is opened and sends no timecode.
start_tc fires, MTC starts from that offsetA separate window (Cmd/Ctrl+L) for the stage or operator monitor.
Contents:
Go/Back in the live view works exactly like in the main window. The script scrolls smoothly so that the next cue always sits a little below the middle.
The live view in a browser on another device in the local network — a tablet at the side of the stage, a phone — with Go, Back, ▲ and ▼ (and a tap on a variant's card to choose it). Off unless switched on under Settings → Remote live view, where the interface (0.0.0.0: every network of this computer), the port (7591) and a PIN are set; the address to open is shown there.
Three emergency buttons in the header:
| Button | Function |
|---|---|
| Emergency Light | Sends the MIDI note configured in emLightNote (e.g. emergency lighting) |
| Music Stop | Fades all running audio out over 500 ms and stops it; stops MTC |
| Mics Off | Mutes all configured microphone channels on the mixer |
| Action | Result |
|---|---|
| Click on trigger | Fire cue immediately |
| Click with live view open | Pre-select cue as next (arm) |
| Go (Space in the live view; in the main window only while the show is locked) | Fire next cue |
| Back (Backspace, when live view is open) | Undo last cue (fade-out + undo) |
| MIDI Go/Back | Configurable MIDI notes. Back acts when its note is let go of; held for one second instead, it stops all audio (as the red music button does) and does not go back |
| Arm button on a cue | Arms the cue as the next Go without firing it |
| ▲ / ▼ buttons on a cue | Move the cue (with its variants) one place up or down |
| Cross-hairs button | Scrolls to the next cue |
| Clock button beside it | Scrolls to the cue that fired last |
↑ / ↓ | Leaf to the cue before / after the place the view is scrolled to (whatever is armed). In the live view the two buttons ▲ ▼ beside Back do this and arm the cue they go to |
Editing while the show runs: adding, deleting, moving or editing cues stops nothing. Audio that plays goes on, the cues that have fired stay fired and the armed cue stays armed (cues are known again by their trigger note); the live view shows every change at once. A change to audio that is playing — loop on or off, start, end, fades, crossfade, volume — takes effect on the spot. Only the audio of a cue that was deleted fades out.
The place is remembered: the next cue is stored per show file (in the machine's settings) and armed again when the show is opened the next time.
A cue that has fired says when: fired – hh:mm:ss. Neither that time nor the place the show has got to is written into the show file; the file is only written when the script is edited.
Only scenes and cues: a button in the toolbar folds all text away, leaving the headings and the cues. The view stays at the place it shows when the button is pressed.
Audio that sounds: as in the live view, the foot of the main window has a row for every audio that is playing, with its progress and a button that stops just that one.
Double-clicking a playing cue stops it immediately (undo function).
The view glides to where it is sent (a fired cue, a scene, a search hit, a referenced cue) instead of jumping; scrolling by hand takes over at once.
Go waits for cues that come by themselves: while a cue is on its way without the operator — a delay auto-cue counting down, an auto-cue at a place in audio that plays once, an outro waiting for its loop's end, a cue its predecessor chains into — Go does nothing: the button is greyed out, and the space bar and a remote Go (MIDI/OSC) are ignored too. A cue can still be fired by clicking it, and Back works. (An auto-cue in a loop comes round on every pass and does not hold Go back.) The waiting cue shows a bar — in the live view on its card, in the main window along the bottom edge of its card — that always starts empty when the wait begins and is full when the cue fires, wherever the audio stood at that moment. The bar is orange for an auto-cue and purple for a start/loop/finish handover.
Go after jumping back: Go fires the next cue after the operator's position that has not fired in the current pass. After clicking an earlier cue, every cue from there on is due again — nothing is skipped because it had fired before the jump. Cues that fired by themselves in the current pass (a loop its start chained into, an auto-cue) are still stepped over.
Back puts everything as it was before the last Go, as if Go had never been pressed. It takes back, one press at a time:
What the undone cues did to other audio is undone too:
Device state restoration (MIDI/OSC/HTTP): When Back pops a cue, the app checks which output devices that cue addressed via cue_midi / cue_osc / cue_http. For each such device, it scans backwards through the remaining cue history and resends the message set from the most recent earlier cue that addressed the same device. If no earlier cue addressed that device, nothing is sent — the device is left in its current state. Devices with tracking off (see Tracking) are never sent again on Back: what they do happens with its very cue only.
The old osc: field and trigger_note are not part of this restoration mechanism. Only cue_midi, cue_osc, cue_http and cue_display messages participate in device state tracking.
When a cue is fired out of order — armed and Go, clicked, or fired by its trigger note from outside — MDstage first sets up the state going through the script up to there would have left, as a lighting desk does. That state is worked out from the cues above it in script order (of a variant group: the variant fired last in this performance, or the one chosen when asked). An ordinary Go on the next cue is not a jump and changes nothing.
| On a jump | |
|---|---|
| Microphones | always (even with tracking off): if the cue sets none, those the cues above left. |
| Loops | those that would be playing there start (from their beginning, faded in, at the level the cues above leave them); playing ones that would not fade out. |
| Audio that plays once | is never started. If it plays and came from a cue below the target, it fades out; from above, it plays on — unless a cue above faded it out. |
| Levels | audio that plays on is faded to the level the cues above leave it at (ducked by an adjust, or not). |
| MIDI / OSC / HTTP / displays | each device with Tracking ticked gets what the last cue above that addressed it said, unless it has that already or the target addresses it itself. A display no cue above addressed shows nothing, as at the start of the show. Announcements are never repeated. |
| Finish / bridge as target | the loop it finishes starts (if it is not playing), plays its pass up to where its tail begins, and the finish comes in as usual. |
| Skipped auto-cues, delays | are not made up for. |
Before Go: while a jump is armed, its card (main window and live view) shows a Track row with what Go does beyond the cue: ▶ loops that start, ■ audio that stops, levels that change, the microphones, and each device with the cue whose state it gets (or "empty" for a display).
Variants: if no variant of a group above has been fired yet and the variants differ in something tracking would set, MDstage asks which one counts — when the jump is armed, else before it fires (also for a cue fired from outside: the cue waits for the answer, in the live view if it is open). Cancel fires nothing; Back calls the question off.
Back after a jump takes the jump and its tracking back together: loops tracking started fade out, audio it stopped comes back (loops in beat), microphones and devices are as before, and the jump is armed again.
Settings (both kept in the show file): Settings → Tracking → Tracking on jumps (this show) (config.tracking: false switches it off; microphones still follow), and a Tracking tick on each output device (outputDevices[].tracking: false). One physical device can be set up twice — e.g. a lighting desk once for scenes (tracking on) and once for effects that fire only with their very cue (tracking off).
The Role Editor… menu opens a dedicated window for managing roles.
A new role or group gets the colour that has been used least so far, so that new roles go through the colours one after the other.
Roles can be renamed; all **Role name** occurrences in the script are updated automatically.
Groups bundle multiple roles into a named set. They are managed in the Role Editor window, below the individual roles.
Group names must not conflict with existing role names; the editor highlights duplicates before saving.
config:
groups:
Ensemble:
color: purple
roles: [Anna, Ben]
Chor:
color: cyan
roles: [Anna, Ben, Clara]
A group name can be used anywhere a role name is accepted for mic::
mic: Ensemble # opens all channels of the group
mic: [Ensemble, Clara] # group + additional individual role
When a cue fires, the group is expanded to all its member roles for MIDI/OSC routing. In the cue panel and live view, the group is shown as one outlined label with its name in the group's colour; roles that are open on their own follow as filled labels. A role that is open as part of a group is not listed again.
The built-in group Alle (German) / All (English) always refers to all defined roles and cannot be created manually.
Mic assignments are shown as grouped chips by default. The display can be switched to individual chips (flat list) via Settings → Mic group display.
Settings… (Cmd/Ctrl+,) opens:
| Setting | Description |
|---|---|
| Audio device | Output device for all virtual channels |
| Virtual channels | Define and name the virtual channels; route each 1:1 to a physical output of the device |
The unified device list manages all MIDI, OSC, HTTP and Display output devices. Each device has:
| Property | Description |
|---|---|
| Name | Display name — used in cue_midi / cue_osc / cue_http / cue_display to target the device |
| Type | midi, osc, http or display |
| Colour | Optional colour shown as a badge on trigger panels |
| Send trigger note | MIDI/OSC only: if enabled, receives the cue's trigger_note when a cue fires |
| Device (MIDI) | System MIDI port name |
| Host / Port (OSC) | UDP target address |
| Base URL (HTTP) | Base URL for cue_http requests, e.g. http://192.168.1.50:8080 |
| Scroll / Style / Language (Display) | Auto-scroll interval in seconds (0 = off), stylesheet (dark/light/subtitle or a custom css/…css file), and the TTS language (the client then only offers voices for that language). The URL path is a slug derived from the device name (lowercase, umlauts → ae/oe/ue/ss, other characters → hyphens): http://<address>:<port>/<slug>. The bind interface (0.0.0.0 / 127.0.0.1 / a specific adapter) and port are single machine-local settings shared by all displays; the settings screen lists all addresses the display is reachable from (every network adapter's IP for 0.0.0.0). |
Addresses are machine-local. Only the device name, type and colour are stored in the
shared script.md. The actual endpoint (MIDI port / OSC host:port / HTTP base URL) and the
enabled / send-trigger-note / send-timecode flags are stored locally on each machine
(editor-prefs.json in the app's user-data folder), keyed by device name. This keeps addresses
(incl. internal IPs) out of shared show files and lets the same show run at different venues —
each operator configures the local addresses once. When you open a show whose device is not
configured on this machine, a banner notes it and its cues stay silent until you set the address
in settings. A cue message whose device name isn't configured locally sends nothing (no
fallback to a default device).
| Setting | Description |
|---|---|
| Timecode | Timecode (MTC) and frame rate on a MIDI output device; Art-Net devices with address, port and frame rate |
| Input device (Go / Back / ▲ / ▼) | Filter MIDI input to a specific device, or receive from all |
| Go / Back / ▲ / ▼ note | MIDI notes — can be assigned via MIDI Learn |
| Go/Back/▲/▼ by OSC | UDP port, network access and the four OSC addresses |
The Devices menu lists every MIDI, OSC and HTTP output device with a tick. Without the tick nothing is sent to that device — so that, say, the lighting desk is left alone while its operator is programming during a rehearsal. A line above the script names the devices that are switched off. Microphone control in the same menu does the same for the mixers: without the tick no microphone is switched, not by "Mics Off" either. Block all outputs holds everything back, timecode included. (The tick is the same one as in the device list of the settings.)
The microphone muting method can be configured independently of the MIDI trigger output:
| Method | Description |
|---|---|
| X32 MIDI (CC channel 2) | Default: sends CC on MIDI channel 2; value 0 = active, 127 = muted |
| Custom MIDI | Freely configurable MIDI messages (Note On/Off, CC, Program Change, or SysEx/hex bytes). {ch} is replaced by the role's channel index (0-based). |
| Custom OSC | Sends separate OSC paths for mute (OFF) and unmute (ON). {ch} is replaced by the role's channel number (1-based, two digits). |
| Setting | Description |
|---|---|
| Emergency Light note | MIDI note for the emergency light button (channel + note number) |
| Language | App interface language (German / English) |
| Open in editor (right-click) | A command that opens a line of the show file in a text editor ({file} and {line} are filled in); empty: off |
| Markdown line numbers | Shows, left of every block, the line of the show file it begins on |
| Vim keys in the text editor | Normal and insert mode in the inline editor (see Inline Editor) |
| Auto mic: switch microphones off after | Seconds an Auto-Mic cue leaves a microphone on that it switches off (default 7; 0 = at once) |
| Block output to devices | Nothing is sent to MIDI, OSC, HTTP, displays or timecode |
| Mic group display | When enabled, groups are shown as one outlined label each (groups first, then single roles). Disable for a flat list of roles. |
| Open locked | When enabled, the script editing lock is activated automatically on startup. |
Settings belong to the machine, not to the show: they are stored in settings.json in the app's settings folder (macOS: ~/Library/Application Support/MDstage), so the same show file can use different devices on different computers. The show file only declares which devices and virtual channels the show needs.
Cmd/Ctrl+E opens the export dialog:
HH:MM:SS) of each cue is written into the cue tables. Timestamps are captured live whenever a cue is triggered (overwritten on each re-trigger) and are session-only — they are never saved to the show .md; this option is the only way they leave the app.Output formats:
Each S/L/F group receives a label (Start, Loop, Finish, Bridge).
Cmd/Ctrl+B or the sidebar button opens/closes the sidebar#, ##, and ### headings# 7 In the garden, # 12. Finale) has that number; where a script numbers scenes itself like this, a scene without a number written in gets none. Typing a number outside any text field goes to that scene: 2 for scene 2, 1 then 7 for scene 17.Cmd/Ctrl+F opens the search bar; pressed again it clears what was typeddu sollst nach hause gehen finds "Geh nach Hause", hallo schatz finds "Hallo, mein Schatz"When a file is opened, the app checks whether the script meets the formatting standard:
If formatting is needed, a backup copy (*~unformatted.md) is created before the file is reformatted.
A file that was last saved on another machine is not touched by opening it: it is shown formatted, but nothing is written into its folder until you change something. MDstage never writes through symbolic links in a show folder.
| Shortcut | Function |
|---|---|
Cmd/Ctrl+N | New file |
Cmd/Ctrl+O | Open file |
Cmd/Ctrl+, | Settings |
Cmd/Ctrl+L | Open live view |
Cmd/Ctrl+E | Export |
Cmd/Ctrl+B | Toggle sidebar |
Cmd/Ctrl+F | Search (clears the field if it is open already) |
↑ / ↓ | Main window: go to the cue before / after the place scrolled to |
Space | Live view: Go. Main window: Go only while the show is locked; otherwise play/pause of the audio whose playhead was last moved on its waveform (nothing after a cue was fired by a click) |
Backspace | Back (when live view is open, no editor active) |
Shift+Click | Edit block |
Tab | Accept role name autocomplete |
Shift+Enter | Line break inside a role block |
Escape | Close editor / search |
| digits | Go to the scene with that number |
What changed from version to version — new features, changed behaviour and every change to the show file's YAML keys — is in CHANGELOG.md. Show files written by any version from 2.0.0 on open and play in every later one.
MDstage is a Rust workspace:
| Crate | Contents |
|---|---|
crates/mdstage-core | Reading and writing the show file, cue logic, checks — no I/O |
crates/mdstage-engine | Audio, MIDI, timecode, OSC, HTTP, display server, the running show |
crates/mdstage-app | The user interface (egui) and the export |
cargo test --workspace # all tests
cargo run --release -p mdstage-app -- SHOW.md # run the app
scripts/bundle-macos.sh --dmg # target/bundle/MDstage.app and .dmg
scripts/bundle-macos.sh --dmg --x64 # the same for Intel Macs (built on any Mac)
scripts/bundle-appimage.sh # AppImage (on Linux)
Linux needs the development packages of ALSA, X11/Wayland and GTK 3 (on Debian/Ubuntu:
libasound2-dev libxkbcommon-dev libwayland-dev libx11-dev libxcb1-dev libgtk-3-dev).
Packages for all targets are built by GitHub Actions (.github/workflows/build.yml); a tag
(2.0.0, without v) makes a release.
Audio files go in the audio/ subfolder next to the .md file. Use WAV format for seamless S/L/F transitions.
sibling: true | Marks the cue as a variant of the preceding cue (alternative audio/mic assignment). |