Open-source Linux control for Antelope Audio USB interfaces, reverse-engineered from USB traffic capture on hardware the contributors own (plus facts from Antelope's own public manuals). No vendor software, firmware, or source is used; the device firmware is not touched.
Looking for the wire format? The protocol spec -- frame types, the bus-vs-channel address spaces, every bitmask, the
0x73state-report byte-map, talkback, output trim, meter calibration, open questions -- lives inPROTOCOL.md. This README is the guide to using the tool.
| file | what's in it |
|---|---|
README.md (this file) |
using the CLI; adding a param / a device; RE ground rules |
AGENTS.md |
sole project instruction entry point; local handoff and open tasks have separate roles |
PROTOCOL.md |
the reverse-engineered wire format in reference form — frames, opcodes, state-report byte maps, the 0x74/0x75 readback protocol (§4a), per-device notes (§14) |
docs/profile-schema.md |
what every key in profiles/*.json means, and which the code reads — start here if you're writing a profile or a client (webUI) |
docs/profile-labeling.md |
the cross-client labels and feature-manifest contract — how to add a device and keep shared parameter vocabulary consistent |
docs/discrete-remote-agent-playbook.md |
remote-only workflow for completing the Discrete 4 / 4 Pro / 8 Pro profiles with the repository's probes, capture tools, and safety rules |
CAPTURING.md |
how to capture USB traffic — usbmon on Linux (incl. the webUI + usbmon method), Windows VM + USBPcap, or native macOS |
docs/orion-afx-workflow.md |
current AFX evidence, existing tools, proposed Launcher batch automation and effect completion criteria |
profiles/*.json |
the machine-readable source of truth, one per device (orion_studio_sc is the reference; also zen_go_sc, discrete_8_pro_sc, discrete_4_sc, discrete_4_pro_sc) + mic_models.json and the shared afx_effects.json reference catalog |
SCOPE.md / EULA-ANALYSIS.md |
the distinction between device-side AFX Real-Time controls and host-side Native/Cosmos plugins, plus this repo's control and licensing boundaries |
The UI and application-facing software call the bundled Mix 1 reverb
Gazelle Reverb. AuraVerb remains the hardware/protocol name used in
captures, reverse-engineering findings, and wire-level identifiers. The
existing CLI subcommand, profile key, and API path (auraverb) remain as
compatibility identifiers.
Roughly what's decoded (2026-09): preamp gain/mode/phantom/phase + link, ADAT/S/PDIF gain and device link controls, output buses (monitor/HP/line/reamp) with dim/mute/mono, the full routing matrix (15 dests, 12 source banks, read
- write, self-verifying), the virtual mixer (4 mixes, read + write),
Gazelle Reverb (read/write, hw-verified), mic modeling / emuMic (frame
decoded), sample rate / clock source / pan law / DC-coupling /
oscillator / screen brightness / output trim (all
SET_GLOBAL), talkback, and the surround monitoring tab (per-speaker 16-band EQ + global + 2.1 bass management; Room Correction turned out to be the per-speaker EQ). In-band readback (0x74/0x75) covers routing, mixer, the bundled reverb, device identity, preamp/channel state, both surround frames (categories0x1aper-speaker EQ /0x1bglobal), and the newly structured Orion nested records: category0x0blink tables,0x15AFX instance counts,0x16mic-emulation state, and0x19AFX strip order. The CLI and WebUI display those profile-declared records. There is no built-in per-input-channel EQ on this device: input EQ uses an AFX Real-Time effect; AFX EQ parameter controls have no verified implementation (SCOPE.md,PROTOCOL.md). Normal WebUI format writes are limited to 2.0 and 2.1. The global format wire path was directly round-trip tested through 9.1.6 withtools/surround_format_selftest.py. The meaning of classic category0x07, the outer query bounds for0x0c, and some0x74channel-group names remain unresolved. Tagged AFX instance-state queries use a separate namespace. Category0x0bindex 0 contains six link flags for the device's 12 physical preamps, paired as 1/2 through 11/12. ADAT uses the eight records at0x0b:1; S/PDIF uses the single record at0x0b:2. Direct device transitions and subsequent Windows VM Launcher checks on 2026-09-30 confirmed input link selectors0=Preamp,1=ADAT,2=S/PDIF. Earlier Launcher button captures used incorrect digital selectors; the old project interpretation was corrected. The AFX plugin-chain slot frame (0x23/0xd7, which channel holds which plugin instance) is field-mapped for observation inPROTOCOL.md§12a. General loading remains outside scope; the owner's explicitly requested Orion captured-effect rack pilot is a bounded exception (SCOPE.md). Its operator-driven test interface supports whole-chain loading/removal/reordering and live complete parameter blocks. The AFX-tab channel stereo-link is in scope — it is plainSET_LINK(space0x04). SeePROTOCOL.md§13 for the live open list.
AFX preparation separates device capacity from effect definitions. Orion's
afx metadata declares 32 mono channels and eight insert slots per channel,
as described in Antelope's public demonstration.
The shared profiles/afx_effects.json catalog contains 80 reference effect
entries and 729 declared control names from Gazelle's Discrete 4 findings,
with pinned source provenance. The first three descriptions are drawn from
public effect pages. The Orion Memory Cat Brigade implementation records
six observed 0–100 knob fields and two binary switch fields from an
owner-labelled capture. Switch labels are owner-confirmed: raw0=550ms/Chorus,
raw1=1100ms/Tremolo. Parameter/processing-state readbacks are captured for
instances0–2; intermediate physical scaling remains unresolved. The typed operator path separately records
verified chain/link round trips and owner-reported parameter operation. Owner-labelled load captures establish
Orion type IDs for Memory Cat, V12 Chorus, BBD-Chorus, Instinct and
Master De-Esser; catalog commands stay null. This metadata does not enable
a general effect writer. See the profile schema.
The WebUI's AFX button, between Routing and Mix 1, opens a detachable window with a channel selector and one channel overview on the left, and the selected channel's effects rack on the right. Orion offers 32 channels; the rack shows all eight slot panels together and remembers the selected channel while the main page remains open. Each effect's presentation has its own JavaScript and CSS module. Memory Cat provides six draggable knobs and two mode selectors in an original Gazelle rack design. Local previews retain settings per channel/slot and support drag-and-drop or keyboard-button reordering.
The AFX channel dropdown shows a loaded-effect count (for example, AFX 1 · 2 FX)
from slot readbacks only when at least one effect occupies that channel. Empty
and unavailable channels have no count; existing link markers remain visible.
Opening AFX shows the device rack. Select any of AFX 1–32; each slot in the left channel frame has an effect selector grouped into Dynamics, EQ & Filters, Modulation, Delay & Reverb, Pitch & Tuning, Amps & Cabinets, Preamps, Saturation & Distortion, and Other. These are presentation categories, not device capabilities. Memory Cat, Instinct, Master De-Esser, V12 Chorus and BBD-Chorus have independently captured Orion load IDs; unmapped entries are greyed out. Selecting a different effect replaces a supported insert; selecting Empty removes it. Loading uses fresh instance counters and the complete slot inventory, measured instance indices, and verified whole-chain writes. Counts describe resources, not license ownership. Direct load/remove tests on AFX 3–32 verified matching slot readbacks and exact restoration; AFX 1 was owner-tested earlier. AFX 2 was not part of those mono trials. Only Memory Cat has parameter controls so far. Drag effects within or between the left slot list and the rack to reorder the selected chain.
The compact pair button sits inside the left channel header and follows
the selection: AFX 1 or 2 shows 1–2, AFX 3 or 4 shows 3–4, through 31–32.
Its glow and accessible label identify the current link state. Linked
channel-menu entries show their partner. Link indicators use device readback: all 16 space-4 flags were
independently verified against 0x0b:4 records 0–15 on 2026-10-01. Records
16–31 remain unmapped. Opening or refreshing reads link flags and selected
chain/settings, including the partner when linked.
The button sends only the flag, preserving both chains. Stereo parameter
sharing is enabled for Memory Cat's bounded operator pilot: edits update the existing selected
instance and a same-slot Memory Cat partner when present, using fresh chain
readbacks. Audible confirmation and a Memory Cat-specific vendor stereo
capture remain pending. Other-effect parameter controls remain unavailable.
Adding, replacing, removing and reordering effects works while linked: the
controller reads both adjacent chains, preflights both edits, allocates distinct
instances and writes left then right, matching the captures. Both resulting
chains must verify. Loads require two available instances and empty target slots
on both sides; removal/replacement/reordering requires matching effect types at
the edited slot. Conflicting chains remain unchanged for independent editing.
If the selected linked rack is empty and the partner has effects, the popup
selects that populated channel so its actual controls remain visible. Links
on other pairs do not block that channel.
Memory Cat instances0–2 now initialize from real device readback on opening, channel selection, loading and reconnect. The JSON profiles describe a separate tagged instance-state query, its eight parameter offsets and processing flag. The owner's Launcher-restart capture returns the exact minimum/maximum/near-50% values without replaying parameter writes. New effects were also confirmed active after Bypass All; loading preserves the device-assigned state. Power-cycle persistence remains untested. Other instance reads/effect fields remain guarded; unknown settings stay unavailable. No hardcoded starting preset is sent. Knob and switch edits send automatically from the first edit; there is no Apply step. Opening or refreshing never sends settings. Requests are throttled and coalesced to the latest settings, with one in flight and no rack repaint during a drag. Failed writes pause live sending until an explicit Retry live controls click; they are not retried automatically. A missing or mismatching post-write slot reply, or mismatching parameter reply, disables further AFX writes for the server session; Refresh remains read-only and does not clear that failure. An instance-query timeout blocks further captured instance reads and edits until the device reconnects, because a late reply carries no instance identity. Reconnect clears old slot inventory, link flags and initialized parameter drafts. Wait for the full inventory before loading. After loading or reconnect, the selected rack queries fresh settings before its first edit. The owner confirmed both switches:0=550ms/Chorus,1=1100ms/Tremolo. The WebUI labels them from the Orion JSON. The owner reports the effect and applied parameter settings work. A 2026-10-01 Level 100→99→100 self-test found no stable parameter readback in the observed HID streams; the original settings were resent to restore the baseline, with the command acknowledged but no parameter readback to independently verify restoration. This does not establish absence of a separate parameter-readback protocol; the new restart capture identifies it. That older restoration trial remains send-only evidence. See PROTOCOL.md §12a for the effect IDs, complete device-test findings and remaining evidence limits. The Orion AFX workflow documents Linux readback recording, reuse of existing captures and proposed Launcher batch automation, with completion criteria for each available effect.
Local preview explicitly switches to drafts; Connect device rack returns to device slots. Opening the rack or changing channels sends no hardware mutations.
For profile-audit purposes, a writable parameter may be marked confirmed only
when its evidence records a dated, model-local write, a device-side readback
showing the before/after value, and restoration of the original value (or an
explicit user request to retain the state for cross-controller validation). Vendor
software traffic, UI or audio behaviour, and an undifferentiated moving byte do
not by themselves constitute a readback witness. Observed channel indices may
confirm membership, but the maximum safe index remains derived until an
out-of-range probe is performed; clients must not probe one casually because a
sibling device faults hard on an invalid index.
This is an independent interoperability project. It is not affiliated with, authorized by, or endorsed by Antelope Audio. "Antelope", "Orion", "Discrete", "Synergy Core" and related names are trademarks of their respective owner and are used here only to identify the hardware this software interoperates with.
- No Antelope software, firmware, or source code is included or redistributed here. The device is not modified and its firmware is not touched.
- The protocol description was produced by observing USB traffic to/from hardware the contributors own, on their own machines, for the sole purpose of making that hardware usable on operating systems the vendor does not support (Linux). Reverse engineering for interoperability is recognised in the US (e.g. Sega v. Accolade, Sony v. Connectix) and expressly permitted in the EU (Software Directive 2009/24/EC, Art. 6).
- Antelope's own publicly published documentation (user manuals, spec
sheets, signal-flow diagrams from their download pages) is used only as
a source of facts -- channel counts, feature names, signal flow. No
manual text, tables, or diagrams are reproduced here; documents are
cited in the profile's
evidencefields. - Packet captures committed under
captures/are recordings of the contributor's own device on the contributor's own machine, kept minimal and included only as evidence for a documented finding. Device serial numbers are redacted where practical. - AFX means device-side Synergy Core Real-Time effects here. Antelope
describes these as running on a supported device, with a Real-Time
license assigned to that device through Launcher. Their parameter
controls are in scope when the effect is already available under a
license assigned to the target device. Native plugins run in a DAW on the
computer; Cosmos membership provides Native versions, not Real-Time
licenses. This project does not handle license assignment, activation,
entitlement traffic, or plugin loading. See
SCOPE.mdandEULA-ANALYSIS.md; the Cosmos EULA is not treated as the license for AFX Real-Time controls. - Use at your own risk. Sending control frames to hardware can put it in unexpected states; see "hazards" in the profile JSON. No warranty.
If you are a rights holder with a concern, please open an issue.
- Code, CLI, web UI and docs: GPL-3.0-or-later (see
LICENSE). Copyleft on purpose -- a fork that ships a modified version has to ship its source too, so this can't be quietly taken closed. GPL-3.0 (not 2.0) for the explicit contributor patent grant, which is the real protection a reverse-engineering project has, and for Apache-2.0 dependency compatibility. - Device profiles (
profiles/): MIT (seeprofiles/LICENSE). The hardware knowledge -- request tables, offsets, ranges, bounds, quirks, evidence -- is kept permissive so it can flow into any project, including copyleft ones. Most of it is uncopyrightable fact anyway.
Copyright (c) 2026 diafebus. Contributions are accepted under the same terms (GPL-3.0-or-later for code, MIT for profiles); by opening a pull request you agree to license your contribution that way.
- Linux, Python 3.9+ (no third-party packages -- stdlib only).
- Read/write access to the device's
hidrawnode. Either run withsudo, or add a udev rule granting your user access to the Antelope device'svid/pid(seedevicein the profile JSON) so you don't need root every time. - Captures for adding new params are done either from a Windows VM
(Antelope Launcher + USBPcap) against the passed-through hardware, or --
simpler, when the control exists in this project's own web UI -- with
usbmonon the Linux host while the web UI drives one control. SeeCAPTURING.md.
profiles/orion_studio_sc.json <- single source of truth for the Orion Studio Synergy Core protocol
profiles/discrete_8_pro_sc.json <- sibling device (peer-contributed)
profiles/zen_go_sc.json <- sibling device (Zen Go Synergy Core), profile-driven first pass
profiles/discrete_4_sc.json <- sibling device, STUB (status: UNCONFIRMED -- transport never captured)
profiles/discrete_4_pro_sc.json <- sibling device, STUB (status: UNCONFIRMED -- transport never captured)
profiles/mic_models.json <- account-bound mic-modelling ("emuMic") model catalogue
antelope/transport.py <- generic HID open/read/write (no device-specific code)
antelope/protocol.py <- generic frame build/parse, driven entirely by the profile
antelope/cli.py <- generic CLI, driven entirely by the profile
tools/README.md <- tool index, safety classes, and command guide
tools/capture_diff.py <- offline helper for finding new params from captures
tools/scan_capture.py <- offline helper: auto-finds the transition across a whole capture (Windows TSV)
tools/scan_macos_capture.py <- same, for native-macOS (Darwin XHC) pcapng -- see CAPTURING.md
tools/hid_probe.py <- dump the HID report descriptor + probe for a readable Feature report
tools/selftest.py <- round-trip self-test against real hardware via the readback (read-only by default, --write for restore-guaranteed writes)
tools/surround_eq_selftest.py <- bounded Surround EQ readback and one-field write/restore test
tools/surround_format_selftest.py <- bounded Surround format readback and reversible format write probes
tools/surround_eq_position_selftest.py <- Surround EQ PRE/POST readback and one-bit write/restore probe
CAPTURING.md <- how to capture USB traffic (Windows VM + USBPcap, or native macOS)
PROTOCOL.md <- the reverse-engineered wire format, in reference form
captures/ <- analyzed .tsv exports + raw .pcapng captures/ (full-fidelity)
Rule of thumb: if you're about to hardcode a byte offset, param_id, or magic
number anywhere in antelope/, stop -- it belongs in the profile JSON instead.
That's what keeps this working across new params and new devices without
touching the Python. (When a new feature needs a genuinely different frame
shape -- see channel_link below -- it gets its own block under
profile["frame"], rather than being forced into the existing one.)
--profile accepts a path, a bare filename, or a short name
(orion / zen_go / discrete_8_pro), and defaults to orion (or
$ANTELOPE_PROFILE). The examples below spell out
--profile profiles/orion_studio_sc.json; antelope-ctl status on its own
works too.
Physical input channels (12 hybrid inputs, addressed 0-11):
python3 -m antelope.cli --profile profiles/orion_studio_sc.json status
python3 -m antelope.cli --profile profiles/orion_studio_sc.json set-mode 0 mic
python3 -m antelope.cli --profile profiles/orion_studio_sc.json set-gain 0 12
python3 -m antelope.cli --profile profiles/orion_studio_sc.json set-phantom 0 on
python3 -m antelope.cli --profile profiles/orion_studio_sc.json set-invert 0 on
python3 -m antelope.cli --profile profiles/orion_studio_sc.json meter # see below re: dB calibration
Channel link -- pairs adjacent inputs (ch1+ch2, ch3+ch4, ... ch11+ch12) for recording a stereo signal. Address either channel in the pair:
python3 -m antelope.cli --profile profiles/orion_studio_sc.json set-link 2 on # links ch3+ch4
python3 -m antelope.cli --profile profiles/orion_studio_sc.json set-link 3 off # same pair
Important (2026-08, confirmed on real hardware): the device itself does
NOT propagate mode/gain/phantom/phase across a linked pair -- that's the
official Launcher software sending extra commands, which set-link now
replicates. See "Channel link is real, but the syncing you see isn't the
device doing it" below before assuming any other tool will behave the same
way against this hardware.
ADAT inputs -- a separate 16-channel space (ADAT ch 0-15), gain plus a confirmed eight-pair link control (no mode/phantom/phase):
python3 -m antelope.cli --profile profiles/orion_studio_sc.json adat-status
python3 -m antelope.cli --profile profiles/orion_studio_sc.json set-adat-gain 0 6 # ADAT ch1, +6 dB
python3 -m antelope.cli --profile profiles/orion_studio_sc.json set-adat-link 0 on # links ADAT ch1+ch2 using space 1
ADAT links use SET_LINK space=1, pair indices 0–7, with device flags at
category 0x0b, index 1, bytes 0–7. Each selector was independently toggled
and restored on 2026-09-30 while all other link tables stayed unchanged.
All eight flags were then left ON at the user's request; after reconnecting
the Windows VM, the Launcher displayed all eight ADAT links ON.
The WebUI reads Preamp, ADAT, and S/PDIF flags from their separate tables
and refreshes the relevant table after a link write. A one-time browser
cache migration discards saved link entries from the old incorrect mapping;
complete device readback repopulates the buttons. CLI link commands use the
same corrected selectors but retain their separately labeled local cache
for gain mirroring; mark-adat-link only changes that CLI cache.
The user confirmed on 2026-09-30 that corrected WebUI ADAT and S/PDIF link writes and readbacks work, and the intended links survive controller restarts. These input-link mappings are confirmed.
Earlier ADAT-button captures sent space 0, which targets Preamp flags. Earlier S/PDIF-button captures sent space 1, which targets ADAT. These UI commands explained both the Preamp changes and the misleading historical S/PDIF attribution of ADAT byte 0, as well as the apparent failure to retain the intended digital-input links across Launcher restarts. ADAT pairs 7/8 failed the old tests because they were sent to the six-pair Preamp space. Gain coupling remains software behavior: controllers send a separate gain command to each linked channel. These link-flag trials did not test firmware gain propagation or retention through a physical device power cycle.
S/PDIF input -- a 2-channel space (0 = L, 1 = R), gain + link only:
python3 -m antelope.cli --profile profiles/orion_studio_sc.json spdif-status
python3 -m antelope.cli --profile profiles/orion_studio_sc.json set-spdif-gain 0 6 # S/PDIF L, +6 dB
python3 -m antelope.cli --profile profiles/orion_studio_sc.json set-spdif-link on # links L+R
S/PDIF links use SET_LINK space=2, pair index 0, and read back at 0x0b:2
byte 0. A direct OFF→ON write on 2026-09-30 changed only that byte, with
Preamp links OFF and all ADAT links ON. The state was deliberately left ON
for a Windows VM reconnect; the user confirmed the S/PDIF indicator ON.
S/PDIF gain remains parameter 0x5c, state-report offsets 91/92.
Output buses -- monitor A/B, headphone 1/2, plus the settings-tab Line and Reamp outputs. These are a different address space from the input channels above (see "Buses vs. channels" below); use either the numeric bus id or its name:
python3 -m antelope.cli --profile profiles/orion_studio_sc.json bus-status
python3 -m antelope.cli --profile profiles/orion_studio_sc.json set-bus-level monitor_a 60
python3 -m antelope.cli --profile profiles/orion_studio_sc.json set-bus-dim mona on
python3 -m antelope.cli --profile profiles/orion_studio_sc.json set-bus-mute hp1 on
python3 -m antelope.cli --profile profiles/orion_studio_sc.json set-bus-mono hp2 off
python3 -m antelope.cli --profile profiles/orion_studio_sc.json set-brightness 75 # front-panel screen, 0-100
python3 -m antelope.cli --profile profiles/orion_studio_sc.json sample-rate # show current rate
python3 -m antelope.cli --profile profiles/orion_studio_sc.json set-sample-rate 96k # 32k/44.1k/48k/88.2k/96k/176.4k/192k
python3 -m antelope.cli --profile profiles/orion_studio_sc.json clock-source # show (0=Oven 1=WordClock 2=ADAT 3=ADATx2 4=ADATx4 5=SPDIF 6=USB)
python3 -m antelope.cli --profile profiles/orion_studio_sc.json clock-source --set 1 # -> Word Clock (disruptive)
python3 -m antelope.cli --profile profiles/orion_studio_sc.json pan-law --set 2 # 0=-6dB 1=-3dB 2=-4.5dB 3=0dB
- Screen brightness only works when the device talks to a native host -- a VM Launcher no-ops the same slider (see the section further down).
set-sample-rateandclock-source --setare disruptive: the device drops audio and re-locks its clock (~1 s), and selecting a clock source that is not present/locked leaves the device unlocked. Change them with nothing streaming, then confirm (sample-rate/clock-source). Clock source reads back from0x73@19; the running rate reads back both as the 0-6 index (0x73@18) and in Hz (0x73@21-23, shown bysample-rate). pan law has no device readback, sopan-lawreports only what this CLI last sent.- Two things block a rate/clock write on Linux (neither is a device
limit -- macOS is unaffected): (1) any app or the audio server holding
the USB audio interface -- release it first, e.g.
pactl set-card-profile <orion-card> off(restore afterwards); (2) the write is also ignored while the clock source is USB (the device follows the host) -- set the clock to Oven, change the rate, set the clock back.
python3 -m antelope.cli identity # name / hw rev / firmware
python3 -m antelope.cli identity --serial # ...and the serial, on requestname OrionStudio_III
hw rev 7.0
firmware 4.41
stamp <u32> (?= YYYY-MM-DD, unconfirmed)
serial <13 chars, hidden -- pass --serial to show>
(The stamp is shown as a placeholder above for the same reason as the
serial -- it is per-unit data, so no real value from anyone's hardware is
committed here.)
Policy: no device serial is committed to this repo, ever. Profiles
describe the layout of the identity record (frame.readback.identity);
the value is read from the hardware on demand via readback category 0x01.
A serial identifies a specific physical unit and ties to an Antelope
account, so it is treated as the owner's data, not project data.
identity therefore hides it unless you pass --serial, so it can't slip
into a pasted terminal log or a bug report by accident. If some future
online feature needs it, it should ask the device at that moment rather
than read it from a file.
The same applies to captures and tool output: tools/readback_enum.py
blanks category 0x01, and tools/*_out.txt is gitignored. If you are
filing an issue, redact it.
The routing frame (opcode 0x53) is decoded: after the destination byte
it's a plain array of (source_bank, source_index) pairs, one per
output channel of that destination, all sent every time. The device
does report routing back -- via the 0x74/0x75 in-band query
protocol, category 0x03 (decoded 2026-08-31, PROTOCOL.md §4a). So
matrix-status is a live read of all 15 destination groups, and
route verifies every write against a read-back and seeds keep from the
device. Wired write destinations: line out (16 channels), HP1, HP2,
Monitor A, Monitor B, Reamp (2) -- all hardware round-tripped against a
real Orion Studio Synergy Core.
python3 -m antelope.cli ... route hp1 all preamp3 preamp4 # set every channel (seeds the cache)
python3 -m antelope.cli ... route lineout all compplay1..16 # range shorthand for a 16-ch seed
python3 -m antelope.cli ... route hp1 R preamp7 # change one channel, keep the rest
python3 -m antelope.cli ... route lineout 3 afx5 # line-out channel 3 <- AFX 5
python3 -m antelope.cli ... route lineout 4 mix2R # <- virtual mix 2, right
python3 -m antelope.cli ... route hp2 mute # mute every channel
python3 -m antelope.cli ... route lineout 6 mute # mute one channel
python3 -m antelope.cli ... matrix-status # LIVE read of the whole matrix from the device
python3 -m antelope.cli ... mix-status 1 # LIVE read of virtual Mix 1 (cat 0x04)
python3 -m antelope.cli ... readback 0x03 0 # raw: routing record for dest 0 (line out)
python3 -m antelope.cli ... readback 0x0b 0 # six Preamp-pair link flags
python3 -m antelope.cli ... readback 0x16 0 # structured mic-emulation state
python3 -m antelope.cli ... readback 0x19 0 # structured AFX strip order
python3 -m antelope.cli ... readback # list the readback categories
- Channel selector: 1-based (
route lineout 3 ...).L/R= 1/2 for the true stereo destinations (HP1/HP2/Mon A/Mon B) -- not Reamp, whose two outs are separate mono (route reamp 1/route reamp 2). - Source spec:
preampN(1-12),emumicN(mic-modeled preamp, numbered 5-12 by preamp like the Launcher),compplayN(Computer Playback;playbackNalias),adatN(1-16),afxN(1-32),surroundN(1-16),spdifL/R,mix1L…mix4R,oscN(1-2),mute, orkeep. - No un-route -- same as the Antelope Launcher: you replace a
channel's source or set it to
mute; there is no "empty" state. - Per-channel edits just work now. A per-channel
routereads the current group from the device first (category0x03), so you no longer have to seed the cache.route <dest> all <s1> <s2> …still sets the whole group at once; inall, a range token likecompplay1..16(orcompplay1-16, descendingadat16..1) expands to that many sequential sources. matrix-statusis a live device read of all 15 destination groups (line out, HP1/2, Mon A/B, Reamp, Comp Rec, ADAT out, S/PDIF out, AFX in, Mix Ch 1-4, Surround in). It falls back to the CLI cache only if the device is unreachable. The readback is the same0x74/0x75in-band protocol the Antelope Launcher uses --PROTOCOL.md§4a,frame.readbackin the profile.- All 15 destination groups are writable (2026-09-01):
line_out,hp1,hp2,mona,monb,reamp,com_rec,adat_out,spdif_out,afx_in,mix_ch1-mix_ch4,surround_in.spdif_outtakesL/Rlike the other stereo pairs. mix_chNis the virtual mixer's input side. Its 32 channels are what each of mix N's 32 strips listens to -- soroute mix_ch1 5 preamp3picks strip 5's source andmix-set 1 5 --fader -6sets that same strip's level. Two halves of one signal path.- Mind what you are pointing at:
com_recis what the computer records from the interface, andadat_out/spdif_outare physical digital outputs. Re-pointing those changes real signal flow, not just monitoring. Every write is read back and confirmed, so you will see exactly what landed.
See PROTOCOL.md §4a + §7 and frame.readback / frame.routing_command
in the profile.
The Mix windows are a separate UI from the routing matrix -- mixing
happens there, and each mix's L/R then shows up as a source in the
matrix (mix1L … mix4R). Decoded 2026-08 from
macos-mix1-send-pan-fader-mute-solo-link (opcode 0x17 / param 0xd4):
- one frame per
(mix 0-3, channel 1-32)strip, carrying fader (0 dB … −90 dB), pan (−30 … +30, centre0x20), mute ([21]bit 6), solo ([21]bit 7), and send (0 … 96, 0 = 0 dB and 96 = −∞). - soloing a channel makes the Launcher re-send all 32 strips (that's how we know each mix has 32 inputs).
- mix channel link =
SET_LINKwith a newspacebyte0x03(0 = Preamp, 1 = ADAT, 2 = S/PDIF, 3 = mixer); software-mirrored and scoped independently per mix. A link in Mix 1 never mirrors a Mix 2-4 pair. - not in the passive
0x73stream, but mixer state is readable via the0x74/0x75query protocol -- category0x04, index = mix number 0-3, fully decoded and hardware round-trip verified 2026-08-31.
Reading it back. mix-status is a live read of the device:
python3 -m antelope.cli mix-status # all four mixes
python3 -m antelope.cli mix-status 1 # just Mix 1Mix 1 (master + 32 strips)
ch fader pan send flags
mast 0 dB +0 96/96
1 -24 dB -30 0/96
2 0 dB +30 95/96
...
The record is 33 three-byte slots <fader> <pan|mute|solo> <send> -- the
same field order as the write frame -- and slot N is the strip the
write frame addresses as channel N. Verified by writing a distinctive
strip (mix 1 / ch 5 / fader 40 / pan +12 / mute / send 33 → 28 6c 21)
and reading exactly those bytes back at slot 5, with no other slot
touched. Slot 0 is the mix master, addressed as channel 0 by the
write frame -- confirmed by capturing the Launcher sweeping the Mix 1
master fader (68 frames, all d4 05 00 00 <fader> 20 60). See
PROTOCOL.md §4a.
Changing a strip. mix-set names the mix, the strip, and only the
fields you want to change:
python3 -m antelope.cli mix-set 1 5 --fader -24 --pan 10 --mute on
python3 -m antelope.cli mix-set 1 master --fader -6 # the mix master
python3 -m antelope.cli mix-set 2 12 --send 80 --solo off
python3 -m antelope.cli mix-set 1 5 # no flags = just show itMix 1 ch 5: fader 0 dB pan -30 send 95/96
-> ch 5: fader -24 dB pan +10 send 95/96 MUTE
verified: device readback matches
- The strip is read back first, so unnamed fields are kept. The write
frame carries the strip's whole state every time, so a naive
mix-setwould silently reset everything you didn't mention. This one reads category0x04, applies your flags, writes the result and re-reads to verify -- the same read-modify-writeroutedoes. There is no local cache. If the read fails it refuses to write rather than clobber blindly. --faderis dB, 0 (unity) to −90. The sign is optional: the mixer only attenuates, so24and-24both mean −24 dB.master(or0) addresses the mix master;1-32are the input strips. Same address space as the readback slots.- If other strips move too, it says so -- a linked pair or a solo re-muting the rest, both host-side behaviours the Launcher also has.
protocol.build_mix_command(profile, mix, channel, fader, pan_deg, send, mute, solo) builds the frame directly. See PROTOCOL.md §12 and
frame.mix_command.
⚠ Never query a readback index past a category's record count. The firmware does not bounds-check it:
category 0x04 index 5crashed the Orion Studio Synergy Core outright (Cortex-MBusFault_Handler, physical power cycle required). The CLI andtools/readback_enum.pynow refuse to go out of range. SeePROTOCOL.md§4a.
The front-panel EMU button on preamps 5-12 (8 channels; the
button shows per channel only in Mic mode) runs Antelope's
mic-emulation DSP (for their Edge Solo / Edge Duo / Edge Note modelling
mics). Same opcode 0x17 as the mixer -- [16] is 0xe5 instead of
0xd4. Decoded 2026-08-31: per preamp, an enable bit, a model id
(0 = EdgeDuo / raw, 1…18 = emulations), a channel-order swap
switch, and a polar pattern -- with model 0 a free 0-100 morph
(omni → cardioid → figure-8), with a selected model a 0-based index into
that model's pattern list (fixed / 3-way / 9-detent), preset to the
model's default on select. Enabling also auto-turns on 48 V phantom and
links the preamp pair; disabling reverses all of it (phantom off, gain
re-sync, unlink). Current state is available as eight records from readback
category 0x16 (target, model, swap, pattern). The write path still has no
verified write/readback round-trip, so the WebUI controls remain optimistic
while the readback diagnostics show the device response.
The modeled signal appears as routing source bank 0x01 (emumicN,
N = preamp 5-12). Preamps 5-12 confirmed on the wire 2026-09-03 (a
usbmon capture of the project's webUI driving EMU on preamp 5/6): the
0xe5 frame's channel byte is 0x00 for preamp 5 … 0x07 for preamp 12
(a -4 bias on the 0-based index), and each change is written to both
channels of the pair. The EMU button shows per channel only in Mic mode
(earlier work saw "7-12" because that unit had 5-6 in Line). A listening
test confirmed the taps carry the modeled sound and model selection
audibly changes it, matching the Launcher; emumic5 and emumic6 are
one mono signal for a mono emulation.
protocol.build_micmodeling_command(profile, channel, enabled, pattern, swap, model) builds the frame. The model list is account-bound (Edge
mics + model packs activate against an Antelope account), so it lives in
profiles/mic_models.json as one account's snapshot -- a client should
let the user pick by name. Not in the CLI yet.
Gazelle Reverb (the bundled reverb on the Mix 1 window; called AuraVerb
in the hardware/protocol findings) has its own frame -- opcode 0x1d /
param 0xda. Fully decoded (2026-08-31): byte 28 =
on/off, plus 8 DSP controls, each a plain 0-100 byte -- Room Size (@19),
Color (@20), Pre-Delay (@21, 0-100 → 0-32 ms), Early Reflection Gain
(@23), Late Reflection Delay (@24), Richness (@25), Reverb Time (@26),
Reverb Level (@27). Gazelle Reverb is bundled with the device (no per-plugin
activation), so it's in scope. Decoded + hardware round-trip verified
2026-09-03: it reads back via the 0x74/0x75 query protocol as
category 0x0a (one record, a block per mix), so the CLI now live-reads
the device and does a verified read-modify-write; the local cache is only
an offline fallback:
antelope-ctl ... auraverb # live Gazelle Reverb state (all 4 mixes)
antelope-ctl ... auraverb --on # enable Gazelle Reverb
antelope-ctl ... auraverb --reverb-time 55 --color 40 --room-size 70
antelope-ctl ... auraverb --off --defaults # reset params, disable
protocol.build_auraverb_command(profile, params, enabled) /
protocol.parse_auraverb_record. See PROTOCOL.md §12 / §4a and
frame.auraverb_command.
The SET_PARAM command frame has one byte (channel_offset) that means
different things depending on which param you're setting:
- For
gain/input_mode/phantom/phase_invert, it's a physical input index, 0-11. - For
bus_level/bus_dim/bus_mute/bus_mono, it's an output bus id instead. All 6 are now identified (2026-08):0=monitor A,1=headphone 1,2=headphone 2,3=line out,4=reamp,5=monitor B. Ids 3/4 turned out to be the settings-tab Line and Reamp output levels (not the headphone-3/4 that was previously guessed).bus_dim/bus_monowere restore-guaranteed live-round-tripped on all six ids on 2026-09-14, including Line and Reamp; bus 1 was temporarily moved off the raw-96 silent endpoint so its status marker could be tested.bus_muteis confirmed on 3 only. The Orion III has two physical reamp outputs (Reamp 1 / Reamp 2 -- separate mono outs for two guitar amps); bus 4 is one shared "Reamp" level slider, whether Reamp 1/2 have independent levels is untested.
Don't mix the two up -- set-gain 5 10 and set-bus-level 5 60 both use
"5" but mean completely different things (physical channel 6 vs. Monitor B).
Reading bus_mute back: the bus status byte's 0x04 bit is set both
by an explicit mute and whenever that bus sits at bus_level == 96
(the silent / maximum-attenuation endpoint) -- reproduced on buses 0, 3 and
4. A bus-status reader must check the level: at 96, 0x04 means "at the
silent endpoint", not necessarily muted.
Same discipline as Phase 1/2 -- capture, correlate against a real user action, confirm, only then trust it. The AntelopeAudio folder can provide a useful schema and shared-family hypothesis, but it does not establish another device's category bounds, field semantics, or transition behavior. Nothing is promoted from a sibling device on byte patterns alone.
- Capture a full, untruncated report -- see
CAPTURING.mdfor the step-by-step (tshark, usbmon, USBPcap, or Wireshark are all suitable raw capture paths; Wireshark's plain-text export truncates past ~110 bytes and hides anything past the first ~90 bytes of a 320-byte report). - In the official Launcher, change only the one thing you're investigating (e.g. routing), and note the state report just before and just after.
- Save those two 320-byte reports as hex dumps and run:
This prints exactly which offsets changed and how -- your candidate field for the new param. If you dumped a whole session instead (see
python3 tools/capture_diff.py before.hex after.hex \ --known-offset 49 --known-offset 61 # suppress already-explained bytesCAPTURING.md), usetools/scan_capture.pyon the.tsvinstead -- it walks every report in order and finds the transition(s) for you, so you don't have to hand-pick before/after frame numbers. - Also check what the Launcher actually SENT, not just what changed in
the readback. The state-report diff only tells you which bytes moved;
it doesn't tell you the opcode, param_id, or exact frame layout the
Launcher used to cause that. Filter the same capture for outgoing
URB_INTERRUPT outframes (magic0x70) and look at the opcode byte (offset 4) and the bytes afterparam_id_offset(offset 16) directly -- don't assume they land at the usualchannel_offset/value_offset(17/18).channel_linkturned out to use a different opcode (0x14, not the usual0x13) with its pair-index and enabled bytes shifted one position later (18/19) -- the state-report diff alone would never have revealed that shift, only the outgoing command bytes did. Talkback (2026-08) turned up a third opcode,0x12(frame.global_command): same header as SET_PARAM but for device-global params with no target, so the value byte sits at offset 17 (where SET_PARAM keeps its channel byte) and offset 18 is unused.talkback_button/_source/_gainall use it;talkback_dest_assignuses the ordinary0x13because it does take a target. Moral: check the opcode first, then figure out where the payload bytes actually are for that opcode. - Add an entry to
profiles/orion_studio_sc.jsonunder"params"with"status": "unconfirmed"and your candidate offset/id in"notes". - If it looks like the existing SET_PARAM(param_id, channel, value) shape,
test it deliberately with the CLI's escape hatch:
and confirm via another capture that only the expected offset moved, across a few different values, before calling it confirmed.
python3 -m antelope.cli --profile profiles/orion_studio_sc.json raw-set 0 0x53 10raw-setenforcesprofile["constraints"](target must be a valid index in some address space) and prints a hazard note for an unmappedparam_id----forcebypasses the bound. Never sweep opcodes or feed it values from an untrusted source: on the sibling Discrete 8 Pro, opcodes0x01/0x02and out-of-range indices wedged/BusFaulted the unit (seeprofile["hazards"]andPROTOCOL.md§14). - Don't assume a new feature fits the SET_PARAM shape. Seven opcodes
are known now (
0x12/0x13/0x14/0x17/0x1d/0x53/0xab), each with its own frame block under"frame"and its ownbuild_*_command()inprotocol.py. Routing (0x53/frame.routing_command) is the worked example: a distinct opcode whose payload is an array of(bank, index)pairs (one per output channel of the destination), not a fixed field -- and the first read of it (op bytes02 01/00 02) was wrong until a single-variable capture (macos-matrix-ch1-12-mute-hp1L/-hp1R) showed00 02was just the other channel's untouched routing. Confirm the shape from a capture that changes ONE thing; add a new"frame"block and builder rather than bending an existing one. - Once confirmed, flip
"status"to"confirmed", add real ranges/enums, and add a proper subcommand toantelope/cli.pyif it deserves one (or leave it onraw-setif it's rarely used).
Copy profiles/orion_studio_sc.json to profiles/<device>.json, reverse
its VID/PID and protocol from a fresh capture (don't assume it matches --
different product lines may use a different frame format entirely), and
point --profile at the new file. antelope/transport.py,
antelope/protocol.py, and antelope/cli.py should need zero changes if
the new device follows the same general "vendor HID, N-byte reports,
SET_PARAM(id, channel, value)" shape. If it doesn't, that's a sign the
frame format itself needs to become part of the profile too (it already
mostly is -- see "frame" in the JSON).
Every device profile also carries a versioned profile_schema, labels, and
features block. Read docs/profile-labeling.md
when adding or porting a profile so a future launcher (including Rust) can
reuse the same stable keys without copying device-specific wire facts.
Sibling profiles so far:
| profile | device | PID | state |
|---|---|---|---|
orion_studio_sc.json |
Orion Studio Synergy Core | 0xa221 |
reference; most complete |
discrete_8_pro_sc.json |
Discrete 8 Pro | 0xa2b5 |
peer-contributed |
zen_go_sc.json |
Zen Go Synergy Core | 0xa015 |
first pass from USBPcap/TUI evidence -- preamps / buses / sample rate / clock, two mixer layouts, q03 mixer-input mirrors, q0b/03 mixer links, and selector-gated mixer-strip lanes are mapped; DSP, mic-modelling, complete category bounds, physical meter ownership/calibration, and some routing roles remain open (see open_questions in the file) |
The family shares magic 0x70 command / opcode @4 / param_id @16 /
320-byte reports / HID interface 3 (EP 0x01 OUT, 0x82 IN) and most
param IDs. It does not share report magics, the mixer frame shape, or
source-bank numbers -- verify each against a capture, don't inherit.
The shared WebUI consumes those profile differences without copying Zen Go
values into the Orion path. For Zen Go, the profile-confirmed source controls
use the four mirrored routing records (destinations 6-9), the two mixer
readback layouts expose 16 strips with no master/send, 0x49 selects the
Monitor/HP1 or HP2 surface for the shared 16-lane meter bank, and a complete
q0b/03 bitmap can seed the visible mixer-pair links. These mappings remain
device-specific evidence; they are not defaults for other Antelope products.
This is a reference of what the published implementation does not claim to
have verified. It is not a development task tracker; the active Orion
completion queue is maintained locally in the ignored AUDIT.md.
See "status": "unconfirmed" entries in the profile, and
"unresolved_state_offsets". Nothing there is used by the CLI's normal
commands -- it's flagged so nobody mistakes "solved the MVP" for "solved
the whole protocol."
As of the 2026-08 vumeter session: all 12 physical inputs are confirmed
working through the CLI (set-mode/set-gain/set-phantom/set-invert),
and Hi-Z is hardware-limited to channels 1-4 (channels.hiz_channels in the
profile; set-mode <ch> hiz on ch 5-12 now refuses unless you pass --force).
As of the follow-up 2026-08 mona/monb/hp1/hp2/chlink captures:
-
Output buses are confirmed. Monitor A, Monitor B, Headphone 1, and Headphone 2 each have a
bus_level(0-96, 0/0x00=0dB maximum, 96/0x60=-inf/silent) plusbus_dim/bus_mute/bus_monobooleans, all readable back from a newbus_blockin the state report (seebus-status). Update (2026-08): bus ids3and4are the settings-tab Line and Reamp output levels -- confirmed insettings-linevol-mute-reampvol-togglevia fullbus_levelsweeps (readback offsets 37 and 40) plus abus_mutetoggle on bus 3. A restore-guaranteed self-test on 2026-09-14 also toggledbus_dimandbus_monoon all six bus ids, including Line and Reamp, and restored every flag and level. All 6 bus slots are now identified; see "Buses vs. channels". The endpoint meaning is device-confirmed: Monitor A reads raw0at maximum (0 dB) and raw96at minimum (-inf); the command/state byte itself is unchanged. -
Channel link syncs gain, phantom, AND phase_invert live -- but not mode.
set-link <channel> on/offsends a real, verified command (a distinct opcode/frame from everything else -- seeframe.link_commandin the profile), and all 6 pairs were individually confirmed in the capture. A follow-up capture (ch-link-gain-ph-inv-test, 2026-08) showed that once a pair is linked, changing gain, phantom, or phase_invert on either channel mirrors to the other channel immediately -- butinput_modedoes not, and the Launcher grays out the mode control while linked. The confirmed workflow to change mode on a linked pair is: unlink -> set mode independently per channel (gain resets to fit the new mode's range) -> re-link. This matches user reports and is expected device behavior, not a CLI bug. Still no dedicated link-enabled bit appears in the state report -- everything observed changing there is explained as a side effect of the gain/status sync above, not a standalone flag. The extracted Orion panel schema and manager-server log do identify readback category0x0bas five nested link tables: six Preamp pairs at index 0, eight ADAT pairs at index 1, one S/PDIF pair at index 2, mixer at index 3 and AFX at index 4. Input flags were independently correlated on 2026-09-30 and now drive their own WebUI buttons. Historical digital-button traffic used incorrect selectors; those writes did not establish the intended domain.Re-verified independently (2026-08) against the raw
all_reports_ch-link-gain-ph-inv-test.tsv: every report in that capture has first byte0x70,0x73, or0x75-- no other magic byte shows up anywhere in the session, so there's no hidden fourth report type carrying a link flag either. The capture toggled pair 0 (ch1&ch2) on/off/on/off (40x70/0x14/0xa2frames, pair_index always0x00); diffing the0x73state report from immediately before to immediately after each of those 4 commands shows zero changed bytes every time -- consistent with (not just "not yet found", but actively re-confirmed for this session's pair-0 toggles) there being no readback bit. Separately, the live gain-mirroring behavior itself is real and fast: sweeping ch0's gain while linked produces a matching ch1 state-report update about 8ms later, in lockstep, for the whole sweep -- this is whatset-link's new before/after check (below) leans on.set-linkstill does an indirect confirmation. The CLI has not yet adopted the confirmed0x0b:0flag for its own verification;set-link ... onsnapshots gain/phantom/phase_invert for both channels in the pair before and after sending the command. If they disagreed before and agree after, that's real (if indirect) evidence the link engaged, grounded in the confirmed mirroring behavior above -- not a guess. If they already agreed before (nothing to observe changing), or still disagree after, the CLI says so plainly instead of claiming confirmation it doesn't have.set-link ... offstill can't be confirmed this way (disengaging has no known observable effect on its own).statusnow shows a link indicator, e.g.:0 mic 0dB on off -. 1 mic 0dB on off -' (linked, pair 0 -- CLI-tracked, not device-confirmed)This is a small local cache (
~/.cache/antelope-ctl/link_state_<vid>_<pid>.json) of the lastset-linkcommand this CLI has sent for each pair. The newly mapped0x0btable is not yet a verified control-state source: the earlier whole- report diff did not query category0x0b, and the manager log mostly shows zero-valued responses. The cache therefore remains the safe fallback and can go stale if link state changes some other way (the Launcher, another instance, or a power cycle). Treat the markers as "what I last told the device" until the transition capture is done.Channel link is real, but the syncing you see isn't the device doing it (confirmed 2026-08, real hardware). Using an earlier version of this CLI to send only the raw SET_LINK frame did engage a real link flag -- visible on the Orion's own front-panel control, so the hardware itself really is tracking "linked" -- but each channel kept whatever mode/gain it already had. Linking ch1 (mic, 20dB) and ch2 (line, 10dB) through the Launcher app snaps ch2 to mic/20dB immediately; doing the exact same SET_LINK frame from the CLI alone did not. Re-examining the outgoing frames in
all_reports_ch-link-gain-ph-inv-test.tsvexplains why: every gain change made while linked in that capture shows two separate outgoingSET_PARAM(gain=0x50, channel, value)frames (channel 0, then channel 1, ~2ms apart, identical value) -- not one command with an observed two-channel effect. The "live sync" documented above is the Launcher software choosing to send two commands every time, not the device firmware fanning one write out to both channels. Cleanest proof (user, 2026-08): turning the physical gain wheel on a linked channel moves only that channel -- the wheel bypasses all host software, so the device plainly has no link-propagation logic, and the Launcher only mirrors changes it makes in its own UI.profiles/orion_studio_sc.json'sparams.channel_link.side_effectsand.live_sync_while_linkednotes have been updated to say this plainly, so nobody re-discovers it as a "CLI bug" later.cli.pynow replicates the Launcher's behavior itself:set-link ... onimmediately pushes the higher-numbered channel's mode/gain/phantom/phase_invert to match the lower one's (only sending a command for fields that actually differ, mode-before-gain-before-phantom to respect real constraints -- see_push_full_sync).- From then on, for as long as this CLI's local cache says the pair is
linked,
set-gain/set-phantom/set-inverton either channel also sends the same command to its partner (mirroring what the Launcher's two-frames-per-change behavior does).set-modeon a linked channel is refused (--forceto override) -- mode genuinely can't be changed on a linked pair on real hardware; the confirmed workflow is still unlink -> set mode independently -> re-link. - This mirroring is driven entirely by the local cache, not a device
fact. If you link a pair through the official Launcher instead of this
CLI, this CLI won't know and won't mirror -- use the new
mark-link <channel> on/offcommand to tell it (updates only the cache, sends nothing to the device) soset-gain/etc. mirror correctly afterward.
-
Resolved (2026-08; endpoint semantics confirmed on the device 2026-09-14): the "unexplained"
mutestatus bit that flipped on by itself whenbus_levelhit 96 in the monitor-A capture is real and reproducible --settings-linevol-mute-reampvol-togglesweeps buses 3 and 4 to the silent endpoint repeatedly and the0x04bit trackslevel == 96exactly, every pass. So the bit means "muted or at the silent endpoint"; a bus-status reader must special-caselevel == 96. The connected device was read at both endpoints: Monitor A reported raw 0 at maximum volume and raw 96 at minimum volume. Seeparams.bus_muteandunresolved_state_offsetsin the profile. -
A single-byte state-report change at offsets 17 and 19 (seen once in the original vumeter-test-ch1 session) turned up again in two later, otherwise-unrelated captures, always ~3.0-3.06s from the start of the capture and with no link/unlink action anywhere nearby -- this rules out "link-state readback" as the explanation and points instead to a periodic or connection-startup event. Still not deliberately isolated, so it stays in
unresolved_state_offsets.
As of the 2026-08 ADAT test (adat-ch1-2-3-12-link12 capture):
- ADAT gain is confirmed, including the outgoing param_id:
param_id 0x5b,channel_offset = adat_channel_index(0-indexed, ADAT ch1=0 ... ch16=15), range -6..+12 dB matching the UI. Readback lives atadat_gain_base_offset = 75, one byte per channel (confirmed for channels 1, 2, 3, and 12 in this capture).params.adat_gain.idis now0x5b-- distinct from physical-channel gain's0x50, as suspected. - ADAT UI link frames were captured, with pair indices encoded as
adat_channel_index // 2over the 16-channel space (eight pairs, indices 0-7). This confirms what the UI sent, not that the device engaged an ADAT link. The old interpretation of the six Preamp readback records as ADAT state was incorrect. The UI's paired gain writes show software mirroring, not device-side ADAT link behavior.
As of the follow-up 2026-08 ADAT-link1-2-7-8 capture (activate link1, link2, link7, link8 in order; sweep gain on ch1/ch3/ch13/ch15; deactivate link8, link7, link2, link1):
- Confirms the UI's pair-index encoding, not device ADAT link state.
link1/2/7/8 sent pair_index
0x00/0x01/0x06/0x07respectively, each followed by software gain-sync writes for the matching pair. ADAT has eight pairs; the six0x0b:0records belong to the six physical Preamp pairs and cannot confirm any ADAT pair. The captured ON/OFF frames show command traffic only; they do not verify an ADAT link took effect. - A momentary missed-sync frame was observed and is noted but not investigated further (per instruction): while sweeping ch1's gain down, one step (ch1 -> -6dB) has no matching mirrored command for ch2, which stayed at -4dB. Looked like a one-off dropped command on the wire rather than a parsing artifact. Worth another look if it recurs during real use.
- Historical ADAT link mapping error (superseded 2026-09-30). The
physical-pair-0-ON frame (
ch-link-on-off.pcapng) and the ADAT-pair-0-ON frame (ADAT-link1-2-7-8.pcapng) were compared byte-for-byte across all 320 bytes plus the USB metadata (endpoint0x01, device address 2, interface, direction): zero differences. The six0x0b:0records are the six physical Preamp pairs, covering the device's 12 physical inputs. ADAT has eight pairs and is not reported by that table. The earlier project mapping treated the Preamp table as ADAT readback and was incorrect. Identical wire frames alone do not establish that an ADAT link was engaged or that one command affects both domains.
Three captures (talkback-bttn, talkback-select, talkback-gain-int-ch1-2-12)
pinned down the whole talkback feature. It is protocol-confirmed but
deliberately not wired into cli.py yet (protocol-first; add commands
later if wanted). Full details in profiles/orion_studio_sc.json --
frame.global_command, state_report.talkback_block, and the four
talkback_* params. Summary:
| Control | opcode / param | payload | readback (0x73 report) |
|---|---|---|---|
| Talkback button (hold-to-talk) | 0x12 / 0x1f |
value@17: 1=press, 0=release | offset 73, bit 6 (0x40) |
| Talkback source | 0x12 / 0x27 |
index@17: 0 = INT (built-in talkback mic, the one behind the unit's physical TB button), 1-12 = preamps 1-12 | offset 73, bits 0-1 only (source & 3) |
| Talkback gain (per source) | 0x12 / 0x20 |
value@17: raw 0-96 (dB direction not independently confirmed) | offset 74 (selected source's gain) |
| Talkback destination assign | 0x13 / 0x5d |
dest 0-3 @17, on/off @18 | offset 73, bits 2-5 (dest N = bit N+2) |
Key takeaways:
- New opcode
0x12(global_command) -- for global params with no target; value goes at offset 17, not 18. See "Adding a new param" above. - Talkback gain is per-source. Select a source, then set its gain; offset 74 always shows the currently selected source's value.
- Source readback is incomplete -- only the low 2 bits of the source index appear (offset 73 bits 0-1). Track the full 0-12 value client-side.
- Untested overlap: offset-73 bits 2-3 are shared between the source index (values 4-12) and the dest-assign bitmask (dest0/dest1). The three captures never set a high source index and a destination at the same time, so how the packed byte behaves with talkback fully configured is still unverified -- one combined capture would close this.
- Destinations = Monitor A / Monitor B / HP1 / HP2 (user-confirmed), four toggle buttons in the Monitors/Headphones menu -- not routing- matrix sources. They combine: talkback can go to all four at once (the bitmask holds multiple bits). Exact index→name order (0-3) isn't pinned down; likely 0=Mon A, 1=Mon B, 2=HP1, 3=HP2.
settings-trim-mona-monb-line-panlaw pinned down the settings-tab output
trim selectors: SET_PARAM(0x4b, target, value), target 0/1/2 =
Monitor A / Monitor B / Line Out, value a 7-position selector. It's an
output-reference-level selector, 1 dBu per step: 0 = 20 dBu … 6 = 14 dBu
(user-confirmed against the Launcher). Readback is packed into two bytes
just before bus_block: target 0 at offset 24 (value << 4), target 1 at
offset 25 bits 2-4 (value << 2), target 2 at offset 25 bits 5-7
(value << 5). Re-confirmed live on the device 2026-09-03 -- swept
all three targets, the readback tracked value << shift exactly, restored.
See params.output_trim and state_report.output_trim_block.
- Pan law — DECODED 2026-09-03 (
panning-law-6-3-45-0): it is not0x4btarget 3 (ruled out) but its ownSET_GLOBALparam0x24, value0=-6 dB1=-3 dB2=-4.5 dB3=0 dB (Launcher button order). No device readback. CLIpan-law [--set N]. - Readback category
0x16is the mic-emulation state table: one outer record containing eight{target, emu_model, ch_swap, pattern}records. It is unrelated to trim and pan law; the CLI/WebUI expose it as a read-only structured response.
clock-source-WC-adat-adatx2-adatx4-spdif-usb-oven: SET_GLOBAL param
0x04, value 0=Oven (internal) 1=Word Clock 2=ADAT 3=ADAT x2
4=ADAT x4 5=S/PDIF 6=USB. Readback = 0x73 offset 19, 1:1 —
which also finally explains the "0x73 @19 blip ~3 s after connect" seen
in every capture (the device re-locking from USB to its saved source at
startup). CLI clock-source [--set N].
The VM capture showed nothing for the brightness slider, which had it
filed as host-side. Wrong -- the VM Launcher just no-ops the slider.
Recaptured on native macOS (macos-scrbrght-0-100-50-multvalue) with the
device's physical screen visibly changing:
- Command: opcode
0x12(global), param0x0e, value 0-100 (0x00-0x64) at payload offset 17. - Readback:
0x73state report offset 26 = the value, exactly, on all 25 commands. - CLI:
set-brightness <0-100>(viaprotocol.build_global_command, the first SET_GLOBAL builder -- talkback params can use it next). Hardware-confirmed: the CLI command changes the device's physical screen. Seeparams.screen_brightness.
Lesson: "zero frames under the VM" ≠ host-side. The VM Launcher silently drops some controls. Re-check on native macOS before concluding.
macos-smplrt-32k-44k1-48-88k2-96k-176k4-192k: stepping every rate sent
opcode 0x12 (global), param 0x03, an index 0-6 at offset 17
(0 = 32000, 1 = 44100, 2 = 48000, 3 = 88200, 4 = 96000, 5 = 176400,
6 = 192000). Readback is 0x73 offset 18, tracking ~1 s behind each
command (clock re-lock). CLI: sample-rate / set-sample-rate <hz>
(params.sample_rate). Hardware round-trip verified 2026-09-04 across
every rate 44.1k-192k. Offsets 21-23 are the same rate in Hz (24-bit
big-endian) and 27 is the rate family (0x10 >> [21]: base / 2x / 4x)
-- both decoded in that sweep; sample-rate prints the measured Hz too.
- Oscillator / test-tone generator -- the settings-tab panel is
SET_GLOBAL(0x12), param0x0a, one packed byte @17:0x01/0x04= osc 1/2 frequency, bits 4-5 (0x30) = level (0 / -6 / -12 / -18 dBFS),0x40/0x80= osc 1/2 mute. The earlier "sends zero frames" was an inbound-only capture. Separately, inserting an oscillator into an output is the0x53routing frame, source bank0x0c(params.oscillator,params.routing). - DC-coupling --
SET_GLOBAL(0x12), param0x26, value 0/1. A live WebUI check on 2026-09-14 confirmed device readback at0x73byte 93 bit 0: Off -> On moved0x00 -> 0x01, and On -> Off restored0x00. Talkback fast/normal/safe latency modes still send nothing (host-side) -- a trustworthy negative, since the same capture carried DC-coupling's OUT frames. Thunderbolt/buffer settings are probably host driver only, but that rests on a capture that cannot be trusted -- seePROTOCOL.md§11. - Surround monitoring tab -- two whole-state frames:
0xab/0xeb= global (EQ pre/post[18]bit 7, level, delay, format, per-speaker bypass/mute/dim masks, 2.0/2.1 bass-management window) and0x87/0xea= per-speaker ×16 ([18]= speaker 0-15, level + polarity, delay, a full 16-band parametric EQ per speaker). Decoded 2026-09-03 from thesrrnd-*captures. Both read back (found 2026-09-04): global = readback category0x1b, per-speaker EQ = category0x1a. The CLI decodes both readbacks, and the WebUI Surround tab polls both categories. Its 2.0/2.1 global delay/level and format paths perform a fresh read-modify-write; EQ PRE/POST is a confirmed one-bit global write. Mute and dim remain read-only. Speaker-monitor bypass and the per-speaker phase, delay, and level fields were hardware round-tripped on speakers 0 and 1 against fresh readbacks and restored exactly. The WebUI exposes those per-speaker controls alongside the EQ head controls. EQ knobs write one field at a time through a fresh read-modify-write; direct graph drags use the bounded/api/surround/eq/pointpath to update frequency and gain together in one complete record. Both paths preserve the rest of the state and perform a post-write readback. The graph supports damped point dragging and Q adjustment by wheel only while the pointer is over a point. The 2.0 Bass Management popup is writable for its L/R strips using the same bounded fresh-readback path as 2.1's L/R/LFE strips.tools/surround_eq_selftest.pycan read all 16 records or, with explicit confirmation, probe one frequency, Q, gain, or raw mode byte in one selected band and restore the complete record. The per-speaker head mapping is now confirmed by the combined delay/level/phase readback probe. Room Correction turned out to be just the Launcher computing a curve host-side and writing it into that0x87per-speaker EQ -- no opcode, no toggle (params.surround_speakeris the RC interface). The global format wire path was subsequently round-tripped through 9.1.6 on the connected Orion Studio III; the WebUI still exposes only 2.0 / 2.1 until the licence-dependent vendor behavior is better understood. The Bass Management popup displays only the active strips; 2.1 is ordered L · R · LFE with fixed strip widths. Faders use the profile dB range for a reload-safe percentage position, retain the supplied artwork, accept exact values by double-click, and keep their travel inset from the readout and Mute/Solo row. Its bounded writes cover crossover cutoffs, filter order, bypass, fader, mute, filter type, link, and solo. Each field was toggled in both 2.0 and 2.1 and matched the immediate category-0x1b readback; distinct fader values also survived the 2.0/2.1 transitions. Seeparams.surround_monitor+params.surround_speaker.
For a targeted hardware probe, stop the WebUI first so it releases the HID device, then run the dedicated self-test. It reads all speakers by default:
python3 tools/surround_eq_selftest.py
Write mode requires one speaker, one band, one field, an explicit value, and
an acknowledgement of the 0x87 write path. It verifies the readback
and restores the complete record:
python3 tools/surround_eq_selftest.py --write --speaker 0 --band 3 --parameter gain --value -1.00 --confirm-experimental-write
Frequency and Q use Hz and the Q value shown by the UI; gain uses dB. Mode is accepted only as a raw byte because its end-band labels are not fully proven.
The global EQ position can be independently read and round-tripped with:
python3 tools/surround_eq_position_selftest.py --write --confirm-experimental-write
The probe toggles PRE/POST, verifies category 0x1b, and restores the original
complete global state.
The combined Surround control probe exercises 2.0/2.1 Bass Management faders, filter type, Link, Solo, and speaker delay/level/phase/bypass, then restores the saved global and speaker records:
python3 tools/surround_format_selftest.py --write \
--test-bass-controls --test-speaker-controls \
--confirm-format-write --confirm-surround-control-write
Four native-macOS captures of the Launcher connecting to the device
(captures/macos-captures/macos-antelopeINIT-*): device powered fully
off, Launcher quit, start recording, launch Launcher, power device on.
Two of them (...poweroff-on2-itsavedstate, ...poweroff-on3-itsavedstate)
were recorded with deliberately different LineOut routing -- preamp
1-12 vs computer-playback, swapped between the two.
- The whole host->device connect handshake is one frame:
SET_PARAM(param 0x49, channel 1, value 0). Present in all four macOS captures and in the older WindowsAntelopeINIT.tsv-- cross-platform confirmed. The Launcher also sends the0x74topology/readback query burst; the device answers those queries with0x75responses while normal0x73/0x75polling continues. Inon2the Launcher also sent a shortSET_PARAM(gain 0x50)sequence -- the user nudging gain sliders to force the buggy Launcher to flush state, not handshake or device behaviour. - The routing readback is present at connect. The
0x74routing queries are identical between the on2/on3 captures, but their corresponding0x75category-0x03responses contain the deliberately different routes. Earlier analysis compared the requests and filtered the responses as meter traffic. The USB descriptors are identical, the final0x73state report differs only in one preamp-gain byte and meter noise, and there is no0x53frame in either direction. So the readback fires later -- prime suspect: opening the routing-matrix tab (these captures only watched the connect). - macOS capture format: Darwin "XHC" pcapng wraps each 320-byte
vendor HID report in a 40-byte pseudo-header (
frame.len == 360); payload byte 0 is the usual magic.tools/scan_capture.pyonly reads the Windows TSV layout -- usetools/scan_macos_capture.pyfor these (seeCAPTURING.md).
Physical-input raw meters use 0x73 full-report bytes 221–232. Their
dBFS calibration and separate clip indication remain unknown; the old
0x75 curve below does not apply to this bank.
Mixer windows expose a selector-gated 32-strip meter bank. The live
restore-safe test confirmed all four mixer-window selections:
SET_PARAM(0x49, target 1, value 0..3) gates 0x73 @157..188 (strip N is
@156+N), while the separate Meters-window selections 21..24 gate a second
copy at @125..156 (strip N is @124+N). The selectors read back at
0x73[122] and [121] respectively.
Surround meters are now mapped. A restore-safe direct-HID sweep temporarily
selected the already-probed 9.1.6 layout, routed Oscillator 1 directly to each
of the 16 Surround In channels, and read only free-running 0x75 reports
with byte [1] = 0x1f. Both selector 25 (Surround In) and selector 9
(Surround Out) expose the same primary one-byte bank: Surround channel N is
at full-report offset @33 + N, or @34..@49 for channels 1..16. Raw 96
is silence and active test samples reached approximately raw 18; this is
not yet a calibrated dB scale. The 0x73 virtual-mixer meters were excluded.
Weaker changes around @50..@52 remain unresolved auxiliary candidates and
are not part of the per-channel map. The original format, routing, selector,
and oscillator mute state were restored after the test.
The physical Preamp 1 tone was observed at 0x73 @221 (raw 18, reaching 0
at louder moments); the other physical preamps, emuMic, Computer Playback,
ADAT In, and S/PDIF In still need their corresponding live sources. A
separate top-level-tab capture contains only device-to-host reports: no HID
OUT command, no 0x49 selector transition, and fixed selector bytes
0x73[121]=22, [122]=0. It therefore does not prove that ADAT metering is
independent; ADAT input must be tested with a signal, ideally in a capture
that includes both OUT and IN traffic. See PROTOCOL.md §9 and the profile.
The live route tests for Line Out, HP1, HP2, Monitor A/B, Reamp, ADAT Out,
and S/PDIF Out found no stable meter lane in the HID reports; host-side
isochronous-audio metering remains possible. This is distinct from the
mixer-window selector at [122].
The meter report (magic 0x75) originally had one candidate offset:
frame.meter_report.channel_meter_base_offset = 32, one byte per channel
(same ordering as gain/status), scale inverted -- resting near 0x60 (96)
and falling toward 0x00 as the input gets louder. That came from a single
correlated capture (scream into ch1 while it was hot, watch byte 32 dive to
0 and climb back), not the capture-correlate-confirm-repeat discipline the
rest of the profile follows.
As of the 2026-08 vumeter-sinewave session, channel 0's offset and scale are
confirmed, with a real raw-byte -> dBFS calibration
(frame.meter_report.db_curve) to go with it. Method: feed a steady 1kHz
sine into ch0 (a tone sweeps more predictably than white noise) and sweep
gain down from its max instead of touching the source level, watching the
Launcher's own on-screen meter. The useful trick: click phase_invert
on/off as a marker after each reference point -- it's a real SET_PARAM
command, so it shows up as an unambiguous, precisely-timestamped
status_base_offset bit flip in the very same state-report capture, with no
need for a second outgoing-frame capture just to bookmark timestamps. Two
swipes were done this way: one by color (clip, then where orange stops
appearing, then where yellow stops appearing = -12dB) and one by absolute dB
steps read off the Launcher meter (clip, -10, -20, -30, -40, -60). The
reconstructed raw meter byte at each marker tracks the target dB exactly
1:1, raw == -dB, across the whole sweep from 0 to -60 -- see db_curve and
its notes for the full point list and one leftover oddity (a stray marker
click at raw=72 that doesn't fit either swipe's sequence or the line at
all, presumably an incidental click made while resetting gain between
swipes).
The same session also pinned down the meter LED color thresholds in
frame.meter_report.led_scale: yellow/green at exactly -12dB (both stated
directly and matching the measured raw=12 point), and orange/yellow at
-4dB (matching two independent estimates -- the captured marker's raw=4, and
the user's own LED-segment count). There's no separate solid-red band below
clip -- orange runs straight through to 0dB, and red only appears at clip
itself.
All 12 channels' meter offsets are now confirmed working (2026-08 -- each
channel's meter tracks its own signal correctly). db_curve is still from
the channel-0-only sweep above and is applied to every channel for lack of
a per-channel one -- reasonable estimate, not independently verified past
channel 0. Don't build anything on the deep-silence tail past -60dB either
-- it isn't swept yet, and the stray raw=72 point above hints it may not
stay linear that far down. Try python3 -m antelope.cli --profile profiles/orion_studio_sc.json meter while making noise into a channel to see
it live.
Capture format matters. Wireshark's plain-text/"Copy as Text" export
only exposes ~111 of each 320-byte report's bytes, which is why early raw-byte
meter claims remained unconfirmed. Always use tshark -x, or better, the
whole-session -T fields dump described in CAPTURING.md (that's what all
of the mona/monb/hp1/hp2/chlink captures used, and it's also what let
tools/scan_capture.py find every transition automatically instead of
hand-picking frame numbers).