File formats

The workspace file

A show is one .cuerunner file: JSON, holding the cues in order, the show's setup — the desk, the cast, the scenes, the mic plot, the triggers, the lighting rig — and the window's own state such as which cue was selected. The rig is a lighting section — fixtures, the types they are, groups, each universe's outputs and the consoles heard — written only once a show has one, so a show with no lights saves the bytes it always did. Which network interface the lights leave by, and the sACN identity this machine sends as, are the machine's, in dmx.json in the user-data folder.

A fixture type read from a GDTF file (DIN SPEC 15800; Lighting → Patch → Import GDTF file…) is copied into the show like any other type, so the show does not need the file again. Only the archive's description.xml is read — the models and wheel images beside it are not — up to 16 MB of XML, and a description that declares a document type is refused. Each DMXMode becomes a mode, as wide as its highest offset. Each DMXChannel with an offset becomes a parameter: its kind from the attribute of the function it starts on (Dimmer, Pan, Tilt, ColorAdd_R and the other emitters, colour wheels and macros as colour, everything else as other), its label from the attribute's pretty name — with the geometry after it where cells share an attribute — its coarse and fine slots from Offset, and where it rests from the default of its initial function (or, in a GDTF 1.0 file, the channel's own Default). A channel with more than one ChannelFunction keeps each one's name and the share of travel it starts at (DMXFrom), up to 64, and the faders name the one a value is in. A 24- or 32-bit channel is driven on its two most significant bytes and the bytes below are held at zero. A virtual channel (no offset) is left out. Refused, with a sentence: a mode spread over more than one DMXBreak, a channel that overwrites another break, an offset past 512, two channels on one slot, a mode with no DMX channels, more than 32 modes or 128 parameters to a mode.

A show with more than one cue list carries a cueLists section: each list's id, name, colour, whether it is a cart, whether it is disarmed, how it follows the clock and its tiles' own keys, in tab order. A cue at the top level of a list other than the main one names it with listId; a cue inside a group never does — it runs in its group's list. A show with only its main list under its own name writes no section and no listId, so a show saved before lists existed opens as one list and saves the bytes it always did. A cue naming a list the file does not have runs in the main list, and the bar across the top says so, the way a grouping repair does.

It does not hold the audio. Each audio cue stores the path to its file. Moving a show to another machine means taking the sound with it and keeping the paths valid: the simplest arrangement is a folder with the .cuerunner file and the audio beside it, copied whole.

Video and image cues store their files the same way, and move with the show the same way. The show's outputs are a videoOutputs list — name, size, timer, corner pin and soft edges, a mirror and a mask — written only when the show has one; where each output goes on a computer is kept on that computer, in video-outputs.json in the user-data folder. A show whose pictures go on the next GO carries a pictures section; a camera cue carries its camera's name, and each computer keeps the device it picked for that cue in camera-devices.json.

A cue whose file has gone says missing where its length should be, and a bar across the top of the window offers to relink it — point it at the file once and the rest of the cues in the same folder follow.

If a file's cue grouping is damaged, it is repaired as the show opens rather than loaded as-is: a group's children are gathered into one block behind it, a cue whose group is not in the file keeps its place at the top level and fires with the show, and a loop of groups is broken so every cue in it returns to the top. A bar across the top of the window says how many repairs were made, and the list behind it names each one. The file on disk is unchanged until you save, which writes the repaired grouping.

Show files are not converted. The shape is declared and checked down to the fields, and a file this build does not know is refused — naming the file and the fact, with nothing on disk touched — rather than filled in. A settings section this build writes and the file leaves out is a refusal, and so is a field inside a section that this build does not write or that the file has not got. What a field carries is checked as well, but only for its JSON kind: a string where this build writes a number — or a number where it writes a list of strings, or an entry of such a list that is not what the list carries — is refused the same way, and null is read only where the show's own types allow it. A cue's own fields are read the same way, and the sentence names the cue: a MIDI cue's messages are a list, every message an object, a crosspoint's gain a number, and an OSC argument's value a number, a string or a boolean — a cue field carrying any other kind refuses, as does null where a cue's types allow none. What a cue carries that this build does not declare is kept rather than refused, because a cue on disk holds fields the runtime cue type does not: media, the route its file was found by. What a value says is the app's reading rather than the file's, so a name the code does not offer, a desk kind among them, is read as it stands. The one that catches a show saved before the scenes changed is inside the settings: the strips, the faders and the scenes are checked field by field, so a show whose channels carried a fader of their own, or whose scenes listed who was open rather than what rides what, is refused by name. Setting it up again takes minutes and no hand-editing: the channels and faders in Show Settings → Channels, and the placements in the Scenes grid, which is where they live now. A file refused over a value is the other case, and not one to set up again: the sentence names the field and both kinds, and since nothing here guesses what the value was meant to be, the file is corrected where that value sits or opened in the build that wrote it. Saving does not convert one either: the show on screen is written as it stands, and a save over a file this build cannot read keeps the history it had and replaces the file all the same.

The bundle file

A .cuerunnerx is the show and its media as one file — the audio, video, image and MIDI files its cues play — a zip holding show.cuerunner and a media/ folder, with the showfile pointing inside it. It opens in any file manager, and its bytes are a function of the show, so two exports of the same show are the same file.

It carries up to about 4 GB of media — the zip format's own limit without its 64-bit extension, which Cuerunner does not write. A larger show is refused rather than written as an archive that misreports its own contents.

The file also carries a checksum of everything in it, and an import whose contents do not match is refused rather than half-unpacked. See Versions and handover.

QLab workspaces

Import QLab Workspace… reads a .qlab5 or .qlab4 workspace, a QLab 5 workspace saved as a folder (the property list inside it), a QLab 5 backup (.qlab5backup, the same archive; in its backups folder its audio is looked for from the folder above, where the workspace was), and a .cues file of cues with no list around them. A workspace is an NSKeyedArchiver archive in a binary property list, with its cue lists stored as a second archive inside the first; both are Apple's container formats, read by Cuerunner's own reader. QLab 5 archives each cue list as a Group cue of mode 0 (a cart, mode 5) under a root group, and each becomes a cue list rather than a group. An audio cue's slices are its AudioSlice markers, each the end of a slice, and its lastSlice; a marker past 127 is not carried. A workspace past 64 MB is refused before it is read, and so is one holding more than 10,000 cues.

From a running QLab, the import connects over TCP to the address and port given (53000 by default), SLIP-framed as QLab's OSC over TCP is, and sends /workspaces, /workspace/{id}/connect with the passcode, /workspace/{id}/cueLists, and /workspace/{id}/cue_id/{id}/valuesForKeys once for each cue — twice for an audio or devamp cue, whose slice markers and devamp settings are asked for on their own so a QLab that does not know them still gives the rest. One message is in flight at a time. The connection has 3 seconds to open and every reply 5 seconds to arrive; a reply larger than 16 MB, or a conversation past 64 MB, ends the import.

The result is a new show with no file, checked by the same reading every .cuerunner file goes through. Audio paths are absolute: the QLab workspace's folder joined to the path QLab recorded relative to it, or QLab's last known absolute path where there is none. Find in Folder… reads each folder it looks in to at most 10,000 entries, once, for a file whose name differs only by ' ’ : / ? " < > | * \ written as _ or by its Unicode form, and takes it only when exactly one does. The mapping is in Coming from QLab.

The import saysBecause
… is not a QLab workspaceThe file is not a binary property list, which every QLab workspace is.
… looks like a QLab workspace but cannot be readIt is a property list whose bytes contradict themselves, or not an archive; the rest of the sentence says which.
… holds no QLab cue listsAn archive, but not of a QLab show.
… is N MB, and a QLab workspace this import reads is at most 64 MBThe size limit, checked before the file is read.
no QLab answered at …Nothing is listening at that address and port, or QLab's OSC access does not let this computer in.
… did not accept that passcodeThe workspace's passcode is not the one typed.
… stopped answeringA reply did not arrive within 5 seconds.

Wireless telemetry

Apart from Sennheiser EW-DX receivers, which Cuerunner reads directly, it does not speak any receiver's own protocol. A source — an HTTP address or a file on disk — returns this document:

{ "channels": [ { "id": "ulxd-1", "name": "Pack 1", "batteryPercent": 42, "batteryMinutes": 95, "rfDbm": -62, "audioLevel": 71, "muted": false, "frequencyMhz": 606.5 } ] }
FieldMeaning
idStable identifier for the channel. Required.
nameWhat the receiver calls it.
batteryPercent0–100.
batteryMinutesRuntime left, if the receiver reports it.
rfDbmSignal strength, negative dBm.
rfWeakWhether the receiver itself calls the signal weak. When it is present it replaces the Signal below line for that channel, and it goes with rfDbm.
audioLevel0–100.
mutedWhether the channel is muted.
warningsWhat the receiver itself says is wrong, as a list of { "text": "audio clipping", "critical": false }. Each is announced once when it appears and shown under the pack; critical marks one that makes the pack unusable until it is dealt with.
frequencyMhzThe frequency it is on.

The field names are the contract: a bridge written against a receiver's own API only has to emit them. Any field a channel does not report is left out, and the panel shows nothing for a reading that was never made rather than a zero.