MatchPatch keeps device support in the source tree. Adding a device should be a small, explicit change:
- Add a new package under
src/matchpatch/devices/. - Implement a
DeviceProfile,PatchFileHandler, and, when needed, aDeviceController. - Add one instance of the profile to
DEVICE_PROFILESinsrc/matchpatch/devices/available.py. - Add focused tests for the profile, file handler, and any GUI behavior.
The in-tree demo device in src/matchpatch/devices/demo/ is the reference
implementation for developers. It is deterministic, offline, heavily commented,
and intentionally simple. Its .demobank JSON format is fake example data, not
a recommendation for real processor file formats.
Each device gets its own sibling directory:
src/matchpatch/devices/
available.py
base.py
demo/
__init__.py
helix/
__init__.py
file_ops.py
preset_handling.py
line6/
common.py
file_ops.py
helix/
__init__.py
podgo/
__init__.py
preset_handling.py
For a new device named my_device, create:
src/matchpatch/devices/my_device/
__init__.py
Large devices can split helpers into additional files inside that directory. Keep device-specific parsing, subprocess adapters, SDK wrappers, and steering code inside the device package.
Register built-in devices in exactly one place:
# src/matchpatch/devices/available.py
from matchpatch.devices.my_device import MyDeviceProfile
DEVICE_PROFILES = (
HelixDeviceProfile(),
PodGoDeviceProfile(),
DemoDeviceProfile(),
MyDeviceProfile(),
)The registry validates the list and exposes get_device_profile(name) and
list_device_profiles(). Built-in Line 6 devices currently include helix and
podgo. Device names must be unique, non-empty strings. The GUI, CLI, config
loader, workflows, diagnostics, and file-operation helpers all use that same
registry, so adding the profile instance makes the device selectable everywhere.
DeviceProfile describes device-wide behavior. The demo profile shows the
minimum shape:
class DemoDeviceProfile(DeviceProfile):
name = "demo-device"
display_name = "Demo Device"
snapshot_count = 2
max_snapshot_count = 8
def create_patch_file_handler(self, project_dir: Path) -> PatchFileHandler:
return DemoPatchFileHandler()
def default_audio_routing(self) -> AudioRouting:
return AudioRouting(None, 48000, (1, 2), (1, 2))
def default_steering_options(self) -> SteeringOptions:
return SteeringOptions(None, 1, 0.0, 0.0, 0.0)
def create_controller(self, options: SteeringOptions) -> DeviceController:
return DemoController()Required methods:
create_patch_file_handler(project_dir)returns a fresh file adapter.default_audio_routing()returns audio defaults. Channel mappings are one-based stereo pairs.default_steering_options()returns target-selection defaults. MIDI channels are zero-based.create_controller(options)returns a controller for hardware-style target and subdivision activation.
Useful optional overrides:
terminology()changes words such as device, preset, snapshot, and setlist.file_capabilities()advertises device-level file operations.measurement_backends()declares supported measurement modes.audio_transport_factories()adds custom device audio transports.diagnostics_provider()adds preflight checks.supports_normalization()andnormalization_unavailable_message()let example, inspection-only, or file-operation-only devices appear in the GUI without starting normalization.naming_rules()validates and sanitizes device-owned names.setting_descriptors()defines GUI-free settings.format_patch_id()formats numeric preset IDs for status text and CSVs.
The demo device intentionally returns False from supports_normalization().
When selected in the GUI, pressing Normalize shows a short message explaining
that Demo Device is only an example and cannot normalize files.
PatchFileHandler owns device files. MatchPatch calls it for path validation,
target discovery, selector parsing, measurement-file creation, adjustment
writes, and file-operation workflows.
Required methods:
validate_input(input_path)checks source files.validate_output(input_path, output_path)checks destination files.list_assignments(input_path)returns legacy numericPatchAssignmentvalues.parse_patch_set(value)parses numeric preset selectors.select_preset_ids(input_path, assignments, requested_ids)resolves legacy numeric preset selections.format_patch_id(preset_id)formats numeric IDs.create_measurement_file(input_path, output_path)writes a measurement variant.apply_analysis_csv(...)writes normalized output from MatchPatch analysis.automation_output_path(input_path, postfix)builds device-compatible sibling output paths.
The demo handler implements these methods for a small JSON file. Real devices should use structured parsing for their actual file format and should preserve unknown or unrelated fields when writing adjusted files.
Modern devices should implement list_targets() in addition to
list_assignments(). This avoids forcing every processor into Helix-style
numeric preset IDs.
MeasurementTarget models a measurable top-level item. IDs may be integers or
strings. Targets include a display label, zero-based index, name, optional
source filename, optional numeric compatibility ID, target-level gain points,
and subdivisions.
MeasurementSubdivision models snapshots, scenes, channels, layers, or any
within-target concept. The demo device maps presets to targets and scenes to
subdivisions:
MeasurementTarget(
id="preset:clean",
display_label="D001",
index=0,
name="Clean",
subdivisions=(
MeasurementSubdivision(
id="scene:intro",
display_label="Intro",
index=0,
name="Intro",
gain_points=(GainPoint(...),),
),
),
compat_numeric_id=1,
)Use GainPoint for adjustable device controls. Set scope="target" for
target-level controls and scope="subdivision" for per-scene or per-snapshot
controls. Override apply_gain_adjustments(input_path, output_path, adjustments) when the device can apply explicit gain changes. The demo device
supports one subdivision gain point named main-output and changes only a
scene's output_level_db, leaving unrelated JSON data intact.
File metadata drives GUI filters, validation, and file-operation commands.
file_types() returns user-facing extensions:
DeviceFileType(
kind="setlist",
extensions=(".demobank",),
description="Demo Device Banks",
)file_kind(path) classifies paths as "preset", "setlist", or "unknown".
file_capabilities() returns FileOperationCapabilities:
reads_preset_filesandwrites_preset_files;reads_setlist_filesandwrites_setlist_files;joins_presets_to_setlist;splits_setlist_to_presets;replaces_setlist_slots;exports_selected_setlist_slots.
Only advertise operations the handler really implements. Unsupported optional
operations should keep the base behavior, which raises NotImplementedError.
DeviceSettingDescriptor is the device settings API. MatchPatch uses
descriptors for config defaults, CLI flags, validation, diagnostics, and the
generic GUI settings panel.
Descriptor fields include:
name,scope, andkind;default;config_path;cli_flags;labelandhelp;choices,minimum,maximum, andrequired.show_in_gui, which defaults toTrue.
Supported kinds are string, integer, float, boolean, choice, path,
and channel_mapping. The base validate_settings() checks required settings,
types, numeric ranges, choices, and one-based channel mappings. Unknown settings
are ignored so config files can carry values for code that has not loaded them.
The base DeviceProfile.setting_descriptors() already exposes common audio and
steering settings. Override it when a device needs different defaults or
device-specific settings. The demo adds a demo_mode choice setting, and the
GUI uses the generic DescriptorSettingsPanel for it automatically. The demo
also marks preset_wait, snapshot_wait, and measurement_wait with
show_in_gui=False: those settings remain available to config, CLI, and
normalization plumbing, but they are hidden from the device panel because the
GUI already exposes them in the timing tab.
If a device needs a bespoke GUI, add it directly to
src/matchpatch/gui/device_panels.py and keep device-specific widgets small.
Prefer descriptors whenever possible.
Validation happens in layers:
- registry validation checks profile names, display names, and handler creation;
validate_settings(settings)validates descriptors;validate_input()andvalidate_output()reject unsupported paths before writing;naming_rules()can enforce device name limits and allowed characters.
For preflight checks, return a DiagnosticsProvider from
diagnostics_provider(). MatchPatch passes a DiagnosticsContext containing
the request, profile, handler, resolved settings, and project directory. Provider
exceptions are caught and reported as failed diagnostics.
Most hardware-style devices can use the built-in hardware, loopback, or
simulated backends. Devices that need custom processing can return factories
from audio_transport_factories().
An AudioTransportFactory exposes:
capabilities, anAudioTransportCapabilitiesobject;supports(mode, settings);create(context).
Created transports implement process(reference_audio) for real-time style
measurement or process_offline(request) for offline rendering. If your device
expects a custom mode such as offline, include that mode in
measurement_backends().
Keep new device tests deterministic and offline unless hardware coverage is
explicitly required. Follow tests/test_devices.py and
tests/test_gui_settings_renderer.py:
- assert the profile is listed by the static registry;
- write a small fixture in a temporary directory;
- verify
list_targets(),list_gain_points(), and selectors; - verify adjustment writes mutate only the intended fields;
- verify unsupported capabilities fail clearly;
- verify GUI selection when the device should appear in the main window;
- add focused parser tests for any real file format helpers.
Useful checks while developing device-facing changes:
UV_CACHE_DIR=/tmp/matchpatch-uv-cache UV_PROJECT_ENVIRONMENT="$HOME/.local/share/matchpatch/.venv-wsl" uv run --frozen --no-default-groups --group wsl pytest tests/test_devices.py tests/test_gui_settings_renderer.py
UV_CACHE_DIR=/tmp/matchpatch-uv-cache UV_PROJECT_ENVIRONMENT="$HOME/.local/share/matchpatch/.venv-wsl" uv run --frozen --no-default-groups --group wsl ruff check src/matchpatch/devices tests/test_devices.py tests/test_gui_settings_renderer.py
UV_PROJECT_ENVIRONMENT="$HOME/.local/share/matchpatch/.venv-wsl" uv run --frozen --no-default-groups --group docs sphinx-build -W --keep-going -b html docs docs_html