Skip to content

Latest commit

 

History

History
308 lines (243 loc) · 11.2 KB

File metadata and controls

308 lines (243 loc) · 11.2 KB

Adding Devices

MatchPatch keeps device support in the source tree. Adding a device should be a small, explicit change:

  1. Add a new package under src/matchpatch/devices/.
  2. Implement a DeviceProfile, PatchFileHandler, and, when needed, a DeviceController.
  3. Add one instance of the profile to DEVICE_PROFILES in src/matchpatch/devices/available.py.
  4. 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.

Directory Layout

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.

Registration

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

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() and normalization_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

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 numeric PatchAssignment values.
  • 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.

Targets And Gain Points

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 Types And Capabilities

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_files and writes_preset_files;
  • reads_setlist_files and writes_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.

Settings And GUI

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, and kind;
  • default;
  • config_path;
  • cli_flags;
  • label and help;
  • choices, minimum, maximum, and required.
  • show_in_gui, which defaults to True.

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 And Diagnostics

Validation happens in layers:

  • registry validation checks profile names, display names, and handler creation;
  • validate_settings(settings) validates descriptors;
  • validate_input() and validate_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.

Audio Transports

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, an AudioTransportCapabilities object;
  • 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().

Testing A Device

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