Skip to content

V1.9.12

Choose a tag to compare

@fiverecords fiverecords released this 14 Sep 20:29
· 300 commits to main since this release
V1.9.12

This release opened with a full audit of the code base and grew from there:
every finding that could be fixed without hardware was, four testers'
issues were answered along the way, and one long-standing design problem --
each audio component opening the interface for itself -- was replaced by a
shared device.

Upgrading. Existing configurations load unchanged. Three behaviour
changes are visible without touching a setting:

  • LTC user bits now put the first digit you type in binary group 8
    instead of group 1, which is what most readers expect; if you had typed a
    manual value backwards to suit your reader, un-reverse it and switch on
    REVERSE DIGIT ORDER.
  • 23.976 now counts as the standard specifies and runs 0.1 % slow
    against the wall clock (3.6 s per hour), like any 23.976 generator. If you
    used it as a wall-clock timecode, use 24 or 29.97 drop-frame.
  • The generator's audio playback no longer has its own sample rate and
    buffer size; it follows the global setting.

Features

Track Map: per-engine overrides

Reported by a tester running a lighting console on one engine and a video
server on another: the shared Track Map meant remapping cues for the
lightshow re-programmed the video too. The map stays shared for what a
track is -- identity, offset, notes -- and gains a per-engine override
layer for what differs between show systems: the triggers, the BPM
multiplier and the cue points.

  • The Track Map editor has a Scope selector: "Global (all engines)" or
    "Engine N overrides". In override scope the list shows that engine's
    overrides; offset and notes are read-only with the global values; Import
    is disabled (a playlist import would create empty overrides).
  • Copy from global starts an override from the global entry. Learn and
    Add in override scope also start from the global triggers and cue points
    when the track has a global entry, so you change only what differs. An
    override with no triggers is a deliberate "nothing on this engine".
  • Lookup: the engine checks its overrides first (same key rules as the
    global map) and falls back to the global entry. Engines without
    overrides behave exactly as before.
  • Stored as trackMapOverrides inside the engine's block of settings.json
    (omitted when empty), so overrides follow the engine and travel in the
    backup bundle. Existing configurations are unchanged.
  • Fixed alongside: applying a playlist order from a rekordbox import saved
    the file but did not refresh the engines' lookup until the next edit.

TCNet metadata from audio playback, and localhost (issue #20)

  • When the engine's source is the generator, the TCNet track metadata now
    carries the preset in use as the title -- the name the operator gave
    that part of the show -- with the artist from the loaded audio file
    (WAV/AIFF artist tag, or the "Artist - " half of a file named "Artist -
    Title"). With no preset selected, the file's title and artist (tags, or
    the file name). Resolume showed only the engine name before. The preset
    in use is remembered per engine across restarts and shown in the preset
    selector.
  • Every network interface selector for the software protocols -- TCNet,
    Art-Net (timecode and DMX), LA-Net input, HippoNet, OSC input -- offers
    "Localhost (127.0.0.1)" as its last entry, for a receiver running on the
    same PC. Pro DJ Link and StageLinQ do not (hardware on the LAN). Saved
    selections keep their meaning.

Thanks to @trickofdjequip for both.

Freewheel for the signal inputs

MTC, LTC, Art-Net, LA-Net and HippoNet inputs keep counting as present for
an operator-chosen time after the last frame or packet -- FREEWHEEL in the
input panel: 150 ms (as before), 250, 500 ms, 1 or 2 s, per engine. The
senders count on their own through the window, so a dropout shorter than
it never reaches the wire; a real stop reaches the outputs that much
later. Your trade-off, not STC's.

LTC user bits: SMPTE ST 309 date (issue #13)

The date mode now carries what SMPTE ST 309:2012 specifies instead of the
free-form YYYYMMDD placeholder: groups 1-6 hold the local date as YYMMDD
(units before tens, ascending groups), group 7 the low four bits of the
time zone code, group 8 its high two bits plus the daylight-saving and MJD
flags, and the binary group flags declare it (BGF2 = 1). The zone code
follows ST 309 Table 2 (whole and half hours, +12:45, "unknown" for
offsets the table has no code for) and is read from the system clock
together with the DST state, once a second.

On input, the binary group flags are decoded and the user-bits display
interprets what the sender declared: an ST 309 date with zone and DST (both
the YYMMDD and the modified-Julian-date forms), or the four characters of
an eight-bit character set. A user-bits mode combo bug is fixed alongside:
an engine restored in NAME mode showed the combo at SYSTEM DATE.

Thanks to @mungewell for locating the standard in SMPTE's public library.

Digit order of the user bits. Three readers (libltc-based tools,
Aputure's Sidus TC Sync, Pico-Timecode) print the eight binary groups with
group 1 as the least significant digit, which is also how the standards
assign digits to groups; STC put the most significant digit in group 1,
so those readers showed a manual value reversed. STC now follows the
majority: the last digit you type goes into group 1. In the hex display
this makes the other modes readable too -- a NAME of STC1 shows as
53544331, its ASCII bytes in order, and the ST 309 date as <zone><YYMMDD>.
For readers that print the groups the other way round (GoPro, and STC
before this version), the MANUAL mode has a REVERSE DIGIT ORDER
option, per engine. If you had typed a manual value reversed to suit such
a reader, un-reverse it and turn the option on. The date, name and
passthrough modes are unaffected on the wire.

Timing and accuracy

LTC output phase lock

The LTC bit stream runs on the audio interface's clock and the timecode it
carries on the source's; the two drift apart at the difference between the
clocks -- 50 ppm is a frame every eleven minutes -- and until now the frame
phase sawtoothed through a full frame between whole-frame corrections. The
encoder now measures the source's phase at every frame boundary and trims
its bit clock by up to +/-200 ppm to hold it: drift is absorbed, the
phase stays within a fraction of a frame, and no frame is repeated or
skipped for it. Found on the way and fixed: the encoder rounded every
frame to whole samples, which also cost the pitch multiplier its
precision (1778 samples per frame at +8 % on 25 fps instead of 1777.8).

Senders under pitch: one-frame corrections instead of jumps

MTC and Art-Net keep their nominal frame rate by design, so when a deck
runs at a pitch the value has to step. The senders snapped to the engine
value whenever the distance exceeded a frame (two for MTC): at -8 % Art-Net
went backwards two frames every 0.8 s and MTC three frames every 1.6 s,
which a console treats as a locate. All four senders now correct one frame
at a time -- a repeated frame when ahead, a skipped frame when behind --
and snap only on a real seek. The same policy absorbs clock drift between
the audio device and the source on LTC (a two-frame jump every ~20 minutes
before).

Drop-frame resync in every sender (LTC, MTC, Art-Net, LA-Net)

The auto-increment/resync rule measured its distance on a linear frame
count, which reads the 59;29 -> 00;02 crossing as three frames. With the
engine value a fraction of a frame behind at that instant (normal at a
60 Hz tick) the sender snapped back, repeated a frame (two for MTC), and
then ran a frame behind the source for the rest of the track. All four
now measure on the drop-frame index.

Timecode arithmetic

  • Drop-frame frame index. TimecodeCore gains
    timecodeToFrameIndex / frameIndexToTimecode / frameDistance: the
    true frame count of a 29.97 address, honouring the two numbers skipped at
    every non-tenth minute. Every place that did frame arithmetic on a
    linear count now uses them.
  • Output offsets are exact at 29.97. offsetTimecode was up to two
    frames off for |offset| frames around every non-tenth minute (54 times
    an hour) -- 1,166,400 wrong results per day across the +/-30 range
    against an exact reference. Now zero.

23.976 is counted as the standard specifies

The time address at 24/1.001 now counts 24 frames per timecode second and
runs 0.1 % slow against real time (3.6 s per hour), as ST 12-1 states and
as every 23.976 generator does. Previously STC kept HH:MM:SS on the wall
clock and fitted 24 frame numbers into each real second: one number too
many on the 23.976 carrier every 41.7 s (a skipped frame on every output),
and a video server counting clip frames drifted 3.6 s per hour from the
audio. If you used 23.976 as a wall-clock timecode, use 24 (same frame
grid, real-time speed) or 29.97 drop-frame instead.

Phase alignment (issue #15), all sources

Art-Net and LA-Net timecode inputs publish the packet arrival as the frame
start, like MTC and LTC. The LTC encoder is re-seeded on every
inactive -> active transition for every source (it used to be only Pro DJ
Link and StageLinQ), so a resume from pause on MTC, LTC, Art-Net, the
generator or Winamp re-aligns the frame phase.

LTC output (issues #14, #15)

  • Frame value at seed. The phase alignment shipped in 1.9.11 positioned
    the bit pointer correctly but discarded the whole frames contained in
    "phase + age + latency"; whenever that sum crossed a frame boundary the
    first frame carried a value one frame late. Since the phase at the
    instant of seeding is arbitrary, each restart landed 0 or 1 frame off --
    the per-restart offset #15 set out to remove. The frames are now carried
    into the seeded value, and the sample index inside the audio buffer is
    included in the elapsed time.
  • Hold on pause holds. With HOLD ON PAUSE enabled the encoder kept
    auto-incrementing and the resync rule pulled it back every other frame:
    the signal alternated V / V+1. It now carries the stop value on every
    frame. (Reported behaviour of the option as contributed in #14 was the
    intent; this makes the output match it.)
  • Phase published for Pro DJ Link and StageLinQ. The 1.9.11 notes said
    every source published its sub-frame phase; the two DJ sources did not,
    so on a CDJ or a Denon deck the encoder still seeded at bit 0 of whatever
    buffer came first. Both now publish the position their timecode was
    derived from, TrackMap offset included.

LTC output survives a device hole (issue #19)

Reported and captured by @mungewell: on a display wake, a USB audio
interface left a hole in the LTC (10 ms on one interface, 24.5 ms on
another) and resumed the stream where it left off, so every frame after it
was late by that much for good. The encoder now measures its callback
cadence and, when the device has run dry, re-seeds at the phase the source
publishes, so the frame boundary is back where it belongs after one
glitchy frame (the hole itself). The LTC output status line counts the
gaps, since an interface that keeps doing this is worth knowing about, and
each one is logged with its time, engine, device, size and the device
period to ltc_gaps.log next to settings.json, so a capture on the wire
can be lined up with STC's own view of the event. For chasing such an
interface further, the LTC user bits have a DEBUG: BUFFER COUNTER mode
that puts the low four bits of the audio callback count in every binary
group -- a capture then shows which buffer each frame was packed in --
and the buffer size list goes up to 4096 and 8192 so that a buffer holds
a whole frame or more.

LTC input

  • Current frame is the live one. The decoder delivers a frame when its
    sync word has gone by, i.e. when the next frame has already begun on the
    wire; the engine took the decoded value as current, so every LTC -> X
    conversion was one frame late. The live value is now the frame after the
    decoded one. A source holding a static frame reads one higher than its
    own display.
  • Frame arrival is stamped at sample precision (buffer position and input
    latency) instead of the audio callback time.
  • The frame rate is adopted only after three consecutive frames agree on the
    same classification; one jittery frame period no longer flips it.

MTC input (issue #16)

  • Interpolation between quarter-frame sequences and the continuity guard
    work on the frame index instead of a linear count with a patch for
    addresses 00/01.
  • A malformed Full Frame message is dropped with the same range check the
    quarter-frame path applies.
  • The sync instant is placed where the reconstructed value begins (a quarter
    frame after piece 7 arrives), so the phase handed to the LTC encoder is
    not a quarter frame early.

TCNet

  • SMPTE mode follows the engine rate. The time, status and metrics
    packets declared SMPTE 30 at every rate while the per-layer H:M:S:F block
    was counted at the real rate; a console reading that block at 25 fps saw
    frame 24 as 0.80 s instead of 0.96 s. Each layer now carries its own mode
    byte (24 / 25 / 29 for 29.97 DF / 30; 23.976 sent as 24), the node-wide
    byte follows the first active layer, and the block uses drop-frame
    numbering at 29.97. Resolume, which reads the millisecond field, is
    unaffected.
  • Layer speed follows the deck's pitch (2^20 = 100 %); it was fixed at
    100 % whenever the layer played.

MIDI clock

The clock timer accumulates elapsed time instead of counting callbacks, so
the tempo no longer depends on the timer's rate accuracy and missed
callbacks are no longer lost pulses.

Audio devices

One shared audio device per interface

Until now each audio component -- LTC in, LTC out, Audio Thru, Audio BPM
and every engine's audio playback -- opened the interface for itself. With
an ASIO interface, which has one sample rate and one buffer for the whole
device, that meant several clients each believing their own settings, and
whether it worked at all depended on the driver tolerating several clients.
Every physical interface is now opened once and shared: all its clients see
the same sample rate and buffer, a rate or buffer change restarts the
device once for everybody, and several outputs on one interface (an LTC per
engine on different channels, the generator's music, Audio Thru) are mixed
by STC itself, each on its own channels. The LTC IN and LTC OUT status
lines show the rate and buffer the driver actually settled on
("48k/512"), so a driver that keeps its own values -- ASIO interfaces set
them in their control panel -- is visible at once. Audio Thru on the same
interface as LTC OUT is allowed now; only the same output channel is a
conflict. The generator's audio playback no longer has its own per-engine
sample rate and buffer size: it follows the global setting like everything
else (on a shared interface it is the same device, and a separate setting
could only disagree with what was running). Files at another sample rate
are resampled on the fly, as before.

Generator, cues and triggers

Generator

  • Timecode follows the audio. While a loaded file plays, the generator
    timecode is disciplined to the audio transport (the clock the audience
    hears) instead of free-running on the system clock; the two differ by
    tens of ppm, i.e. 70-360 ms an hour between the sound and the LTC the
    lights and video chase.
  • Clock mode on the high-resolution counter. Time of day is read for
    its value and advanced with the high-resolution counter (re-anchored on a
    clock adjustment), so the timecode and its published sub-frame phase no
    longer move in the system timer's 15.6 ms steps on Windows.
  • Text fields (start/stop TC, cue points, presets) share one parser and
    formatter with drop-frame numbering, so at 29.97 the value typed, the
    value shown back and the value the engine displays agree. Addresses that
    do not exist in drop-frame (00 or 01 at the start of a non-tenth minute)
    are moved to 02.

Generator cue points

A cue point placed exactly at the generator's START TC (or at loop In) now
fires when you press GO, and on every loop iteration. It never did: the
cue cursor started at the play-from position and only fired cues past it,
so "at the top of this preset, fire X" was silently dead.

Cue points (Pro DJ Link, StageLinQ)

  • Cues fire on the interpolated position rather than the raw packet value
    (up to a packet interval late before: 33 ms on a CDJ-3000, a beat on the
    NXS2 beat-derived fallback).
  • Seek detection uses the position estimator's own verdict. The old rule
    ("more than 500 ms forward is a seek") could not tell a seek from a source
    that reports once per beat and marked cues as fired without firing them:
    below 120 BPM on the NXS2 fallback no TrackMap cue fired at all.

Trigger notes

MIDI trigger notes (cue points and track changes) are held for 100 ms
before the Note Off instead of being sent as zero-length notes.
Consoles that act on the Note On see no difference; receivers that need the
note held now get it, and a flash key mapped to a note sees a press.

Robustness and build

Robustness

  • Configuration files. settings.json, trackmap.json,
    mixermap.json and generator_presets.json keep their previous version
    as .bak on every save (and on restore from a bundle). A file that
    exists but cannot be read is moved aside as .corrupt-<timestamp> and a
    warning is shown once at startup; before, it was indistinguishable from
    a missing file, defaults loaded silently and the next save overwrote it.
  • Network parsing. Lengths read from the wire in the StageLinQ BeatInfo
    and string readers and in the NFS/RPC client are bounded in unsigned
    arithmetic before use; a crafted or corrupt value could previously wrap
    to a negative offset and read outside the buffer. The rekordbox PDB page
    walk is bounded the same way.
  • The generator's frame-phase publication ran for every input source
    (missing braces); output offset setters clamp to +/-30 like the UI does.
  • The Track Map's automatic entries (metadata learned from a deck) are
    written to disk on a background thread instead of inside the 60 Hz
    engine tick.
  • The LTC passthrough no longer risks replaying stale audio for a very long
    time if the LTC input restarts while Audio Thru runs on another
    interface.
  • The fps-conversion invariant for millisecond sources (Pro DJ Link,
    StageLinQ, Winamp: no conversion, current rate = output rate) is enforced
    by the engine, not only hidden by the UI; StageLinQ was missing from the
    rate alignment.
  • The OSC command handler reaches the window through a SafePointer, so a
    packet arriving during shutdown cannot run against a destroyed component.

Build

  • macOS build fixed (shipped in the tree after the 1.9.11 tag): the Winamp
    input's non-Windows stub had no default constructor, and
    GeneratorWaveformView.h used INT_MIN without <climits>. Confirmed
    building on macOS.
  • The trigger MIDI device visibility test is parenthesised explicitly
    (Clang warned about && inside ||; behaviour unchanged).
  • The container syntax gate now runs with -Wall -Wextra and is silent.

Thanks

@mungewell (LTC phase, the SMPTE ST 309 date format, the display-wake
holes, and a sigrok LTC decoder), @robertmcrostie (user bits and the date
format), @pasque (the StageLinQ playhead, and the timeline question that
closed it), @Achandler (hold on pause), @mixtern (the channel report that
started the per-engine overrides) and @trickofdjequip (TCNet metadata and
localhost).