StageLinQ: catalogue-heartbeat handling
A community contributor with SC5000 + X1800 hardware shared a Wireshark capture and a Python reference implementation that they had been working on independently. Cross-checking that material against STC produced two findings worth shipping.
The first is purely cosmetic. SC5000 firmware revisions in the field periodically re-broadcast their available StateMap path catalogue using opcode 0x07D2 -- the same opcode STC sends in the other direction to subscribe -- with no JSON value, just the path string and a 4-byte minimum-interval field. Until now, debug builds logged each of these as StageLinQ: Unknown smaa subtype 0x7d2, once per path every few seconds, which made debug captures noisier than they needed to be. STC now recognises the device-direction 0x07D2 as a periodic catalogue announcement and silently consumes it, the same way it has always silently consumed the 0x07D1 subscription acknowledgements. There is no behavioural change in release builds; this only affects log output.
The second finding is the BeatInfo handshake itself: the eight-byte payload STC sends to start a BeatInfo stream is byte-identical to what the contributor's Python implementation sends, including the explicit absence of a token. The contributor reports having confirmed via packet capture that adding a payload to that frame (e.g. an attempt to authenticate the BeatInfo connection by appending the StageLinQ token) is enough to make the device silently refuse to stream. This is now documented in a comment block above buildBeatInfoStart() so the property is preserved if the function is ever revisited.
StageLinQ: documented BeatInfo silence conditions
Independent of the protocol bytes, the same contributor surfaced four hardware-side conditions that can silence the BeatInfo stream from a Denon player even when everything on the network and protocol layers is correct:
- The deck has no track loaded, or is paused / not playing.
- The physical LINK button on the player is on.
- Decks are linked from Engine DJ (same effect as the physical LINK button).
- The player was already running when the consumer started listening; some firmware revisions only emit BeatInfo if a consumer was listening at the time the player booted, and a power-cycle of the player restores the stream.
These have been added in two places. A comment block above buildBeatInfoStart() in StageLinQInput.h lists them so future maintainers see them at the same spot they would land if debugging "BeatInfo connects but no packets ever arrive". A new troubleshooting entry under "Known Issues & Platform Notes" in the README ("StageLinQ: Playhead Does Not Advance") walks an end user through the same four checks in the order they should try them.
These are not fixes -- they are recorded prerequisites that the protocol does not advertise. The motivation is that the failure mode (StageLinQ view shows BPM and track name, but the playhead stays at zero) was not previously documented anywhere, and the four conditions had been collected only across community testing.
Generator: stop SMPTE at end of file, and resume audio with one click after a natural end
Two related bugs in the Generator's audio playback transport, reported together. Both stem from the fact that the generator's wall-clock SMPTE tick and the JUCE audio transport that drives the loaded file were keeping their state independently, with no path for one to learn that the other had reached a terminal condition.
The first bug: when a loaded audio file played to its end, the audio transport auto-stopped at EOF (which JUCE does internally), but the generator tick had no end-of-file check, so genCurrentMs kept advancing every tick and the SMPTE clock kept running past the end of the file as if nothing had happened. The only existing auto-stop was the user-defined Stop TC, which most users do not set when they are using the player as a transport. The tick now reads the loaded file's length and, if not looping and no Stop TC has fired first, clamps genCurrentMs to the end-of-file mark and stops the generator. A user-defined Stop TC still takes precedence, so the previous behaviour for that case is unchanged.
The second bug: after the audio transport had auto-stopped at EOF, clicking earlier on the waveform timeline moved the SMPTE position correctly but no audio resumed -- the user had to press Stop and then Play to get sound back, which restarted from the beginning and lost the position they had clicked. The intended behaviour, modelled after consumer media players, is that a click on the timeline after a natural end is interpreted as "play from here", in a single action.
The engine now distinguishes a natural end (the audio file ran out) from a user-initiated stop. When the EOF auto-stop fires, an internal genEndedAtEof marker is set in addition to transitioning to Stopped. The next call to setGeneratorPosition (which is what the waveform click handler calls) consumes that marker: if it is set, the seek transitions the engine straight to Playing and re-arms the audio player so the transport restarts at the seeked position. If the marker is not set (cold start, manual Stop, post-Pause scrub), the seek behaves as it always did and transitions to Paused, requiring a separate Play to start audio. The marker is cleared by every other explicit user transition -- Play, Stop, file change, input source change -- so it never survives a context switch and never causes spurious auto-play.
A second, smaller change in seekSeconds() itself adds defence in depth for the case where the audio transport has auto-stopped at EOF but the generator tick has not yet detected it (e.g. the tick is briefly delayed): if the player intent is "should be playing" (shouldPlay set, not user-paused) and the device is open, the seek now also re-engages the transport. start() is a no-op if the transport is already playing, so the in-flight seek-while-playing case is unaffected.
Files changed
Modified
StageLinQInput.h--kSmaaSubscribeconstant comment expanded to describe both directions of the opcode; the StateMap parser's "unknown subtype" debug log now also excludes0x07D2from the device side, treating it as a periodic catalogue announcement; a comment block abovebuildBeatInfoStart()records the eight-byte handshake constraint and the four field-reported BeatInfo silence conditions.TimecodeEngine.h-- generator tick now auto-stops the engine when the loaded audio file has played past its end (only when not looping, only when a user-defined Stop TC has not already fired). When the EOF auto-stop fires, an internalgenEndedAtEofmarker is set;setGeneratorPositionconsumes that marker so a click on the waveform timeline immediately after a natural end transitions toPlayingand resumes audio at the seeked position. The marker is cleared by every other explicit user transition (Play / Stop / file change / input source change) so it never survives a context switch.GeneratorAudioPlayer.h--seekSeconds()now re-engages the audio transport after the seek when the caller intent is "playing" (shouldPlayset, not user-paused, device open), so a seek that lands while the transport had auto-stopped at EOF resumes playback rather than leaving the file silent.README.md-- new troubleshooting entry "StageLinQ: Playhead Does Not Advance" under "Known Issues & Platform Notes", listing the four field-reported conditions that can silence the BeatInfo stream from a Denon player. CMake snippet bumped to1.9.9.BACKLOG.md-- removed the "StageLinQ real beat grid and detailed waveform" item, which had shipped in v1.9.8.Main.cpp-- version bump to1.9.9.