Control a Neural DSP Quad Cortex from Python, over USB.
pyquadcortex is a USB client for the Quad Cortex, in the same way Cortex
Control is a USB client for it: it connects over the cable and speaks the
protocol the device already speaks. Nothing on the unit is modified, unlocked,
or jailbroken - no custom firmware, no SD card surgery, no developer mode. Plug
in the USB cable and the device answers.
Recall and edit presets, build scenes, place and route blocks, drive the I/O and the Global EQ, browse the unit's own catalogue of blocks and Neural Captures, and manage setlists - the operations you would do in Cortex Control or on the touchscreen, from a script. What is and is not covered is listed feature by feature in docs/manual-coverage.md.
The library imports as pyquadcortex; a command-line tool named qcctl comes
with it.
Unofficial and not affiliated with, endorsed by, or supported by Neural DSP Technologies. "Quad Cortex" and "Neural DSP" are trademarks of their owner and are used here only to describe what this software talks to.
Working, and verified on real hardware on macOS (Apple Silicon) and Windows,
against CorOS / Cortex Control 4.0.1 (device firmware d14e). Every
operation listed below has been exercised on a physical unit. The test suite runs
fully offline, with no device attached.
The device protocol carries no version of its own, so a future CorOS update could change it. If you are on a newer version and something misbehaves, that is the first thing to suspect - see docs/protocol.md.
pip install pyquadcortexYou also need the hidapi C library, which is what actually opens the USB device:
- macOS:
brew install hidapi - Debian/Ubuntu:
sudo apt install libhidapi-hidraw0 - Windows: included; nothing to do.
Python 3.11 or newer.
macOS note: the
hidpackage looks forlibhidapiby bare name, and Homebrew's library directory is not on the default search path. Prefix commands that talk to the device withDYLD_LIBRARY_PATH=/opt/homebrew/lib.
Quit Cortex Control first. It holds the USB interface exclusively, so while it is running nothing else can talk to the device. (Wi-Fi can stay on, it makes no difference. The Quad Cortex just has to be plugged in over USB.)
Everything here uses the factory library, so it works on any unit.
import pyquadcortex
from pyquadcortex import Input, Instrument, Scene, Setlist
with pyquadcortex.connect() as qc:
# What are we talking to?
print(qc.version().app_fw_version)
# What is in the factory library?
for entry in qc.list_presets(Setlist.FACTORY)[:5]:
print(entry.name)
# Recall a preset by the name shown on the unit, then switch scenes.
amp = qc.find_preset("Brit 2203", Setlist.FACTORY)
qc.recall_preset(Setlist.FACTORY, amp.index)
qc.switch_scene(Scene.B)
# Re-point a preset's input to Return 1 and save it as your own.
# (Pick an empty slot; this overwrites whatever is in it.)
bass = qc.find_preset("Cali Basswalk", Setlist.FACTORY)
preset = qc.read_preset(Setlist.FACTORY, bass.index)
qc.reroute_grid_input(preset, Input.RETURN_1)
qc.save_current_preset(Setlist.USER, "30A", "Cali Basswalk [Ret1]",
instrument=Instrument.BASS)connect() finds the device, opens it, and completes the handshake the device
requires, so what you get back is ready to use. As a context manager it also
releases the device when the block ends; otherwise call qc.close().
Closing tells the device the client is leaving, which is what Cortex Control does on
quit. If you supplied your own transport and so own teardown yourself, send it with
qc.disconnect() before you tear down.
Presets are addressed by name via find_preset(), or directly by the slot name
shown on the unit ("30A"), or by linear index. Scenes, inputs, outputs, and
instrument tags all have readable names, so nothing here is a bare number.
Four things worth knowing about, each with the full detail in docs/api.md.
Blocks, parameters, routing and bypass, addressed by row and column. Rows are zero-based here and 1 to 4 on screen, which is the single easiest thing to get wrong - an edit to the wrong row still succeeds and still reads back correctly.
row = free_rows(preset)[0] # not just "a row with no blocks"
qc.set_block(row=row, column=0, model=models.BassAmplifier.AMPED_FLIP_TOP_6464)
qc.set_chain_input(row=row, in_portid=Input.INPUT_2)
qc.set_chain_output(row=row, out_portid=Output.MULTIPLE) # required, not optional
qc.set_param(row=row, column=0, param="MASTER", real=5.0, model=amp)
qc.save_current_preset(Setlist.USER, "30A", "Bass on In 2")An edit persists only through this kind of row/column-keyed write followed by a save. Writing a whole preset back does nothing.
Scenes are the Quad Cortex's signature feature, and most factory presets build them from per-scene parameter VALUES rather than from bypass. Name a scene and the value belongs to that scene alone.
qc.set_param(row=0, column=5, param="MIX", value=0.8, scene=Scene.C)
qc.set_lane_output(row=0, param="VOLUME", value=0.0, scene=Scene.E) # a silent scene
qc.set_bypass(row=0, column=2, bypassed=True, scene=Scene.B)I/O port levels and ground lift, the Global EQ, footswitch modes, the tuner, the Looper's state, master volume, Gig View, and most of the Device Settings menu.
qc.set_input_port(1, level=0.4) # global: no save, nothing to undo it
qc.set_global_eq(band=3, gain=0.75, filter_type=GlobalEQFilter.PEAK)
qc.set_scene_bypass_behavior(SceneBypassBehavior.ALWAYS_OVERWRITE)Read Reading global settings safely first - a read straight after a write can return the old value, which looks exactly like a refusal.
The device tells you its own contents: every block it has, every preset, every folder, and over two thousand Neural Captures.
qc.catalog[21005].name # '212 Darkglass Neo (M)'
qc.list_presets(Setlist.FACTORY) # or any folder key at all
qc.list_folders() # 399 of them on the observed unit
qc.captures() # the Neural Capture libraryFive runnable examples are in examples/, with
examples/readme.md describing each. Start with
inspect_preset.py - it only reads, and it prints what a
preset actually contains.
The ones that write are dry runs unless you pass --write, and they name the slot they
would overwrite.
If the device disappears mid-session with DeviceNotFoundError, see
docs/troubleshooting.md.
If all you need is to change presets or scenes, you may not need this library at all - the unit accepts MIDI over USB and DIN: bank select plus program change for presets, CC#43 for scenes, CC#35-42 for the footswitches. This library exists for everything MIDI cannot reach.
qcctl covers the common one-off actions (on macOS, with the
DYLD_LIBRARY_PATH prefix from above):
qcctl version
qcctl recall --slot 28C
qcctl scene --index 3
qcctl dump-preset --slot 28C- docs/api.md - everything the library can do, grouped by what it touches. The reference to the introduction above.
- docs/manual-coverage.md - every feature the Quad Cortex manual describes, against what this library covers. Read this to find out whether your case is supported before you start.
- docs/protocol.md - how the device's USB protocol works: framing, the connect handshake, each operation, and what has been verified.
- docs/architecture.md - how this library is put together, and how to add support for something it does not do yet.
- docs/capture.md - how to read the device's own traffic when you need a message shape this library does not implement yet.
- docs/roadmap.md - where this is meant to go, including the object model of the device that should eventually hide the protocol's rough edges entirely.
- changelog.md - what changed between released versions.
- docs/releasing.md - the release checklist, and why each step exists.
- docs/troubleshooting.md - when the device stops answering.
- contributing.md - development setup and how to submit a change.
Inspired by OpenCortex.
MIT.