Skip to content

Feature: Remote participant volume control#47

Open
christofmuc wants to merge 23 commits intomasterfrom
feature/remote-participant-volume-control
Open

Feature: Remote participant volume control#47
christofmuc wants to merge 23 commits intomasterfrom
feature/remote-participant-volume-control

Conversation

@christofmuc
Copy link
Copy Markdown
Owner

@christofmuc christofmuc commented Feb 14, 2026

Summary by CodeRabbit

  • New Features

    • Remote participant volume control per channel and session-aware group updates.
    • Slider-drag detection with throttled sends, hold/snapshot to prevent wobble, and programmatic session channel volume setting.
    • Optional diagnostic remote-control logging (env-controlled log files).
  • Bug Fixes / Reliability

    • Message deduplication and ordering for remote-volume commands; routing-stability guards to avoid drift; session-rate throttling.
  • Tests / Docs

    • End-to-end test script, analyzer tool, and detailed remote-volume documentation and handover notes.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Feb 14, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds an end-to-end remote participant volume control feature: client UI drag handling and throttling, client-side SetRemoteVolume JSON sending with sequencing and debug logging, server-side routing/forwarding with identity mapping and deduplication, and client-side ApplyLocalVolume parsing/deduplication and application, plus supporting schema, registry, tooling, and docs.

Changes

Cohort / File(s) Summary
Client Volume API
Client/Source/AudioCallback.h, Client/Source/AudioCallback.cpp, Client/Source/AudioService.h, Client/Source/AudioService.cpp
Add setRemoteParticipantVolume(...), per-command sequence counter, JSON SetRemoteVolume construction, RemoteControlDebugLog calls, and send via jammerService sender.
Channel UI & Grouping
Client/Source/ChannelController.h, Client/Source/ChannelController.cpp, Client/Source/ChannelControllerGroup.h, Client/Source/ChannelControllerGroup.cpp
Track slider-drag lifecycle, expose update handler, identity-based gating for session sliders, APIs to query/set session volumes, and session identity bookkeeping.
Client Receive & Deduplication
Client/Source/DataReceiveThread.h, Client/Source/DataReceiveThread.cpp
Process GENERIC_JSON control messages, parse ApplyLocalVolume payload, per-(source,channel) sequence dedupe, routing cache, and session/client-change clearing helpers.
MainComponent UI Logic
Client/Source/MainComponent.h, Client/Source/MainComponent.cpp
Throttle/aggregate remote sends (50ms/1%/300ms), hold-and-snapshot during drag, incremental session-layout updates, and prune remote-volume command state.
Server Accept & Forwarding
Server/Source/AcceptThread.h, Server/Source/AcceptThread.cpp
Parse SetRemoteVolume, per-(src,target,channel) dedupe, resolve endpoints via ClientIdentityRegistry, forward ApplyLocalVolume with sendControlMessageToClient, and schedule session-revision bumps.
Server Threads & Session Revision
Server/Source/MixerThread.h, Server/Source/MixerThread.cpp, Server/Source/SendThread.h, Server/Source/SendThread.cpp, Server/Source/Main.cpp
Wire ClientIdentityRegistry into threads, annotate session channels with sourceClientId, add sessionControlRevision sharing and session-info send logic, and adjust overflow handling (drop-and-count).
Client Identity Registry
Server/Source/SharedServerTypes.h
Add thread-safe ClientIdentityRegistry mapping endpoint ⇄ clientId with getOrAssignClientId and endpointForClientId.
Serialization Schema & Package
common/JammerNetzChannelSetup.fbs, common/JammerNetzPackage.h, common/JammerNetzPackage.cpp
Add source_client_id and source_channel_index fields; propagate through (de)serialization and equality checks; update CreateJammerNetzPNPChannelSetup signature.
Debug Logging
common/RemoteControlDebugLog.h, common/RemoteControlDebugLog.cpp, common/CMakeLists.txt
Add env-controlled, thread-safe RemoteControlDebugLog::logEvent and include it in JammerCommon build.
Tests, Tools & Docs
common/JammerCommonTests.cpp, run_test.sh, README.md, docs/remote-volume-control-handover.md, log_analyze.py, issues/*
Add test assertions for new channel fields, run_test harness, README section for remote logs, handover doc, log analyzer tool, and numerous issue/debt/test docs tracking follow-ups and known risks.
Misc client args
Client/Source/Main.cpp
Add optional --auto-start-audio flag to auto-enable audio on startup.
Build & Packaging
common/CMakeLists.txt
Add RemoteControlDebugLog sources to JammerCommon build.
Repository housekeeping
.gitignore
Ignore __pycache__/, *.pyc, and logs/.

Sequence Diagram(s)

sequenceDiagram
    participant UI as MainComponent (Client A)
    participant AS as AudioService (Client A)
    participant Server as JammerNetzServer
    participant Accept as AcceptThread (Server)
    participant Forward as SendThread/Mixer (Server)
    participant Rx as DataReceiveThread (Client B)
    participant Audio as AudioCallback (Client B)
    participant Log as RemoteControlDebugLog

    UI->>AS: setRemoteParticipantVolume(targetClientId, targetChannelIndex, percent)
    AS->>AS: clamp percent, bump sequence
    AS->>Server: send JSON SetRemoteVolume {target, channel, volume, sequence}
    Server->>Accept: receive control message with source endpoint
    Accept->>Accept: parse & validate payload
    Accept->>Accept: deduplicate by (source, target, channel, sequence)
    alt forwardable
        Accept->>Forward: resolve target endpoint via ClientIdentityRegistry
        Forward->>Server: sendControlMessageToClient(ApplyLocalVolume JSON)
        Server->>Rx: deliver ApplyLocalVolume JSON
        Rx->>Rx: parse payload, check routing cache and sequence
        alt new sequence & routed
            Rx->>Audio: apply volume on UI/audio thread (async)
            Rx->>Log: logEvent("apply", details)
        else stale/dup or unmapped
            Rx->>Log: logEvent("drop", details)
        end
    else invalid/stale
        Accept->>Log: logEvent("drop", details)
    end
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Poem

🐰 I hop and tap a tiny key,
A slider hums across the net.
Sequences travel, routes align,
Throttled whispers, logs that shine.
A gentle change — the mix feels right.

🚥 Pre-merge checks | ✅ 2 | ❌ 2
❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Merge Conflict Detection ⚠️ Warning ❌ Merge conflicts detected (28 files):

⚔️ .gitignore (content)
⚔️ Client/Source/AudioCallback.cpp (content)
⚔️ Client/Source/AudioCallback.h (content)
⚔️ Client/Source/AudioService.cpp (content)
⚔️ Client/Source/AudioService.h (content)
⚔️ Client/Source/ChannelController.cpp (content)
⚔️ Client/Source/ChannelController.h (content)
⚔️ Client/Source/ChannelControllerGroup.cpp (content)
⚔️ Client/Source/ChannelControllerGroup.h (content)
⚔️ Client/Source/DataReceiveThread.cpp (content)
⚔️ Client/Source/DataReceiveThread.h (content)
⚔️ Client/Source/Main.cpp (content)
⚔️ Client/Source/MainComponent.cpp (content)
⚔️ Client/Source/MainComponent.h (content)
⚔️ README.md (content)
⚔️ Server/Source/AcceptThread.cpp (content)
⚔️ Server/Source/AcceptThread.h (content)
⚔️ Server/Source/Main.cpp (content)
⚔️ Server/Source/MixerThread.cpp (content)
⚔️ Server/Source/MixerThread.h (content)
⚔️ Server/Source/SendThread.cpp (content)
⚔️ Server/Source/SendThread.h (content)
⚔️ Server/Source/SharedServerTypes.h (content)
⚔️ common/CMakeLists.txt (content)
⚔️ common/JammerCommonTests.cpp (content)
⚔️ common/JammerNetzChannelSetup.fbs (content)
⚔️ common/JammerNetzPackage.cpp (content)
⚔️ common/JammerNetzPackage.h (content)

These conflicts must be resolved before merging into master.
Resolve conflicts locally and push changes to this branch.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Feature: Remote participant volume control' clearly summarizes the main change—adding remote volume control capability across client and server. It is concise, descriptive, and directly reflects the primary feature added throughout the changeset.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/remote-participant-volume-control

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@christofmuc christofmuc marked this pull request as ready for review February 14, 2026 22:18
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
Server/Source/AcceptThread.cpp (1)

66-88: ⚠️ Potential issue | 🔴 Critical

Constructor member initializer order does not match declaration order in the header.

The initializer list order in AcceptThread.cpp (lines 66-88) places bufferConfig_ immediately after sessionControlRevision_, but in AcceptThread.h (line 48), bufferConfig_ is declared after readbuffer (line 46) and qualityTimer_ (line 47). This mismatch will trigger -Wreorder compiler warnings. Reorder the initializer list to match the member declaration order in the header.

🤖 Fix all issues with AI agents
In `@Client/Source/ChannelControllerGroup.cpp`:
- Around line 111-119: In
ChannelControllerGroup::isSessionVolumeSliderBeingDragged, remove the
unconditional jassertfalse that fires when findSessionChannelIndex returns
nullopt or an out-of-range index; instead perform a simple early return false
for that transient/missing case (or, if you want a debug-only check, replace
jassertfalse with jassert containing a descriptive condition like
channelIndex.has_value() && *channelIndex >= 0 && *channelIndex <
channelControllers_.size()). This keeps the existing return false fallback while
avoiding spurious halts during session transitions; update the block that checks
channelIndex (the call to findSessionChannelIndex and the bounds check against
channelControllers_.size()) accordingly.

In `@Client/Source/DataReceiveThread.cpp`:
- Around line 110-146: The lambda passed to MessageManager::callAsync re-reads
Data at async execution time using targetChannelIndex (physical index) which can
become stale; instead compute the logical controller index synchronously before
calling MessageManager::callAsync and capture that computed index in the lambda.
Concretely: perform the loop over channels (using Data::instance().get(),
inputSetup.getChildWithName(VALUE_INPUT_SETUP),
channels.getChildWithName(VALUE_CHANNELS), checking VALUE_CHANNEL_ACTIVE)
outside the MessageManager::callAsync call to derive activeControllerIndex (or
null/optional), verify it is valid, then call MessageManager::callAsync
capturing activeControllerIndex and remoteVolumePercent and inside the lambda
directly update mixer.getOrCreateChildWithName("Input" +
String(*activeControllerIndex)).setProperty(VALUE_VOLUME, remoteVolumePercent,
nullptr).

In `@Client/Source/DataReceiveThread.h`:
- Line 47: The map lastAppliedRemoteVolumeSequence_ in DataReceiveThread retains
per-(clientID,volumeID) sequence state and is never cleared, causing new
connections that reset sequence counters to have their commands treated as
stale; add logic to expire or remove entries from
lastAppliedRemoteVolumeSequence_ when a session setup changes or when a client
disconnects/reconnects (e.g., provide a method on DataReceiveThread such as
ClearRemoteVolumeSequenceForClient(clientId) and another
ClearAllRemoteSequencesOnSessionReset()), and call these from the session
setup/change handler and client-disconnect handling code so entries for missing
or reinitialized client IDs are removed before new sequences arrive.

In `@Client/Source/MainComponent.cpp`:
- Around line 369-378: The call to
allChannels_.isSessionVolumeSliderBeingDragged(...) can hit jassertfalse when
thisSetup (new layout) contains a channel identity that no longer exists in
allChannels_ (old layout); before calling isSessionVolumeSliderBeingDragged in
the loop over thisSetup.channels, first verify the identity exists in the
current session (e.g., call findSessionChannelIndex or a new
hasSessionChannel(uint32, uint16) on ChannelControllerGroup) and only call
isSessionVolumeSliderBeingDragged if that check succeeds; to implement this
either make findSessionChannelIndex public or add a lightweight
hasSessionChannel method to ChannelControllerGroup, or alternately change
isSessionVolumeSliderBeingDragged to return false instead of asserting when the
identity is not found, and update the loop using remoteVolumeCommandState_,
thisSetup, allChannels_, and isSessionVolumeSliderBeingDragged accordingly.

In `@run_test.sh`:
- Around line 21-24: The trap registration is happening after the client
processes are launched, risking orphaned PIDs if the script exits early; move
the trap setup so it runs before spawning clients (or at minimum immediately
after launching the first background process). Specifically, register the trap
for cleanup (trap cleanup EXIT INT TERM) prior to creating
client1_pid/client2_pid or right after the first background launch, and ensure
cleanup() safely guards against unset PIDs (check client1_pid/client2_pid before
kill) so kill calls won't error if a PID is unset.
- Around line 16-26: The clients (JammerNetzClient launched into background and
tracked by client1_pid/client2_pid) are started before the server
(JammerNetzServer), causing a race; reorder the script so JammerNetzServer is
started first (and its stdout captured as before), wait for readiness (simple
sleep or better: poll the server port/health endpoint) before launching the two
JammerNetzClient instances, then start the clients in background and set
client1_pid/client2_pid, and keep the existing cleanup() and trap to kill those
pids on EXIT/INT/TERM; ensure any readiness-wait uses the same stamp/log naming
(JN_REMOTE_LOG_NAME) as the clients.

In `@Server/Source/AcceptThread.cpp`:
- Around line 199-205: Throttling can drop the final slider update; modify the
logic around sessionControlRevision_, lastSessionRevisionBumpMillis_ and
kSessionRevisionMinIntervalMs so that the final SetRemoteVolume is always
reflected: detect when commandSequence (or the incoming command ID) differs from
the last-processed sequence and force a revision bump even if within the 60ms
window, or alternatively schedule a short debounce timeout that fires after the
last slider event to call sessionControlRevision_.fetch_add(1) and update
lastSessionRevisionBumpMillis_; ensure the comparison and forced bump live
alongside the existing timed throttle so downstream consumers always see the
final state.
- Around line 156-170: The sequence deduplication currently uses
lastForwardedControlSequence_[sequenceKey] which default-initializes to 0 and
causes a true duplicate check when the first incoming command has sequence 0;
change the logic to use map::find() (or equivalent) to detect whether
sequenceKey is present before comparing: if find(sequenceKey) == end, treat this
as first-seen and accept the command and insert the incoming sequence into
lastForwardedControlSequence_ (instead of relying on the default 0); otherwise
compare *parsedPayload->commandSequence to the found lastSequence and update the
stored value on accept. Apply the same fix in the mirrored code path in
Client/Source/DataReceiveThread.cpp where the corresponding last-seen map is
accessed.
🧹 Nitpick comments (8)
Server/Source/Main.cpp (1)

59-61: Minor: indentation inconsistency.

Lines 59–61 use an extra tab level compared to the surrounding constructor body (e.g., line 63). This appears to be a whitespace issue introduced with the change.

Client/Source/AudioService.cpp (1)

57-63: Indentation is inconsistent with surrounding code.

Lines 57–62 have an extra level of indentation (looks like a tab was added). The lines before and after (e.g., line 56 and line 63) use a single tab level within the for body.

🔧 Fix indentation
-				JammerNetzSingleChannelSetup channel((uint8) target);
-				double volume = controllerData.getProperty(VALUE_VOLUME, 100.0);
-				channel.volume = (float)volume/100.0f;
-				channel.sourceClientId = 0;
-				channel.sourceChannelIndex = static_cast<uint16>(setup->activeChannelIndices[i]);
-				auto username = Data::instance().get().getProperty(VALUE_USER_NAME).toString().toStdString();
+			JammerNetzSingleChannelSetup channel((uint8) target);
+			double volume = controllerData.getProperty(VALUE_VOLUME, 100.0);
+			channel.volume = (float)volume/100.0f;
+			channel.sourceClientId = 0;
+			channel.sourceChannelIndex = static_cast<uint16>(setup->activeChannelIndices[i]);
+			auto username = Data::instance().get().getProperty(VALUE_USER_NAME).toString().toStdString();
common/RemoteControlDebugLog.cpp (1)

39-49: Dual enable/disable env vars are slightly confusing but benign.

JN_REMOTE_LOG_ENABLE must be explicitly set to "1" or "true" (logging is off by default), and then JN_REMOTE_LOG_DISABLE can override it. Since the enable check already guards entry, the disable check on Lines 45-49 is redundant in practice. Consider documenting the intended precedence (e.g., in the header or a comment) so future maintainers understand why both exist, or simply drop the disable variable.

Client/Source/ChannelControllerGroup.h (1)

23-23: Consider naming the handler parameters for documentation purposes.

The SessionVolumeChangedHandler alias uses unnamed parameters (uint32, uint16, float). While valid C++, adding comments or a brief docstring would help callers understand the meaning (client ID, channel index, volume percent).

💡 Suggested improvement
-	using SessionVolumeChangedHandler = std::function<void(uint32, uint16, float)>;
+	// Parameters: targetClientId, targetChannelIndex, volumePercent
+	using SessionVolumeChangedHandler = std::function<void(uint32 targetClientId, uint16 targetChannelIndex, float volumePercent)>;
Server/Source/AcceptThread.h (1)

50-50: Consider adding a comment or type alias for the composite map key.

std::map<std::tuple<uint32, uint32, uint16>, uint64_t> is not self-documenting. A brief comment or a named alias would clarify what each element of the tuple represents (e.g., source client ID, target client ID, target channel index).

💡 Suggested improvement
+	// Key: (sourceClientId, targetClientId, targetChannelIndex) → last forwarded sequence number
 	std::map<std::tuple<uint32, uint32, uint16>, uint64_t> lastForwardedControlSequence_;
Server/Source/MixerThread.cpp (2)

87-93: Indentation is inconsistent with the surrounding code.

Lines 87–93 have an extra level of indentation compared to the enclosing scope (the for loop on line 74 and the code after line 93). This looks like a leftover from a removed if block or a merge artifact. It doesn't affect correctness but hurts readability.


115-125: getOrAssignClientId is called redundantly inside the for_each lambda.

clientIdentityRegistry_.getOrAssignClientId(client.first) is invoked once per multimap entry for the same client key. Since the result is deterministic for a given client.first, hoist it before the for_each to avoid repeated lookups.

Also, the indentation on line 115 (if) has an extra tab relative to the surrounding block — appears to be a formatting artifact.

Proposed fix
 				if (client.first != receiver.first) {
+					auto sourceClientId = clientIdentityRegistry_.getOrAssignClientId(client.first);
 					auto range = allSessionChannels.equal_range(client.first);
-					for_each(range.first, range.second, [&](std::pair<const std::string, JammerNetzChannelSetup> setup) {
-						auto sourceClientId = clientIdentityRegistry_.getOrAssignClientId(client.first);
+					for_each(range.first, range.second, [&, sourceClientId](std::pair<const std::string, JammerNetzChannelSetup> setup) {
 						for (const auto& sourceChannel : setup.second.channels) {
 							auto copiedChannel = sourceChannel;
 							copiedChannel.sourceClientId = sourceClientId;
 							sessionSetup.channels.push_back(copiedChannel);
 						}
 					});
 				}
Server/Source/AcceptThread.cpp (1)

95-134: atoi for port parsing is fragile; consider std::stoi or juce::String::getIntValue.

atoi silently returns 0 on non-numeric input and has undefined behavior on overflow. The port <= 0 guard catches the common failure case, but std::stoi (with exception handling) or JUCE's String(…).getIntValue() would be more robust. Not a blocking issue since the input comes from a controlled clientName format (ip:port), but worth hardening.

Comment thread Client/Source/ChannelControllerGroup.cpp
Comment thread Client/Source/DataReceiveThread.cpp Outdated
Comment thread Client/Source/DataReceiveThread.h
Comment thread Client/Source/MainComponent.cpp
Comment thread run_test.sh Outdated
Comment thread run_test.sh Outdated
Comment thread Server/Source/AcceptThread.cpp
Comment thread Server/Source/AcceptThread.cpp Outdated
@christofmuc
Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Feb 14, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 Fix all issues with AI agents
In `@Client/Source/DataReceiveThread.cpp`:
- Around line 150-171: The code currently uses targetChannelRoutingCache_ when
cachedRouting exists even if activeControllerIndex is empty; change the logic in
the ApplyLocalVolume handling (the block that reads targetChannelRoutingCache_,
resolvedControllerIndex and uses RemoteControlDebugLog::logEvent) to guard
against stale cache entries by checking if activeControllerIndex has_value()
before trusting cachedRouting; if activeControllerIndex is not present,
remove/invalidate the cache entry (targetChannelRoutingCache_.erase or
equivalent), log a warning via RemoteControlDebugLog::logEvent indicating a
stale cache for targetChannel, and return/drop the command instead of applying
the cached controller index.
- Around line 221-261: The topology-change branch in handleSessionSetupChange
currently clears per-client sequences but fails to invalidate the routing cache;
update handleSessionSetupChange so that when topologyChanged is true it also
clears/invalidates targetChannelRoutingCache_ (same behavior as
clearAllRemoteSequencesOnSessionReset), then proceed to call
clearRemoteVolumeSequenceForClient for currentClientIds; reference
handleSessionSetupChange, targetChannelRoutingCache_,
clearAllRemoteSequencesOnSessionReset, and clearRemoteVolumeSequenceForClient
when making the change.

In `@issues/2020-12-21-branch-createmidiclock-triage.debt.md`:
- Line 14: The phrase "MIDI clock related" in the diff for the createMIDIclock
branch should be hyphenated as a compound modifier; update the text to "MIDI
clock-related" (or "MIDI-clock-related") so the line reads "...adds server-time
transport and MIDI clock-related changes..." referencing the createMIDIclock
branch text in the diff.

In `@issues/2024-10-18-branch-rustserver-prototype-disposition.debt.md`:
- Line 14: In the line describing the Rust prototype (the text containing
`rustServer`), change the phrase "non working" to the hyphenated "non-working"
so the sentence reads that `rustServer` contains early prototype work for a Rust
mixing server with explicit "non-working leftover" status in the latest commit;
update only that phrase to correct the typo.

In `@issues/2026-02-15-packetstreamqueue-quality-divide-by-zero.bug.md`:
- Around line 18-21: In qualityStatement(), avoid dividing by zero by checking
packagesPopped before computing droppedPacketCounter / (float)packagesPopped *
100.0f: if packagesPopped == 0 return a sensible default (e.g., 0% or "N/A") or
handle the case explicitly; update the code around the percentage calculation
that uses droppedPacketCounter and packagesPopped so the division only occurs
when packagesPopped > 0 and ensure the function still returns the expected
type/format.
🧹 Nitpick comments (8)
issues/2026-02-15-mixerthread-incoming-map-reset-race.bug.md (2)

19-25: Consider using more maintainable code references.

Hardcoded line numbers will drift as the source files evolve, making it difficult to verify this issue in the future. Consider anchoring references using:

  • Git commit SHAs with line numbers (e.g., commit abc123, lines 87-90)
  • Function/method names (e.g., MixerThread::handleUnderrun())
  • Code snippets or surrounding context that's easier to search for

This will improve long-term maintainability of the issue documentation.


3-11: Consider adding additional tracking metadata.

The current metadata is well-structured, but you might consider adding:

  • Assignee: Who is responsible for addressing this issue
  • Milestone/Target: When this should be resolved
  • Related Issues: Cross-references to related bugs or features
  • Severity: Classification beyond priority (e.g., Critical, High, Medium, Low)

These additions would improve issue tracking and project management, though the current structure is already quite good.

issues/2026-02-15-packetstreamqueue-quality-divide-by-zero.bug.md (2)

30-31: Clarify the preferred fix approach.

The next steps propose two alternatives (n/a or 0.00%) without recommending which is preferred. Consider being more prescriptive: n/a is semantically clearer since it indicates no data is available yet, whereas 0.00% could be misinterpreted as perfect quality (zero drops).

📝 Suggested clarification
 1. guard denominator:
-   - if `packagesPopped == 0`, print `n/a` or `0.00%`
+   - if `packagesPopped == 0`, print `n/a` (preferred: indicates no data vs. zero drops)
 2. add tiny unit test for zero-populated quality summary

34-37: Consider adding regression testing to exit criteria.

The exit criteria are clear and testable. Consider explicitly mentioning that the unit test mentioned in "Next steps" should be included in the exit criteria to prevent regression.

📝 Optional enhancement
 - no divide-by-zero in quality report path
 - stable output for empty/initial stream states
+- unit test coverage for zero-populated quality scenarios
issues/PRIORITY_PLAN.md (1)

131-136: Self-referential PR mention.

Line 136 references "merge or close PR #47" — which is this very PR. If the intent is to track follow-up work after this PR merges, consider rewording to clarify the scope (e.g., "address remaining validation items from PR #47" or link to a separate follow-up issue).

Client/Source/DataReceiveThread.cpp (1)

80-116: Deduplication logic looks correct.

The per-source/per-channel sequence tracking correctly drops stale or duplicate commands. The negative index guard (line 92) before the uint16 cast (line 99) is good.

One minor note: static_cast<uint16>(payload->targetChannelIndex) could silently truncate values above 65535. In practice channel counts are tiny, but a jassert(payload->targetChannelIndex <= std::numeric_limits<uint16>::max()) would make the assumption explicit.

log_analyze.py (2)

39-53: STDOUT_ERROR_PATTERNS contains overly broad patterns that may produce false positives.

The r"error" pattern (line 52) will match any line containing "error" case-insensitively, including benign output like "errors: 0", "error_count=0", or JUCE info messages. Similarly, r"assert" matches "assertion", "asserting", or even benign log contexts.

Consider using word boundaries (\berror\b) or more specific patterns to reduce noise. Alternatively, add an exclusion list for known-benign patterns.

Tighter patterns example
 STDOUT_ERROR_PATTERNS = [
     re.compile(pat, re.IGNORECASE)
     for pat in [
         r"fatal",
         r"overflow",
         r"send queue length overflow",
         r"malloc\(\)",
         r"unaligned tcache",
         r"aborted",
-        r"assert",
+        r"\bassert(?:ion)?\s*(?:failed|error)",
         r"network down",
         r"disconnect",
-        r"error",
+        r"\berror\b",
     ]
 ]

65-87: summarize_sequence could produce a very large missing list for big gaps.

If a sequence jumps from e.g. 1 to 1,000,000, missing.extend(range(...)) materializes ~1M elements. This is unlikely in normal operation but could be triggered by corrupt or misinterpreted log data. Consider capping the gap expansion or only recording the gap range endpoints.

Comment on lines +150 to +171
auto targetChannel = static_cast<uint16>(payload->targetChannelIndex);
std::optional<int> resolvedControllerIndex;
auto cachedRouting = targetChannelRoutingCache_.find(targetChannel);
if (cachedRouting != targetChannelRoutingCache_.end()) {
resolvedControllerIndex = cachedRouting->second;
if (activeControllerIndex.has_value() && *activeControllerIndex != cachedRouting->second) {
RemoteControlDebugLog::logEvent("client.recv",
"routing drift targetChannel=" + String(payload->targetChannelIndex)
+ " computedController=" + String(*activeControllerIndex)
+ " cachedController=" + String(cachedRouting->second)
+ " action=use-cached");
}
}
else {
if (!activeControllerIndex.has_value()) {
RemoteControlDebugLog::logEvent("client.recv",
"drop ApplyLocalVolume inactive targetChannel=" + String(payload->targetChannelIndex));
return;
}
resolvedControllerIndex = *activeControllerIndex;
targetChannelRoutingCache_.emplace(targetChannel, *resolvedControllerIndex);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Cached routing used even when the target channel is no longer active — potential stale volume application.

When cachedRouting exists (line 153) but activeControllerIndex is empty (the target physical channel is no longer active), the code still proceeds with the cached controller index. This could silently apply volume to an unrelated controller or one that no longer maps to the intended channel.

Consider adding a guard: if the channel is confirmed inactive (no activeControllerIndex) and a cached entry exists, either invalidate the cache entry and drop the command, or at least log a warning.

Suggested guard
 	auto cachedRouting = targetChannelRoutingCache_.find(targetChannel);
 	if (cachedRouting != targetChannelRoutingCache_.end()) {
 		resolvedControllerIndex = cachedRouting->second;
+		if (!activeControllerIndex.has_value()) {
+			RemoteControlDebugLog::logEvent("client.recv",
+				"routing stale targetChannel=" + String(payload->targetChannelIndex)
+				+ " cachedController=" + String(cachedRouting->second)
+				+ " action=channel-inactive-drop");
+			targetChannelRoutingCache_.erase(cachedRouting);
+			return;
+		}
 		if (activeControllerIndex.has_value() && *activeControllerIndex != cachedRouting->second) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
auto targetChannel = static_cast<uint16>(payload->targetChannelIndex);
std::optional<int> resolvedControllerIndex;
auto cachedRouting = targetChannelRoutingCache_.find(targetChannel);
if (cachedRouting != targetChannelRoutingCache_.end()) {
resolvedControllerIndex = cachedRouting->second;
if (activeControllerIndex.has_value() && *activeControllerIndex != cachedRouting->second) {
RemoteControlDebugLog::logEvent("client.recv",
"routing drift targetChannel=" + String(payload->targetChannelIndex)
+ " computedController=" + String(*activeControllerIndex)
+ " cachedController=" + String(cachedRouting->second)
+ " action=use-cached");
}
}
else {
if (!activeControllerIndex.has_value()) {
RemoteControlDebugLog::logEvent("client.recv",
"drop ApplyLocalVolume inactive targetChannel=" + String(payload->targetChannelIndex));
return;
}
resolvedControllerIndex = *activeControllerIndex;
targetChannelRoutingCache_.emplace(targetChannel, *resolvedControllerIndex);
}
auto targetChannel = static_cast<uint16>(payload->targetChannelIndex);
std::optional<int> resolvedControllerIndex;
auto cachedRouting = targetChannelRoutingCache_.find(targetChannel);
if (cachedRouting != targetChannelRoutingCache_.end()) {
resolvedControllerIndex = cachedRouting->second;
if (!activeControllerIndex.has_value()) {
RemoteControlDebugLog::logEvent("client.recv",
"routing stale targetChannel=" + String(payload->targetChannelIndex)
" cachedController=" + String(cachedRouting->second)
" action=channel-inactive-drop");
targetChannelRoutingCache_.erase(cachedRouting);
return;
}
if (activeControllerIndex.has_value() && *activeControllerIndex != cachedRouting->second) {
RemoteControlDebugLog::logEvent("client.recv",
"routing drift targetChannel=" + String(payload->targetChannelIndex)
" computedController=" + String(*activeControllerIndex)
" cachedController=" + String(cachedRouting->second)
" action=use-cached");
}
}
else {
if (!activeControllerIndex.has_value()) {
RemoteControlDebugLog::logEvent("client.recv",
"drop ApplyLocalVolume inactive targetChannel=" + String(payload->targetChannelIndex));
return;
}
resolvedControllerIndex = *activeControllerIndex;
targetChannelRoutingCache_.emplace(targetChannel, *resolvedControllerIndex);
}
🤖 Prompt for AI Agents
In `@Client/Source/DataReceiveThread.cpp` around lines 150 - 171, The code
currently uses targetChannelRoutingCache_ when cachedRouting exists even if
activeControllerIndex is empty; change the logic in the ApplyLocalVolume
handling (the block that reads targetChannelRoutingCache_,
resolvedControllerIndex and uses RemoteControlDebugLog::logEvent) to guard
against stale cache entries by checking if activeControllerIndex has_value()
before trusting cachedRouting; if activeControllerIndex is not present,
remove/invalidate the cache entry (targetChannelRoutingCache_.erase or
equivalent), log a warning via RemoteControlDebugLog::logEvent indicating a
stale cache for targetChannel, and return/drop the command instead of applying
the cached controller index.

Comment on lines +221 to +261
void DataReceiveThread::handleSessionSetupChange(const JammerNetzChannelSetup& newSessionSetup)
{
std::set<uint32> previousClientIds;
for (const auto& channel : currentSession_.channels) {
if (channel.sourceClientId != 0) {
previousClientIds.insert(channel.sourceClientId);
}
}

std::set<uint32> currentClientIds;
for (const auto& channel : newSessionSetup.channels) {
if (channel.sourceClientId != 0) {
currentClientIds.insert(channel.sourceClientId);
}
}

for (auto clientId : previousClientIds) {
if (currentClientIds.find(clientId) == currentClientIds.end()) {
clearRemoteVolumeSequenceForClient(clientId);
}
}

bool topologyChanged = currentSession_.channels.size() != newSessionSetup.channels.size();
if (!topologyChanged) {
for (size_t i = 0; i < currentSession_.channels.size(); i++) {
const auto& oldChannel = currentSession_.channels[i];
const auto& newChannel = newSessionSetup.channels[i];
if (oldChannel.sourceClientId != newChannel.sourceClientId
|| oldChannel.sourceChannelIndex != newChannel.sourceChannelIndex) {
topologyChanged = true;
break;
}
}
}

if (topologyChanged) {
for (auto clientId : currentClientIds) {
clearRemoteVolumeSequenceForClient(clientId);
}
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

handleSessionSetupChange does not clear the routing cache on topology change.

When topology changes are detected (line 256), sequences are cleared for all current clients, but targetChannelRoutingCache_ is not invalidated. Topology changes (channel reordering, clients joining/leaving) can make the physical→controller mapping stale. In contrast, clearAllRemoteSequencesOnSessionReset (line 210) does clear the routing cache.

This looks like an oversight — the routing cache should also be cleared when topology changes.

Suggested fix
 	if (topologyChanged) {
 		for (auto clientId : currentClientIds) {
 			clearRemoteVolumeSequenceForClient(clientId);
 		}
+		clearRemoteVolumeChannelRoutingCache();
 	}
🤖 Prompt for AI Agents
In `@Client/Source/DataReceiveThread.cpp` around lines 221 - 261, The
topology-change branch in handleSessionSetupChange currently clears per-client
sequences but fails to invalidate the routing cache; update
handleSessionSetupChange so that when topologyChanged is true it also
clears/invalidates targetChannelRoutingCache_ (same behavior as
clearAllRemoteSequencesOnSessionReset), then proceed to call
clearRemoteVolumeSequenceForClient for currentClientIds; reference
handleSessionSetupChange, targetChannelRoutingCache_,
clearAllRemoteSequencesOnSessionReset, and clearRemoteVolumeSequenceForClient
when making the change.

Comment thread issues/2020-12-21-branch-createmidiclock-triage.debt.md
Comment thread issues/2024-10-18-branch-rustserver-prototype-disposition.debt.md
Comment thread issues/2026-02-15-packetstreamqueue-quality-divide-by-zero.bug.md
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant