Skip to content

Implement per-channel remote processing (Remote: All / Selected… / None + chooser) #46

Description

@huberp

Goal

Extend the current Network UI (currently: Show Local, Show Remote, Broadcast) so remote waveforms can be enabled/disabled per channel (Ch 1–8).

Key behavior requirement: in Selected… mode, only selected channels are processed:

  • non-selected channels are not stored, not displayed, and do not contribute to RMS or cancellation computations.

Selection is by channel index; the chooser should show label + color when available (from remote identity packets). Must respect plugin instance identity:

  • packets are per instanceID, but user selection is by instanceIndex (Ch 1–8)
  • if multiple remote instances share a channel index, and that channel is enabled, they are all allowed through ingestion (unless we later add “pick one instance” behavior).

Current state (for reference)

  • src/PluginEditor.cpp: remoteDisplayToggle (“Show Remote”) currently:
    • calls audioProcessor.setReceiveEnabled(enabled)
    • calls scopeDisplay.setRemoteDisplayEnabled(enabled)
  • timerCallback() fetches remote infos + packets every frame and calls:
    • scopeDisplay.writeRemotePackets(m_remoteDataCache, m_remoteInfosCache);
  • ScopeDisplay draws remotes when m_showRemote is true.

UI changes

1) Replace “Show Remote” with Remote mode selector

Files

  • src/PluginEditor.h
  • src/PluginEditor.cpp
  • src/PluginEditor::resized() layout

UI
In Network group, replace:

  • juce::ToggleButton remoteDisplayToggle; // Show/hide remote waveforms

with:

  • juce::Label remoteModeLabel; text: "Remote:"
  • juce::ComboBox remoteModeCombo; items:
    1. All
    2. Selected…
    3. None
  • juce::TextButton remoteChooseButton; text: "Choose…" (enabled only when mode == Selected…)

Behavior

  • Selecting Selected… auto-opens the chooser (CallOutBox) anchored to either the combo or the Choose… button.
  • Choose… reopens the chooser at any time in Selected mode.

Chooser implementation

2) Add a chooser component shown via CallOutBox

New file(s)

  • src/RemoteChannelChooser.h/.cpp (or keep local in PluginEditor.cpp if preferred)

Component
RemoteChannelChooserComponent : public juce::Component

  • 8 rows (fixed): Ch 1..8
  • Each row contains:
    • juce::ToggleButton enabled checkbox
    • channel text (Ch N)
    • small colour swatch component (or a TextButton with background colour)
    • label text (read-only)
  • Optional buttons: All, None

Data in chooser

  • Maintains/edits a uint8_t remoteChannelMask (bits 0..7 => Ch1..Ch8).
  • Shows label/color for each channel:
    • Derived from the most recent RemoteInstanceInfo for that channel (see “Identity mapping” below).

Identity mapping rule for display

  • For each channel index N, pick the most recently seen online remote instance advertising N (by RemoteInstanceInfo.lastSeenMs) and display its:
    • label (channelLabel)
    • colour (colourRGBA)

If no remote currently advertising that channel:

  • display label as "—" and swatch as neutral/dim.

State + storage

3) Add editor state fields

File: src/PluginEditor.h
Add:

  • enum class RemoteMode { All, Selected, None };
  • RemoteMode m_remoteMode = RemoteMode::All;
  • uint8_t m_remoteChannelMask = 0xFF; // default all enabled
  • uint8_t m_lastRemoteChannelMask = 0xFF;

(Optionally: persist via APVTS later; not required for first pass unless desired.)


Processing changes (core requirement)

4) Replace the old remoteEnabled toggle logic in timerCallback()

File: src/PluginEditor.cpp in timerCallback() near current block:

const bool remoteEnabled = remoteDisplayToggle.getToggleState();
if (remoteEnabled) {
  ...
  scopeDisplay.writeRemotePackets(m_remoteDataCache, m_remoteInfosCache);
} else if (m_lastRemoteEnabled) {
  scopeDisplay.clearRemoteInstances();
}
m_lastRemoteEnabled = remoteEnabled;

Replace with mode-based logic:

A) Mode == None

  • audioProcessor.setReceiveEnabled(false);
  • scopeDisplay.setRemoteDisplayEnabled(false);
  • On transition into None:
    • scopeDisplay.clearRemoteInstances();

B) Mode == All

  • audioProcessor.setReceiveEnabled(true);
  • scopeDisplay.setRemoteDisplayEnabled(true);
  • Ingest all as today:
    • fetch infos + packets
    • scopeDisplay.writeRemotePackets(allPackets, allInfos);

C) Mode == Selected

  • audioProcessor.setReceiveEnabled(true); (multicast is global; we keep receiving)
  • scopeDisplay.setRemoteDisplayEnabled(true); (remote drawing enabled, but only selected will exist)
  • Fetch infos + packets as today, but filter at ingestion:

Filtering

  • Keep only packets whose packet.instanceIndex (1..8) is enabled by m_remoteChannelMask
  • Keep only RemoteInstanceInfo whose info.instanceIndex is enabled by mask
  • Pass filtered vectors to writeRemotePackets()

Important: this ensures non-selected channels are never stored/processed and do not affect RMS/cancellation.


Clearing state when deselecting channels

5) Add ScopeDisplay API to clear by channel selection

Files

  • src/ScopeDisplay.h
  • src/ScopeDisplay.cpp

Add one of:

  • void clearRemoteChannelsNotInMask(uint8_t enabledMask);
    • Deactivate/clear any remote instance slots whose channel index is not enabled.
    • Also remove their entries from any remote-info map if applicable.

When to call

  • Whenever m_remoteChannelMask changes in Selected mode, call:
    • scopeDisplay.clearRemoteChannelsNotInMask(m_remoteChannelMask);
  • Also call it when entering Selected mode (to ensure old non-selected data is purged).

This prevents stale data from lingering after a channel is turned off.


Instance identity considerations

6) Correctness rules

  • Selection is by channel index (instanceIndex), but packets/infos are per instanceID.
  • Filtering MUST use:
    • RemoteRawPacket.instanceIndex
    • RemoteInstanceInfo.instanceIndex
  • If multiple instances share a channel index and it’s enabled:
    • allow them through ingestion (all processed).
  • Self packets are already ignored in network receive (by instanceID); keep that behavior.

Layout updates

7) Update resized() for the Network row

File: src/PluginEditor.cpp::resized()
Current row assigns bounds for:

  • localDisplayToggle, remoteDisplayToggle, broadcastToggle

Update to include:

  • localDisplayToggle
  • remoteModeLabel + remoteModeCombo (+ remoteChooseButton if used)
  • broadcastToggle

Keep overall width ~ similar to current remoteArea (currently 360px), adjust as needed.


Testing checklist

  1. Remote: None
    • remote waveforms disappear
    • remote slots cleared once (no lingering)
    • CPU drops (no remote ingest)
  2. Remote: All
    • behavior matches current “Show Remote ON”
  3. Remote: Selected…
    • chooser opens
    • unchecking a channel immediately removes it from display
    • it does not return until re-enabled
    • RMS/cancellation change as expected (deselected channels no longer influence them)
  4. Label/color in chooser
    • when a remote is online on Ch N, chooser shows their label + colour
    • if no remote online, shows placeholder
  5. Channel collisions
    • if two remotes claim Ch 3 and Ch 3 enabled, both show/process (document this behavior)

additionall

  • Persist RemoteMode + remoteChannelMask in APVTS/state so it restores with the session.
  • Add “All/None” buttons in chooser for quick toggling.
  • Add tooltip text clarifying that filtering is by channel index.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions