Repository navigation
Feature/linkaudio - #360
Conversation
Ports within a group are sorted by display name, which puts a client's ports in alphabetical rather than signal order. Read the JACK "order" metadata key when present and sort by it first, falling back to the display name when either port in a comparison doesn't provide one.
/rnbo/jack/transport/linksync makes JACK transport follow the Link session's tempo and beat grid, which is distinct from the existing runner<->JACK "sync" node. Track it in the transport slice (state, action, selector) and dispatch updates from the bridge; the UI control follows on the Link page. INIT/UPDATE use an undefined-check rather than || so an explicit false from the server survives a reconnect.
New "Link" page (route /link, nav entry, and a double-click target on the
jack-link-audio graph node) backed by a linkAudio redux slice, actions,
selectors and OSCQuery bridge wiring for /rnbo/jack/link. It hosts the
session-level controls — Link enabled, the device's Link name, and Sync
transport to Link — alongside the audio ones, so they stay reachable whether or
not Link Audio is available on the runner.
Link Audio is two explicit, ordered lists: Sources (incoming) and Sinks
(outgoing). A device starts with none of either and nothing connects on its own.
- Add Source is a menu of the channels currently advertised on the network,
grouped by peer, with already-added channels disabled. A source names one
exact peer/channel; if that peer goes offline the row stays, marked
disconnected, and reconnects by itself when the identity returns. Removing
one takes no confirmation — re-adding is a click.
- Add Sink prompts for the channel name announced to the session, prefilled
with the first unused "Send <n>". Non-empty and collision checks run
client-side, so a bad name is a real error message rather than a silently
reverted field; renaming in place validates the same way.
- Up/down buttons reorder either list, which drives the JACK port order shown
in the graph editor. Buttons rather than drag-and-drop: no new dependency,
and it works on the Move's touch screen.
- Latency (ms) sets the receive buffer, and Sync to Incoming Audio (off by
default) controls whether the local timeline is delayed to align with
incoming audio. Incoming audio is buffered and audible either way.
- Per-source receive health — buffered, jitter, dropouts — plus a device
summary. Dropout counts are cumulative, so rows and the summary both offer
a reset, letting you measure from the moment you changed a setting. A row
distinguishes "connected" from "connected, but receiving no audio" and names
the cause: nothing arriving, or audio from a different Link session that
can't be beat-aligned. Both used to look identical to a healthy source.
Slot records are keyed by the key the runner publishes rather than a positional
index, and list order comes from the server-authoritative `order` params rather
than a local sort. A rename changes the key, so it arrives as one slot node
removed and another added, which the existing subtree re-read path handles.
On touch devices a focused input is nudged toward the middle of the viewport
once the on-screen keyboard has animated in, and the page grows bottom padding
while a field has focus, so the Sinks fields at the bottom of the page can
actually clear the keyboard.
A source row reports "Behind: N ms" from the runner's arrival_offset_ms, and the silent-source hint quotes it: Latency has to exceed that figure for any audio to play, which turns "how much buffer does this need?" into a reading instead of trial and error. The hint also points at the sender when the figure is large. A sender that delays its own transport to monitor incoming audio stamps its outgoing audio with the delayed beat, so its latency and monitoring settings are paid for by whoever receives it — and "raise Latency" on its own would send you looking in the wrong place.
The runner moved per-slot nodes to `{sources,sinks}/list/<key>`, so slot keys no
longer share a namespace with the add/remove/order commands.
Drops the slot-key recognition that existed only to work around that: the
matchers take any `[^/]+` key, and initLinkAudio reads every child of `list`
without filtering. The web ui no longer encodes jack_transport_link's hash
width, so a change to the key format wouldn't reach here.
🦋 Changeset detectedLatest commit: fd8346c The changes in this PR will be included in the next version bump. This PR includes changesets to release 2 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
jack_transport_link no longer publishes a fixed "jack-link-audio" port group. Its groups are now display strings -- "Link: Sinks" for the local sinks, "Link: <peer>" per source peer -- so matching the group string to find the Link Audio node stopped working, and with it the double click through to the Link page. Match on the new .../link/audio/slot property instead. Its presence marks a port as Link Audio (the same presence-of-key idiom rnbo-instance-id already uses), and its value is the slot key, which joins the port back to its LinkAudioSourceRecord/LinkAudioSinkRecord. Nothing reads the slot key yet, but per-peer filtering wants it and it is what makes the marker useful rather than a bare boolean. Promote the result to a third NodeType so the decision lives in one place, GraphPortRecord.nodeType, rather than in the patcher/system ternary inlined at each of the three node-construction sites. Two spots assumed NodeType had exactly two members and tested for System where they meant "not Patcher": removeEditorNodeById would have called unloadPatcherNodeOnRemote with an undefined instanceId for a Link Audio node, and getEditorNodesAndPorts would have looked one up as a patcher instance. Both inverted.
The Link page had grown into two unrelated halves: device-wide settings (Link on/off, transport sync, Link name, latency, sync to incoming) and the per-slot Receive/Send lists. The settings half belongs in Settings, and the slot lists are about to move into the Devices pane, so split them apart now. Extract the pieces both halves share into components/linkAudio/ first -- LinkAudioNameInput and scrollInputIntoView, SlotControls, the source and sink rows, and the add-slot menu and modal -- plus the reorder swap into lib/linkAudio. No behavior change beyond hiding the reorder arrows on a list too short to reorder, where they were inert controls anyway. The new Link tab is a hand-built Tabs.Panel rather than a SettingsTabPanel: Link state lives in state.linkAudio and the transport store, not in app settings or the runner config, so it has no ConfigRecord to render through. It borrows settings.module.css's row classes so it lines up with the generic tabs. Receive health sits outside that row layout -- three stats and a button don't fit itemInputWrap's 250px control column, which clipped the button. Sources and sinks are "Receives" and "Sends" in the UI now, matching jack_transport_link's renamed port group. Display only: the model, actions and OSC paths keep jtl's own vocabulary. The Link page keeps the slot lists until the Devices pane absorbs them, at which point the page and its route go away.
The stats and the Reset button shared one nowrap flex row, so on a phone the button ran off the right edge. Stack them on a narrow screen and put them side by side from the sm breakpoint, matching where .item in settings.module.css changes shape. The three stats also wrap among themselves before the row does, so a very narrow screen breaks the stats apart rather than pushing the button off.
Link Audio slots were only reachable from a standalone /link page that sat outside the Devices pane, even though a Link node is a device in the graph like any other. Fold them in: the device select now lists the patcher instances and then, in a second group, every Link node, and double clicking a Link node in the graph opens its page instead of the old catch-all route. getLinkDevices groups the LinkAudio nodes by walking their ports' link/audio/slot property -- never by parsing "Link: <peer>", which is a display string. Port direction picks the key space: jack_transport_link registers a Send as a JACK input and a Receive as a JACK output, and sink and source slot keys are drawn from separate hashes, so a key alone doesn't identify a slot. A Receive page lists that peer's channels with their dropout counts and reset; a Send page lists the announced channels with an Add Send button. Both allow reordering once there are two or more slots. Reordering needs care. jack_transport_link keeps one order list for every source, but a Receive device shows a single peer's slots, so a move has to permute that peer's keys among the positions they already occupy in the global list -- reordering the subset and concatenating would reshuffle every other peer. That's swappedWithin, which assumes the subset is in global relative order, which is how getLinkDevices builds slotKeys. The /link page, its route and its nav entry are gone; the settings tab took the session controls and this took the slot lists. "Devices" in the nav now falls back to the first Link device when no patcher is loaded, so a graph with only Link nodes still has a reachable Devices pane.
Two additions to the Add Node menu. Patchers are grouped once the list is longer than a configurable threshold (default 10). Grouping buckets by first letter and then merges adjacent buckets while they fit, so an initial with many patchers keeps a category to itself while sparse ones collapse into a range: a list heavy in A names yields "A" and "C-F" rather than an even slice. A single initial holding more than the threshold stays oversized -- there is nothing to split it on. Navigation is a drill-down with a back item, not hover submenus, because this menu is used on the Move's touch screen. The threshold also sizes the categories, so it is one knob rather than two. The Link section lists every advertised channel not already bound to a Receive -- unlike the old Add Source menu these are omitted rather than disabled, since this menu is a list of things you can add -- plus an Add Send item. The section is hidden entirely when jack_transport_link isn't running. Supporting changes: AppSettingType gains Numeric, and min/max now reach SettingsItem from an app setting, not just from a runner config. The Add Send modal lives on the page rather than in the menu, since Mantine unmounts a dropdown's children when it closes. Also fixes settings persistence: loadSettingsState fell back to the default whenever a stored value was falsy, so a boolean setting toggled off reverted to on after a reload, and a numeric one could never be stored as 0.
A Link node had no way to be removed: it rendered with the plain system node component, deletable was false, and removeEditorNodeById bailed on anything that wasn't a patcher. Link nodes now render with their own component, modelled on the patcher node but without rename -- the name is the JACK port group, which jack_transport_link derives from the peer, so there is nothing for the user to rename. Its menu opens the node's device page or deletes it. The node is also deletable through reactflow now, so the backspace hotkey reaches the same path. Deleting fans out: jack_transport_link has no notion of removing a device, only a channel, so a node is torn down by removing every slot it stands for -- each Send by its announced name, each Receive by its slot key. They batch on the far side, since each mutates jtl's desired list and the reconcile that follows sees them all. A confirm dialog gates it, matching device unload, and it names how many channels are going and whether they were being announced or received. Afterwards the node's position is dropped from the set meta, and if the user is looking at that device's page they're routed back to the graph rather than left on a page whose device no longer exists.
The Link device page could add and reorder channels but not get rid of the device, so tearing one down meant going back to the graph. Give the page the same actions menu the node has, in the same spot the device page keeps its menu. It reuses removeLinkDeviceOnRemote, so a delete from here gets the same confirm and the same per-slot fan-out, and the route back to the graph that thunk already does now matters -- this is the page whose device is about to stop existing. The Add Send button moves inside the shared action group so a Send device shows both controls side by side.
The Link section ran straight on from the patcher list, and the only rule in the menu sat directly above "Add Send", which made that entry read as a footer of the whole menu rather than as part of Link. The peer names nested inside the section also used plain Menu.Label, so "Live" looked like a heading of the same rank as "Link" itself. Give every section but the first a rule above it, make the section headings (Patchers, Link, a drilled-into patcher group) uppercase and bold so they outrank anything nested under them, and indent each peer's channels. "Add Send" now sits inside the Link block with the receives, with no divider of its own. The empty-list placeholder becomes plain dimmed text instead of a disabled Menu.Item, so it no longer carries the row affordances of something clickable.
The max-height was set on an unstyled wrapper div with no overflow, and every section inherited that same max-height, so the Patchers and Link sections together could be twice the height of the menu and simply spilled out past the bottom of the dropdown -- on a phone the "Add Send" row drew outside the panel. Make the dropdown body a single scroll region instead of giving each section its own nested scroll area, close the unbalanced calc() in the height, and measure against visualViewport, re-measuring while the menu is open so the mobile browser chrome collapsing or a rotation doesn't leave a stale height.
The peers listed their channels inline, so the top level of the menu grew with every channel every peer advertised. Give the Link section the same one-level drill-down the patcher categories use: a row per peer with its available-channel count, then that peer's channels behind it. Both sections share a single drill slot, so whatever the user opened is the only thing on screen -- drilling into a peer hides the Patchers section just as drilling into a category already hid Link. "Add Send" stays at the top level; it isn't peer specific.
Double clicking the graph record sink opened a Link device page showing the Send
device's details, because the record ports carry a Link Audio slot key that
isn't theirs:
rnbo-record:in1 -> { port-group: "rnbo-graph-record-sink",
pretty-name: "Record In 1",
link/audio/slot: "372cb3d7e804", order: 1 }
JackAudioRecord only sets pretty-name and port-group on those ports. The slot
key and order are jack_transport_link leftovers: JACK keys metadata by port
UUID, keeps it after the port is unregistered and recycles the UUIDs, and jtl
unregisters its ports without removing their properties -- so a deleted Send's
metadata reattaches to whichever port claims that UUID next. The slot key is
derived from the send identity, so recreating the Send produced the same key
that's now on both, and the record sink rendered as a duplicate of Link: Sends.
Slot-key presence was the only test for a Link Audio port, and any client's
leftovers satisfy it. Ignore the key on the port groups the runner sets on its
own ports, which are never Link Audio whatever their metadata says.
The root cause still stands: jtl should remove its properties before it
unregisters a port.
The editor listed each node's ports in Immutable.Map iteration order, which is hash order rather than insertion order once the map outgrows a handful of entries -- so the order had nothing to do with anything the user or the port's owner had chosen. On a Link node with four channels that put Main L first and Main R last, split the stereo pairs, dropped a newly added channel into the middle, and rearranged again on reload. GraphPortRecord.order existed for exactly this and had no readers. Sort by that JACK order metadata, which jack_transport_link and the runner both set, and fall back to the port id with numeric collation so a node whose ports carry no order (the system groups, the record sink) is at least listed the same way every time.
Two changes to how a peer's channels are offered. In the Add Node menu, a channel already in the graph keeps its row, disabled and check-marked, instead of being filtered out. Hiding them meant the list changed shape as you added to it, and gave no way to tell "already have this" apart from "the peer stopped advertising it". A peer whose channels are all taken now stays listed too, so the count changes meaning: it reads "(2 of 5)" for what is still addable, or "(all added)". On a Receive device's page, Add Receive now sits where a Send device has Add Send, offering the rest of what that peer advertises -- previously the page sent you back to the graph editor for it. It reuses AddSourceMenu, which was already in the tree with no callers, now marking taken channels the same way the node menu does and labelling peers only when it is given more than one. The device's peer comes from one of its slots rather than from parsing the node id, which is a display string.
…ts of the UI to trigger delete and renaming actions, also reuse the async dialogues here rather than deleting directly as well as when asking for naming / renaming input
fde31
left a comment
There was a problem hiding this comment.
Minor note on the comments. Noticed references to a made up touchscreen on the move.
I also have some changes that consolidate the logic to use the already existing dialogues and confirmation patters. The comments says that deleting a Link channel can easily be restored by adding it again but in reality you also lose all the connections to the ports. So I'd say it's not that easy to revert in practice plus the UX sort of agrees that all desctructive operations are gated by confirm dialoges.
Will put these changes on a PR on top of this one for you to review @x37v
Otherwise this seems good for me at this stage. I had a moment of thinking that rather than squeezing everything into the dropdown we could use the slide in overlay and provide a more tree like "browser" experience that also supports searching across the patchers / peers. But guess at this stage that can just be a FR / improvement down the line and shouldn't get in the way of landing the feature.
Minor changes to Link Audio Editing and Desctruction patters
enabling link audio control via jack transport link thru the runner.
small additional thing, using jack ORDER metadata to sort ports for node