Listen together on the LAN#
Source core/rymembra-insieme/src/lib.rs core/rymembra-insieme/src/rete.rs core/rymembra-insieme/src/messaggi.rs core/rymembra-insieme/src/scoperta.rs core/rymembra-insieme/src/ascolto.rs apps/desktop/src-tauri/src/insieme/mod.rs apps/desktop/src-tauri/src/insieme/fasi.rs apps/desktop/src-tauri/src/insieme/filo.rs apps/desktop/src-tauri/src/insieme/voci.rs apps/desktop/src-tauri/src/insieme/ipc.rs apps/desktop/src-tauri/src/insieme/strada.rs apps/desktop/src-tauri/src/riproduzione/insieme.rs apps/desktop/src-tauri/src/riproduzione/ospiti.rs core/rymembra-app/src/insieme.rs apps/desktop/src/insieme.ts apps/desktop/src/parti/Insieme.tsx
Off by default (insieme.attivo). Switched off, there is no socket and no packet. Switched on and outside a session, the PC runs a discovery round every 10 s — and at once when the panel opens — and keeps a control connection to every host it has seen (at most 16), reconnecting to trusted ones with a backoff from 5 to 60 s. The backoff starts over when the person presses «Search», changes the hand-typed addresses, or the host turns up at a new address: a wait of up to a minute, right while someone is trying to connect, was the «nothing happens» of the first test between two real PCs.
How it is used
- Settings › Listen together: turn it on and name the PC (default: the computer name;
insieme.nome, at most 64 characters). The same card lists the hosting PCs it finds, with «Pair» and «Ask to join», and «Open the panel» opens the full panel. - A hosts: «Host a session» from the button in the player bar, the third column or Settings. The first time, a dialog explains the Windows Firewall prompt (see below). If a paired PC is already hosting, A is offered to join that session instead. While hosting, the panel and Settings show «This PC's address: 10.0.0.5:47811» with «Copy»: the address of the default route first (found with a UDP
connectto a documentation address, which sends nothing), virtual adapters last. - First contact: on B the panel lists «A's PC» with «Pair». Both screens show the same code, «48 21»; both users confirm, and from then on the two PCs trust each other.
- Joining: A presses «Invite» on a connected paired PC and B gets a banner with Accept/Decline and a 60 s countdown (the window is brought forward from the tray); or B presses «Ask to join» and A admits. Either way A picks, next to the button, what B will be able to do — Listen, Control or Everything, see «Who can do what» below.
- In the session a strip at the top of the page says «Listening with A · 3 people», with Leave (End, for the host); the panel adds who is there, invitations and calibration.
- Leaving gives everyone their own queue back, exactly as it was.
Roles and topology
The host is the only source of truth for the queue, the current voice, pause and position, and its audible output is the reference clock. Only the host listens: TCP and UDP on the same port, PORTA = 47811, then the next nine, then any port (the discovery answer says which). Members only connect outwards — they find hosts with a directed UDP broadcast on every IPv4 subnet of every adapter (if-addrs, loopback and 169.254 excluded) plus 255.255.255.255, sent to all ten candidate ports, plus a unicast query to each address typed by hand, and receive unicast answers — so the firewall prompt appears on one PC only and a member is never «unreachable». Every connection carries a heartbeat every 5 s, and 15 s of silence closes it. If the host goes away (connection lost, or that silence) the session ends for everyone: there is no hand-over. At most MASSIMO_PERSONE = 9 people, the host included (insieme.sessionePiena beyond), and 1,000 voices in the session queue.
Each PC decodes the audio itself. A member reads the host's file through a LAN source wrapped in FlussoHttp::da that declares remoto() = false, so seeks inside VBR files stay accurate (Seekable HTTP streams); catalog tracks are opened by every PC from its own catalog and never relayed (TERMS.md § 2).
Handles and session rules
In the player, an id above zero is a library row of this PC and an id below zero is a session voice; riproduzione/ospiti.rs::chi is the only place that tells them apart. The host assigns voice uids from 1; each PC maps them to its own negative handles, and the uid is also the engine label of the anchors. Library gestures — like, rating, playlists, show in folder, go to album or artist, lyrics — are hidden on negative ids in the window (idDiLibreria in insieme.ts) and refused by the core.
Session rules are overrides in memory, never written to settings: crossfade 0; no autoplay or radio; no shuffle; the sleep timer is cancelled on entry and refused during; the saved queue (scrivi_coda) is suspended, so the queue on disk stays the user's; positions are never saved for handles. If the app crashes mid-session, settings and saved queue are intact. Only the owner of a row counts the listen and scrobbles it.
Every transport and queue command passes insieme::instrada first, without the player lock — the media keys, the taskbar thumbnail and the tray call the same functions, so they pass too. strada::regola is exhaustive: on the host, tracks entering the queue become voices (LocaleConVoci); on a member transport and queue are forwarded to the host (phase 2) and everything that would change the music for the others without them seeing it is refused (insieme.gestoNonDisponibile). Volume, EQ, output and latency are always local.
| Phase | What it adds |
|---|---|
| 1 | The host leads and the members follow, with the audio streamed from the host. A member whose level doesn't cover a gesture has its button greyed out (diritti in StatoRiproduzione.insieme), and the session strip says why. |
| 2 | Jam, within the level the host gave (Diritti). A member's gestures travel to the host as Comando/Esito, rate-limited per member, and run through the host's own code paths — translated from queue positions to voice uids, so two people moving the same queue never hit the wrong row; for pause and seek the last one wins. A member's own tracks are served over a service connection the member opens towards the host, which relays the bytes to the others: the member still opens no port. Shuffle, clearing the queue and the queue «Undo» stay refused for members. The host can «Remove» a member, whose voices leave the queue; every queue row that belongs to another PC says whose it is («Marco's», from SessioneInsieme.proprietari). |
| 3 | Local copy: if the receiver has an identical file it plays that instead of the stream, and the strip says «from a local file». The quick fingerprint is blake3(size as u64 little-endian ‖ first 64 KiB ‖ last 64 KiB). A copy is certain with the same UNC path (only \\… shares travel), size and date; it is verified with the same content key, size and duration within 500 ms, and a matching fingerprint read from the local disk. Image-CD voices accept only a certain copy. A copy found counts as a listen of the receiver's own row. |
Who can do what
The host chooses, every time someone joins, one of three levels (Diritti in messaggi.rs), and nothing is remembered per PC: the next time starts again from the preselected Control.
| Level | What the member can do |
|---|---|
Listen (ascolta) | Nothing that changes the music for the others. |
Control (comanda) | Pause and resume, next, previous, seeking, repeat. |
Everything (tutto) | Also the queue: play a voice of the queue, remove and reorder voices, and add tracks of their own (play, play next, enqueue). |
Volume, equalizer, output and latency are each person's own at every level, and shuffle, radio, autoplay, the sleep timer, clearing the queue and «Undo» stay refused for every member. The choice sits next to «Let in» on a join request (in the strip above the page and in the panel, one choice for both), next to «Invite» before the invitation leaves — the level travels with it and applies when the answer arrives — and next to «Remove» in «In the session», where the host changes it at any time. Arrow keys move through the three tabs and the change is sent once they settle (ATTESA_CONCEDI, 400 ms), so passing from Listen to Everything does not grant Control on the way; the member gets one notice (together.notice.rights*).
The host is the door. fasi.rs checks every Comando against the member's level as it arrives, before the rate limiter and before anything runs — an Aggiungi refused there registers no voice and relays no token — and answers insieme.nonConcesso, whatever version sent it. A member does not even send what isn't its own: the level rides in the role atomic (Ruolo::Membro(Diritti)), strada::regola returns NonConcesso, and media keys, the taskbar and the tray log it instead of asking. The window greys out transport and queue (gestoPermesso) and, in the selection bar, Play, Play next and Enqueue; the library menus leave out «Play next» and «Enqueue». Azione::ambito (host) and strada::ambito (member) split gestures the same way, and a test keeps them in step. Gestures already queued on the host when the level drops still run: they passed the door before the change.
The level travels in Ammesso and in each Persona of the session state, with #[serde(default)]: a host that predates the field reads as Everything, as before; an unknown level reads as Listen, the strictest.
Port and firewall
On a network Windows classifies as Public, incoming traffic is blocked and Windows asks the first time a program listens. Before the first «Host» the window explains it (insieme.firewall_spiegato): choose «Allow» and tick «Public networks» too, or set the network to Private first. «Cancel» in that prompt creates permanent block rules, which are removed in Windows Security › Firewall & network protection › Allow an app through firewall. A host behind a silent firewall is indistinguishable from a host that is off, so a member that sees nobody gets a list of causes to check instead of a guess.
Where broadcast does not get through — guest Wi-Fi, VPNs, isolated access points — up to 16 addresses can be typed by hand (insieme.indirizzi, IPv4 ip:port, port 47811 when omitted); discovery also sends its query to each of them by unicast.
Can't see the other PC
- Both PCs run Rymembra 3.1 or later, with «Listen together» on, and the other one is hosting: only a hosting PC answers discovery.
- Same network: a guest Wi-Fi, a VPN or an access point with client isolation keeps broadcasts apart. Type the address the host shows («This PC's address») under Settings › Listen together › Addresses typed by hand.
- On the hosting PC the firewall lets Rymembra through, for «Public networks» too if Windows marks the network that way — Windows Security › Firewall & network protection › Allow an app through firewall.
- An unpaired connection lasts two minutes (
insieme.estraneoScaduto); a pairing in progress keeps it alive, and the PC reconnects by itself as soon as discovery sees the host again. A host that is already pairing another PC answers «busy» (insieme.associazioneOccupata) without counting it as a failed attempt.
If a command is slow
The log (diario) says where the time goes, in milliseconds and codes only: «gesture «pause» answered by the host in N ms» on the member when an answer takes more than 300 ms or is a refusal; «gesture … of member N slow: X ms queued, Y ms to do it» on the host; «the session thread stayed busy N ms (…)» and «network mail waited N ms» when the session thread falls behind (at most one line every 5 s, with the count of the ones left unsaid).
What made gestures slow or lost in the first test between two PCs, and what changed: the host rebuilt and resent the whole session state — the queue window, up to a thousand voices — up to ten times a second while it was still describing the voices of a new queue, and a member's pause waited seconds behind it. A state that changes only because more voices are described (or a copy descriptor arrived) now goes at most once a second, the voice that plays is described before the others, a state over the frame limit is trimmed in linear time, and messages above a few kilobytes are compressed on the wire. A gesture that names a voice that no longer plays (two people pressing «next» together) is answered insieme.superato instead of a silent «done».
The window
One store (creaArchivio) in parti/Insieme.tsx, fed by insieme:stato — a full snapshot at every change, including whose each foreign voice is (proprietari) and whether the calibration clicks are playing (calibrazione) — and insieme:avviso, which carries an i18n key (together.notice.* or errors.insieme.*) and at most a PC name: no free text from the core or from another PC reaches the screen. Even the calibration voice's title is written by the window, in the language of whoever is looking (InsiemeNelLettore.calibrazione). Command answers are adopted only if no event arrived meanwhile, so a late answer never puts the past back on screen. Errors and notices that arrive while the panel is open are drawn inside it — behind its veil they were invisible — and the session queue rereads its rows when InsiemeNelLettore.revisione changes, so covers that arrive later appear (session cover keys are served with no-store: voice numbers restart every session). The settings keys and the insieme_pari table (migration 035) belong to this machine and stay out of profiles, Drive backups and device sync.
See also: Encryption, pairing and what travels in clear, Synchronisation and calibration, Output devices, stream sleep and latency, Seekable HTTP streams, Device sync