Architecture¶
MatchPatch is a Python 3.12+ application for normalizing loudness across audio processor presets. The checked-in code is centered on Line 6 Helix support, but the newer package layout separates front ends, normalization workflow, audio measurement, and device-specific adapters so more processors can be added later.
Repository Layout¶
src/matchpatch/contains the installable package and console entry points.src/matchpatch/gui/contains the PySide6 GUI.src/matchpatch/devices/contains processor profile interfaces and the Helix implementation.Python/contains legacy Helix file-manipulation utilities. The modern Helix profile still delegates.hls/.hlxparsing and rewriting toPython/preset_handling.py.scripts/contains WSL/Windows environment, worker, installer build, and installer smoke-test wrappers.installer/contains the Inno Setup script, PyInstaller specs, and PowerShell smoke tests for the Windows installer.tests/contains unit, workflow, CLI, measurement, and GUI tests..github/workflows/defines quality, installer smoke, PyPI release, and GitHub Release artifact automation.
Entry Points¶
pyproject.toml exposes two console scripts:
matchpatch = matchpatch.cli:mainmatchpatch-gui = matchpatch.gui.app:main
matchpatch.cli handles top-level utility flags such as --devices,
--environment, and --export-default-config. The normalize subcommand is
forwarded to matchpatch.normalize.
matchpatch.gui.app configures WSLg/Wayland behavior, installs a desktop entry
and Qt message handler, creates QApplication, and shows MainWindow.
The native measurement worker can also be invoked directly:
python -m matchpatch.measure measure ...
python -m matchpatch.measure devices
python -m matchpatch.measure check-hardware ...
Normalization Flow¶
The reusable orchestration lives in matchpatch.workflow.normalize_presets.
Both CLI and GUI build a NormalizationRequest, then call the workflow with a
measurement runner and optional callbacks.
The workflow:
Loads a
DeviceProfilefrom the registry.Validates snapshot count, input file, reference DI, and optional custom adjustment file.
In automation mode, writes a device-specific measurement file beside the input and asks the user to import it.
Resolves selected preset IDs from the input file, optional
--preset-set, optional--diff-input, and optional--limit.Creates a temporary directory and asks the measurement runner to write
lufs_analysis.csv.Verifies the CSV row count matches selected presets.
Applies the CSV through the device file handler, unless GUI-style export is deferred.
Keeps or removes temporary files according to
keep_temp, failure state, and deferred export.
workflow.export_adjusted_file reuses a retained CSV to write an adjusted file
later. This is the GUI save/export path after a deferred measurement run.
CLI/WSL Bridge¶
matchpatch.normalize is the WSL-side command implementation. It merges command
line options, ~/.config/matchpatch/config.toml or --config, and environment
overrides:
MATCHPATCH_BACKENDMATCHPATCH_WINDOWS_PYTHONMATCHPATCH_REFERENCE_DI
For hardware measurement from WSL, run_windows_analysis builds a command for
the native Windows environment:
<windows-python> -m matchpatch.measure measure ...
Paths are converted with wslpath -w. When the GUI supplies a progress callback,
the worker adds --progress-jsonl and parses structured progress lines from the
Windows process. Cancellation kills the worker process if needed.
check_windows_hardware performs an equivalent native call for
matchpatch.measure check-hardware.
Measurement Worker¶
matchpatch.measure is designed to run natively where audio and MIDI devices are
visible. It supports three backends:
hardware: usessounddeviceduplex playback/recording and a device controller for preset/snapshot steering.loopback: records the reference DI unchanged and needs no hardware.simulated: validates routing, tracks preset/snapshot state, applies deterministic gain and compression, and can inject preset failures.
The measurement worker loads a reference WAV with soundfile, ensures the
sample rate matches the configured device rate, forces mono files to stereo, and
truncates files with more than two channels to stereo.
measure_presets computes reference loudness once, then for each preset and
measured snapshot:
Activates the preset.
Reapplies the snapshot.
Records processed audio.
Computes average short-term LUFS and crest factor.
Appends one row to the measurement CSV.
Failures for a preset are represented by ERROR fields in that preset’s row so
the rest of the run can continue.
Audio Analysis¶
matchpatch.analysis contains the signal metrics:
calculate_average_short_term_lufsslides a loudness window through the recording, usingpyloudnorm.Meter.integrated_loudnessand averaging valid windows.calculate_crest_factor_dbcomputes peak/RMS in dB.analyze_audioreturns both metrics inAudioMeasurements.
Defaults are window_seconds=3.0, interval_seconds=0.1, and
minimum_valid_lufs=-100.0. The LUFS window must be at least 0.4 seconds.
matchpatch.audio wraps sounddevice. It enables ASIO by default, resolves an
audio device by numeric ID or name substring, validates channel capacity, records
with configured USB mappings, and trims pre-roll/post-roll using
round_trip_latency_seconds.
Device Layer¶
matchpatch.devices.base defines the extension seam:
DeviceProfilenames a processor, exposes default audio routing and steering, creates file handlers and controllers, and defines device limits.PatchFileHandlervalidates inputs/outputs, lists assignments, parses preset selectors, creates measurement files, applies analysis CSVs, and builds automation output paths.DeviceControlleractivates presets and reapplies snapshots.
The registry in matchpatch.devices.registry currently registers only
helix.
Helix Profile¶
HelixDeviceProfile declares:
name:
helixdisplay name:
Line 6 Helixmax measured snapshots:
8preset name length:
16snapshot name length:
10default audio: device query
Helix, sample rate48000, input USB1/2, output USB3/4default steering: MIDI output query
Helix, channel0, preset wait0.5, snapshot wait0.2, measurement wait0.1
HelixMidiController uses mido. Presets are selected with MIDI program
changes, where internal preset ID 1 maps to program 0. Snapshots use CC 69
with values 0..7.
HelixPatchFileHandler shells out to Python/preset_handling.py with the
current Python interpreter. It delegates:
assignment listing via
--list-presetsmetadata extraction via
--metadataloudness-affecting diff selection via
--diff-presetsmeasurement file creation via
--measurementgain application via
--adjust-gain
Modern measurement CSVs use a generic DevicePatch column. Before passing them
to the legacy utility, the Helix handler writes a temporary legacy CSV that adds
or replaces HelixPreset.
Helix File Processing¶
Python/preset_handling.py understands .hls, .hlx, and unpacked .json.
Setlists are stored as JSON wrappers whose encoded_data contains base64 zlib
data; the script preserves wrapper fields while replacing encoded data, size,
and CRC. Presets are JSON files and may contain either a top-level preset or a
wrapper with a data preset object.
Measurement conversion changes Helix routing so processor input comes from USB
3/4 and final output goes to USB 1/2. Stage conversion restores USB 1/2
outputs to XLR and USB 3/4 inputs to Multi.
Gain application finds an active final output block, normalizes output routing, ensures snapshot controller values exist, computes per-snapshot gain deltas from LUFS and crest factor, adds solo boosts and custom/user overrides, applies a deadband, rejects implausible gain values unless bad LUFS is ignored, and writes snapshot output gain values.
GUI Architecture¶
MainWindow owns user-facing state: active file, selected device/backend,
advanced configuration, preset table, metadata view, retained CSV display,
logging, and file save/save-as actions. It reads preset assignments and metadata
through the selected device profile.
matchpatch.gui.worker keeps blocking work off the UI thread:
HardwareCheckWorkerruns the native hardware check.NormalizationWorkerrunsnormalize_presets, forwards progress, asks the UI to confirm import steps, and supports cancellation.MeasurementOptimizationWorkerruns native timing optimization and forwards progress.
Progress is carried as ProgressEvent dataclasses. JSONL progress is used
across the WSL-to-Windows process boundary; Qt signals carry event objects
inside the GUI.
Configuration¶
matchpatch.config reads TOML using the standard library. When no --config
is supplied, it checks default_config_paths() in order and loads the first
existing file:
Windows:
%APPDATA%\MatchPatch\config.toml, then%USERPROFILE%\.config\matchpatch\config.toml;Linux/WSL/macOS:
$XDG_CONFIG_HOME/matchpatch/config.tomlwhenXDG_CONFIG_HOMEis set, otherwise~/.config/matchpatch/config.toml.
--config points at one explicit file and raises an error if that file is
missing.
Configuration is layered with command-line values preferred over config file
values. For normalize commands, selected environment variables override matching
file values. export_default_config writes a complete TOML file containing
normalization, analysis, measurement policy, and per-device routing/steering
defaults.
Legacy Utility Scripts¶
The Python/ directory predates the package architecture. The main integrated
script is preset_handling.py; other checked-in scripts perform Helix-specific
batch transformations such as decrypting/encrypting HLS, listing cab presets,
replacing amp blocks, removing inactive blocks, resetting output levels, and
converting blocks to stereo. They are useful utilities but not the core
normalization API.
CI And Packaging¶
The quality workflow runs Linux, Windows, and WSL jobs across Python 3.12,
3.13, and 3.14. It syncs dependencies with uv, then runs Ruff lint, Ruff
format check, ty check, and pytest with coverage.
The quality workflow also runs a Windows installer smoke job on Python 3.12. It
installs Inno Setup, builds the PyInstaller payload, packages it with
installer/matchpatch.iss, runs payload and silent install/uninstall smoke
tests, and uploads the generated installer artifact for inspection.
The release workflow is tag-driven. A v* tag must match
project.version in pyproject.toml; CI builds distributions with uv build,
smoke-tests the wheel and source distribution, uploads the generated
docs_html/ offline help bundle for installer packaging, and publishes to PyPI
using OIDC trusted publishing. A separate Windows release job repeats the
tag/version check, builds and smoke-tests the installer, uploads it as an
Actions artifact, and attaches MatchPatch-Setup-<version>.exe to the GitHub
Release.
Windows installer packaging is a three-stage flow:
Build strict Sphinx HTML docs into
docs_html/.Freeze the GUI and CLI into
build/windows-payload/MatchPatch/with PyInstaller, including brand assets, offline docs, andbuild-info.json.Package the payload with Inno Setup into
dist/installer/MatchPatch-Setup-<version>.exe.
Installer builds run from a native Windows path. WSL users normally invoke
scripts/build-windows-installer-from-wsl.sh or
scripts/test-windows-installer-from-wsl.sh, which synchronize the Windows
mirror before calling the native .cmd scripts.