The Audio World
Chapter 1Dante and Digital Audio Networking
Before ETR-NodeScope can monitor anything, audio has to be flowing. That audio lives on a Dante network — a standard that turns ordinary Ethernet switches into a professional audio router capable of carrying hundreds of channels with submillisecond latency.
What Is Dante?
Dante (Digital Audio Network Through Ethernet) was developed by Audinate, a company founded in Sydney, Australia in 2003. It carries uncompressed PCM audio as RTP/UDP packets over standard Ethernet. Every Dante-enabled device — wireless receiver, digital console, amplifier, stage box — appears as both a source and destination on the network, addressable by name.
The maths of audio over Ethernet
48,000 samples/sec × 24 bits × 64 channels
= 73,728,000 bits/sec ≈ 74 Mbps
Gigabit Ethernet capacity: 1,000 Mbps
That's 13× headroom — one GbE port carries
the full 64-channel load easily.How ETR-NodeScope Receives Dante Audio
ETR-NodeScope does not handle Dante UDP packets directly. It uses Dante Virtual Soundcard (DVS) — a driver that presents the Dante network as an ASIO audio device to Windows. DVS handles all the protocol work; ETR-NodeScope reads the delivered audio the same way a DAW reads from an audio interface.
| Layer | What it does |
|---|---|
| Dante network (UDP/IP) | Audio flows as RTP/UDP packets between devices on the LAN |
| Dante Virtual Soundcard | Receives UDP packets, presents them as an ASIO device to Windows |
| ETR-NodeScope (ASIO client) | Reads audio frames via the ASIO API — same as a DAW reading a sound card |
| Tile meters | Converts each audio frame to a dBFS/dBu level for display |
From Float to dBu — How the Meter Works
ASIO delivers audio as 32-bit floats normalised to −1.0 to +1.0. ETR-NodeScope converts to dBu (a real-world voltage reference) using the EBU R68 calibration constant:
// ASIO delivers float samples where +1.0 = 0 dBFS
// Professional standard: 0 dBFS ≈ +18 dBu (EBU R68)
double dBFS = 20.0 * Math.Log10(Math.Abs(sample) + 1e-9);
double dBu = dBFS + 18.0;
// Example: sample = 0.5 → dBFS = -6.02 → dBu = +11.98Chapter 1 Quiz
Dante and Digital Audio Networking — 5 questions, pass mark 60%
What protocol does Dante use to carry audio across the network?
Why does ETR-NodeScope use Dante Virtual Soundcard rather than connecting to the Dante network directly?
What is ASIO and why does it matter for ETR-NodeScope?
At 48,000 Hz with a 512-sample ASIO buffer, how often does ETR-NodeScope's audio callback fire?
What does the +18 dBu offset in the ETR-NodeScope level calculation represent?
History of the Protocols
Chapter 2A History of Every Protocol ETR-NodeScope Uses
Every technology in ETR-NodeScope has a story. Understanding where these standards came from — who invented them, what problem they solved, what constraints they were designed under — makes them much easier to understand and remember.
POWER ON\r. When Ethernet replaced RS-232 in the 2000s, manufacturers simply wrapped the same ASCII commands in TCP sockets. Wireless's port 2202 protocol — < GET 1 BATT_CHARGE > — is RS-232-era design philosophy running over modern TCP/IP.
<audio> tags) can decode the stream. The format is 34 years old and every media player still reads it.
Complete Timeline
| Year | Technology | ETR-NodeScope Connection |
|---|---|---|
| 1937 | PCM (Alec Reeves) | Foundation of all digital audio |
| 1958 | Sony WRT-57/WRR-57 — first commercial wireless mic | Start of the wireless microphone industry |
| 1960 | RS-232 serial standard (EIA) | Ancestor of Wireless's ASCII command protocol |
| 1974 | TCP/IP (Cerf and Kahn) | Foundation of all ETR-NodeScope network communication |
| 1983 | MIDI standard | Context for OSC — shows what OSC improved on |
| 1991 | WAV file format (Microsoft/IBM) | ETR-NodeScope HTTP audio stream format |
| 1997 | ASIO (Steinberg) | ETR-NodeScope's audio capture interface |
| 1997 | OSC 1.0 (Wright & Freed, CNMAT) | ETR-NodeScope OSC integration for Qlab etc. |
| 1999 | MQTT (Stanford-Clark, IBM) | ETR-NodeScope MQTT channel state publishing |
| 2000 | REST (Fielding PhD thesis) | Foundation of Wireless SSC v2 API |
| 2001 | JSON (Crockford) | Wireless protocol, WebSocket messages |
| 2002 | mDNS / Bonjour (Apple) | Finds Wireless and digital wireless control IPs |
| 2003 | Dante development begins (Audinate) | The audio network ETR-NodeScope monitors |
| 2006 | Server-Sent Events (Hickson, Opera) | Wireless SSC v2 real-time data delivery |
| 2011 | WebSocket RFC 6455 | ETR-NodeScope WebUI real-time updates and audio |
| 2017 | FCC 600 MHz spectrum repack | Forces wireless frequency coordination |
| 2017 | digital wireless launched (Wireless) | AD4D, AD4Q — Wireless hardware supported |
| 2020 | digital wireless receiver launched (Wireless) with SSC v2 | Wireless hardware supported |
Chapter 2 Quiz
History of the Protocols — 5 questions, pass mark 60%
PCM was invented in 1937 by Alec Reeves. Why was it not commercially exploited until the 1970s?
Why does Wireless's TCP/2202 protocol look like RS-232 serial commands with angle brackets and spaces?
MQTT was invented to solve a problem in which industry, and what were the three key design constraints?
Why did Wireless choose Server-Sent Events (SSE) for digital wireless receiver receivers rather than WebSocket or raw TCP?
What was the fundamental problem that led to the creation of WebSocket in 2011?
The Software Architecture
Chapter 3How ETR-NodeScope Is Structured
ETR-NodeScope is a C# / .NET 8 desktop application using Avalonia UI. Its architecture separates concerns into layers — each layer has one job, communicates upward via events, and knows nothing about the layers above it.
| Layer | Classes | Responsibility |
|---|---|---|
| Models | WirelessChannelState, ChannelStateDto, AlertModel | Pure data — no logic, no UI |
| Services | ShureDeviceClient, DiscoveryService, SmartAlertsEngine, WebUIServer | All business logic and hardware communication |
| ViewModels | MainViewModel, ChannelTileViewModel | Translate service data into bindable UI properties |
| Views | MainWindow.axaml, ChannelTile.axaml | XAML layout — no logic, binds to ViewModel properties |
| Integration | MqttService, OscService, SessionLogService | MQTT broker, OSC UDP, log files |
The 12-Step Journey from Transmitter to Tile
1. Transmitter embeds battery % in RF side-channel (~400 bits/sec)
2. Receiver demodulates: "< REP 1 BATT_CHARGE 078 >"
3. BaseDeviceClientAdapter.ReceiveLoopAsync() reads the line
4. ShureDeviceClient.ParseMessage() routes → HandleRep()
5. case "BATT_CHARGE": state.BatteryPercent = 78
6. PublishChannelIfChanged() compares with last published snapshot
7. If changed → RaiseChannelDataReceived(channel=1, state)
8. C# event fires → all subscribers notified simultaneously
9. MainViewModel.OnDeviceChannelData() receives it
10. tile.BatteryDisplay = "78%"
11. tile.BatteryLevel = 0.78
12. Avalonia data binding updates the tile — no explicit UI codeThe Observer Pattern — C# events
Step 7–9 uses C# events — a built-in implementation of the Observer pattern (Gang of Four, 1994). ShureDeviceClient fires an event; MainViewModel, SmartAlertsEngine, WebUIServer, and SessionLogService all subscribe. ShureDeviceClient knows nothing about any of them:
// ShureDeviceClient declares the event:
event Action<int, WirelessChannelState>? ChannelDataReceived;
// Fires it after parsing a REP message:
RaiseChannelDataReceived(channel, state);
// All four subscribers run — ShureDeviceClient knows nothing about them:
mainViewModel.ChannelDataReceived += OnDeviceChannelData;
smartAlertsEngine.ChannelDataReceived += EvaluateChannel;
webUIServer.ChannelDataReceived += PushStateToClients;
sessionLogger.ChannelDataReceived += MaybeLogBatterySwap;Change Detection — Not Every Frame Reaches the UI
Wireless sends SAMPLE frames every 100ms. ETR-NodeScope filters them before the UI is updated — only meaningful changes cause a redraw:
private void PublishChannelIfChanged(int channel, WirelessChannelState state)
{
var snapshot = ChannelPublishSnapshot.From(state);
if (_lastPublishedState.TryGetValue(channel, out var previous) &&
!snapshot.HasMeaningfulChangeFrom(previous))
return; // RF changed by 0.5 dBm? Skip the UI update.
_lastPublishedState[channel] = snapshot;
RaiseChannelDataReceived(channel, state); // Something real changed
}WebUI Audio Path — Float to Browser
// Called every 10.7ms from ASIO callback
// Converts ASIO float32 → PCM int16 → WebSocket binary frames
unsafe {
fixed (float* pIn = frame)
fixed (byte* pOut = pcm) {
short* pS = (short*)pOut;
for (int s = 0; s < totalSamples; s++) {
float v = Math.Clamp(pIn[s * channelCount + ch], -1f, 1f);
pS[s] = (short)(v * 32767f); // float → int16
}
}
}
// 128 samples × 2 bytes = 256 bytes per WebSocket binary frame
// 375 frames/sec, each 2.67ms apartChapter 3 Quiz
Software Architecture — 5 questions, pass mark 60%
What is the purpose of PublishChannelIfChanged() in ETR-NodeScope?
In MVVM, what is the ViewModel's role and why is it important?
Why does ETR-NodeScope start the receive loop before calling ConnectDeviceAsync()?
When ETR-NodeScope converts ASIO float audio to PCM-16LE for WebSocket streaming, why multiply by 32767 rather than 32768?
What is the Observer pattern and how does ETR-NodeScope use it?
Wireless Protocols in Depth
Chapter 4Wireless Protocol — ASCII over TCP
The Grammar
| Type | Format | Example |
|---|---|---|
| GET | < GET channel PARAM > | < GET 1 BATT_CHARGE > |
| SET | < SET channel PARAM value > | < SET 1 AUDIO_MUTE ON > |
| REP | < REP channel PARAM value > | < REP 1 BATT_CHARGE 078 > |
| SAMPLE | < SAMPLE ch v1 v2 > or keyword form | < SAMPLE 1 058 142 > |
// The entire Wireless protocol parser — one regex + one split:
var match = Regex.Match(line, @"< (?<body>.+?) >");
string[] parts = match.Groups["body"].Value.Split(' ');
switch (parts[0]) {
case "REP": HandleRep(parts); break;
case "SAMPLE": HandleSample(parts); break;
}Device Family Detection
case "MODEL":
_model = value.Trim();
_isAxient = _model.ToUpperInvariant().StartsWith("AD") ||
_model.ToUpperInvariant().StartsWith("ADX");
// Fallback channel count if NUM_CH response is late:
if (model.Contains("AD4Q") || model.Contains("ULXD4Q")) return 4;
if (model.Contains("AD4D") || model.Contains("ULXD4D")) return 2;
if (model.Contains("ULXD4") || model.Contains("AD4")) return 1;wireless receiver vs digital wireless — Key Differences
| Parameter | wireless receiver | digital wireless |
|---|---|---|
| RF level | RX_RF_LVL (raw − 128 = dBm) | RSSI (raw − 120 = dBm) |
| Audio level | AUDIO_LVL (0–255) | AUDIO_LEVEL_RMS (raw − 120 = dBFS) |
| Battery % | BATT_CHARGE (0–100 direct) | TX_BATT_CHARGE_PERCENT or SLOT_BATT_CHARGE |
| Mute | TX_MUTE_STATUS (ON/OFF/UNKN) | TX_TALK_SWITCH (OFF = muted) |
| Audio gain | AUDIO_GAIN reported − 18 dB | AUDIO_GAIN (same offset) |
| RF quality | Derived from dBm only | CHAN_QUALITY 0–5 bars |
Chapter 5Wireless Protocol — HTTPS + Server-Sent Events
SSC v2 Connection Sequence
// Step 1: Confirm SSC v2 supported
GET https://[ip]/api/ssc/version → {"version":"2.3.0"}
// Step 2: Get device identity
GET https://[ip]/api/device/identity
→ {"name":"digital wireless receiver EM2","channels":2,"firmware":"4.2.1"}
// Step 3: Open SSE stream (never closes until ETR-NodeScope disconnects)
GET https://[ip]/api/ssc/state/subscriptions
→ {"uuid":"3f7a-..."}
// Step 4: Subscribe to resource paths
PUT https://[ip]/api/ssc/state/subscriptions/3f7a-.../add
→ {"resources":["/api/device/channels/0/audio/level", ...]}
// Receiver now streams events whenever values change:
data: {"/api/device/channels/0/transmitter/battery/level":{"value":78}}
data: {"/api/device/channels/0/rf/level":{"value":-72.5}}Chapter 6Wireless Protocol — JSON over TCP
Wireless receivers allow only one TCP controller at a time on port 32789. If Wireless Manager is connected, ETR-NodeScope cannot connect. Disconnect all other tools before starting ETR-NodeScope.
// ETR-NodeScope sends: get_device_info
// Receiver responds:
{"result":"ok","data":{"product":"receiver","num_channels":2,"firmware":"1.9.3"}}
// Receiver then pushes continuously:
{"channels":[
{"name":"CH1","frequency":614500000,
"rf_a":-68.5,"rf_b":-71.0,"audio":-14.2,
"battery":78,"mute":false},
...
]}
// Parsed with System.Text.Json:
using var doc = JsonDocument.Parse(json);
var arr = doc.RootElement.GetProperty("channels");
foreach (var ch in arr.EnumerateArray()) {
s.RfLevelA = ch.GetProperty("rf_a").GetDouble();
s.BatteryPercent = ch.GetProperty("battery").GetInt32();
ChannelDataReceived?.Invoke(chIndex - 1, s); // 1-based → 0-based
}Part IV Quiz
Wireless Protocols — 5 questions, pass mark 60%
A Wireless AD4D-A sends < REP 1 RX_RF_LVL 043 >. What is the actual RF level in dBm, and which device family is this?
ETR-NodeScope receives: < SAMPLE 1 RSSI A 083 > from an digital wireless receiver. What does it do with this message?
What does Wireless's SSE subscription PUT request do and why is it necessary?
Why does Wireless's ChannelDataReceived call use chIndex - 1 when invoking the event?
ETR-NodeScope discovers a Wireless receiver at 169.254.158.222 via multicast and at 169.254.100.134 via TCP fingerprint. It connects to 169.254.100.134. Why?
Using the Software
Chapter 7Installation, Setup and Live Operation
System Requirements
| Requirement | Details |
|---|---|
| OS | Windows 10 64-bit (1903+) or Windows 11 |
| CPU | 4-core minimum — i5/Ryzen 5. i7/Ryzen 7 recommended for 32+ channels |
| RAM | 8 GB minimum, 16 GB recommended |
| Network | Wired Ethernet. Wi-Fi not recommended for ASIO audio |
| Dante Virtual Soundcard | DVS 4.x licensed and running. No other ASIO app open simultaneously |
| .NET 8 | Bundled with the ETR-NodeScope installer |
First Launch — Step by Step
1. Launch ETR-NodeScope.exe as Administrator → 2. File → Settings → Audio → select Dante Virtual Soundcard → Channel Count = 8 → Save → 3. Press F5 → 4. Watch Live Logs (Ctrl+L) for "Connected to x.x.x.x:2202"
Use Actions → Demo Mode if you have no hardware. Simulated RF, battery, and audio will populate all tiles so you can explore the interface without a physical show.
Settings Reference
| Setting | Default | Notes |
|---|---|---|
| ASIO Device | — | Must be set to Dante Virtual Soundcard |
| Channel Count | 8 | Maximum 64. Add more without restarting |
| Buffer Size | 512 samples | 10.7ms at 48 kHz |
| WebUI Port | 8080 | Browser access: http://[ip]:8080/ |
| Allow Remote Clients | Off | Turn on for tablet access |
| Enable Remote Commands | Off | Allows browsers to send Start/Stop/mute |
| Shared Secret | empty | Token for remote command auth in URL |
| Battery Warning % | 30 | Per-talent override in Talent Manager |
| Battery Critical % | 15 | Per-talent override available |
| RF Warning dBm | −85 | Per-talent override available |
Keyboard Shortcuts
| Shortcut | Action |
|---|---|
F5 | Start monitoring |
F6 | Stop monitoring |
Ctrl+, | Open Settings |
M | Toggle Mic Check Mode |
Ctrl+L | Open Live Logs |
Ctrl+S | Quick save Show Preset |
Enter / Space | Monitor/unmonitor selected channel |
Part V Quiz
Using the Software — 5 questions, pass mark 60%
ETR-NodeScope's tiles are showing "---" for channel name, battery, and frequency after connecting. What is the most likely cause?
A Wireless receiver is not appearing in ETR-NodeScope's Connected Devices panel. You have verified the IP is reachable. What should you check first?
What is the purpose of the Release Readiness check in ETR-NodeScope?
You want a tablet operator to view the WebUI and be able to send mute commands, protected by a password. What settings do you need?
Why does the WebUI audio stream work in Chrome but a student reports no audio in their browser despite the connection indicator being green?