matchpatch 0.4.0__tar.gz → 0.6.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. {matchpatch-0.4.0 → matchpatch-0.6.0}/.github/workflows/release.yml +5 -0
  2. {matchpatch-0.4.0 → matchpatch-0.6.0}/.pre-commit-config.yaml +2 -0
  3. {matchpatch-0.4.0 → matchpatch-0.6.0}/PKG-INFO +1 -1
  4. {matchpatch-0.4.0 → matchpatch-0.6.0}/docs/dev/architecture.md +11 -2
  5. {matchpatch-0.4.0 → matchpatch-0.6.0}/docs/dev/commands.md +12 -0
  6. {matchpatch-0.4.0 → matchpatch-0.6.0}/docs/developer-notes.md +18 -4
  7. {matchpatch-0.4.0 → matchpatch-0.6.0}/docs/musician-guide.md +17 -0
  8. {matchpatch-0.4.0 → matchpatch-0.6.0}/docs/quick-start.md +3 -0
  9. {matchpatch-0.4.0 → matchpatch-0.6.0}/docs/troubleshooting.md +27 -0
  10. {matchpatch-0.4.0 → matchpatch-0.6.0}/installer/matchpatch.iss +62 -1
  11. {matchpatch-0.4.0 → matchpatch-0.6.0}/installer/pyinstaller/build_support.py +19 -0
  12. {matchpatch-0.4.0 → matchpatch-0.6.0}/installer/pyinstaller/matchpatch-gui.spec +2 -0
  13. {matchpatch-0.4.0 → matchpatch-0.6.0}/installer/smoke/smoke_installed.ps1 +4 -2
  14. {matchpatch-0.4.0 → matchpatch-0.6.0}/installer/smoke/smoke_payload.ps1 +2 -0
  15. {matchpatch-0.4.0 → matchpatch-0.6.0}/pyproject.toml +1 -1
  16. {matchpatch-0.4.0 → matchpatch-0.6.0}/scripts/release.py +12 -1
  17. {matchpatch-0.4.0 → matchpatch-0.6.0}/src/matchpatch/cli.py +5 -0
  18. {matchpatch-0.4.0 → matchpatch-0.6.0}/src/matchpatch/config.py +29 -5
  19. {matchpatch-0.4.0 → matchpatch-0.6.0}/src/matchpatch/devices/helix.py +54 -0
  20. {matchpatch-0.4.0 → matchpatch-0.6.0}/src/matchpatch/gui/device_panels.py +0 -27
  21. {matchpatch-0.4.0 → matchpatch-0.6.0}/src/matchpatch/gui/dialogs.py +21 -1
  22. {matchpatch-0.4.0 → matchpatch-0.6.0}/src/matchpatch/gui/main_window.py +9 -2
  23. {matchpatch-0.4.0 → matchpatch-0.6.0}/src/matchpatch/measure.py +4 -4
  24. {matchpatch-0.4.0 → matchpatch-0.6.0}/src/matchpatch/normalize.py +22 -13
  25. {matchpatch-0.4.0 → matchpatch-0.6.0}/tests/test_cli.py +14 -0
  26. {matchpatch-0.4.0 → matchpatch-0.6.0}/tests/test_config.py +45 -0
  27. {matchpatch-0.4.0 → matchpatch-0.6.0}/tests/test_gui.py +69 -9
  28. {matchpatch-0.4.0 → matchpatch-0.6.0}/tests/test_gui_dialogs.py +36 -0
  29. {matchpatch-0.4.0 → matchpatch-0.6.0}/tests/test_helix.py +45 -0
  30. {matchpatch-0.4.0 → matchpatch-0.6.0}/tests/test_installer_metadata.py +26 -0
  31. {matchpatch-0.4.0 → matchpatch-0.6.0}/tests/test_normalize.py +93 -0
  32. {matchpatch-0.4.0 → matchpatch-0.6.0}/uv.lock +1 -1
  33. {matchpatch-0.4.0 → matchpatch-0.6.0}/.gitattributes +0 -0
  34. {matchpatch-0.4.0 → matchpatch-0.6.0}/.github/dependabot.yml +0 -0
  35. {matchpatch-0.4.0 → matchpatch-0.6.0}/.github/workflows/quality.yml +0 -0
  36. {matchpatch-0.4.0 → matchpatch-0.6.0}/.gitignore +0 -0
  37. {matchpatch-0.4.0 → matchpatch-0.6.0}/.python-version +0 -0
  38. {matchpatch-0.4.0 → matchpatch-0.6.0}/AGENTS.md +0 -0
  39. {matchpatch-0.4.0 → matchpatch-0.6.0}/LICENSE +0 -0
  40. {matchpatch-0.4.0 → matchpatch-0.6.0}/Python/adjust_gain.py +0 -0
  41. {matchpatch-0.4.0 → matchpatch-0.6.0}/Python/decrypt_hls.py +0 -0
  42. {matchpatch-0.4.0 → matchpatch-0.6.0}/Python/encrypt_hls.py +0 -0
  43. {matchpatch-0.4.0 → matchpatch-0.6.0}/Python/list_cab_presets.py +0 -0
  44. {matchpatch-0.4.0 → matchpatch-0.6.0}/Python/preset_handling.py +0 -0
  45. {matchpatch-0.4.0 → matchpatch-0.6.0}/Python/remove_inactive_blocks.py +0 -0
  46. {matchpatch-0.4.0 → matchpatch-0.6.0}/Python/replace_amp.py +0 -0
  47. {matchpatch-0.4.0 → matchpatch-0.6.0}/Python/reset_output_levels.py +0 -0
  48. {matchpatch-0.4.0 → matchpatch-0.6.0}/Python/stereofy.py +0 -0
  49. {matchpatch-0.4.0 → matchpatch-0.6.0}/README.md +0 -0
  50. {matchpatch-0.4.0 → matchpatch-0.6.0}/audio/reference-di/DI_Strandberg_Boden_Fusion_Bridge_Humbucker.wav +0 -0
  51. {matchpatch-0.4.0 → matchpatch-0.6.0}/docs/_static/matchpatch-docs.css +0 -0
  52. {matchpatch-0.4.0 → matchpatch-0.6.0}/docs/assets/matchmatch-icon-512.png +0 -0
  53. {matchpatch-0.4.0 → matchpatch-0.6.0}/docs/assets/matchmatch-icon.png +0 -0
  54. {matchpatch-0.4.0 → matchpatch-0.6.0}/docs/assets/matchmatch-logo.png +0 -0
  55. {matchpatch-0.4.0 → matchpatch-0.6.0}/docs/assets/screenshots/backend-selector.png +0 -0
  56. {matchpatch-0.4.0 → matchpatch-0.6.0}/docs/assets/screenshots/completed-results-table.png +0 -0
  57. {matchpatch-0.4.0 → matchpatch-0.6.0}/docs/assets/screenshots/failed-measurement-row.png +0 -0
  58. {matchpatch-0.4.0 → matchpatch-0.6.0}/docs/assets/screenshots/hardware-routing.png +0 -0
  59. {matchpatch-0.4.0 → matchpatch-0.6.0}/docs/assets/screenshots/loaded-setlist.png +0 -0
  60. {matchpatch-0.4.0 → matchpatch-0.6.0}/docs/assets/screenshots/normalization-ongoing.png +0 -0
  61. {matchpatch-0.4.0 → matchpatch-0.6.0}/docs/assets/screenshots/optimization-dialog.png +0 -0
  62. {matchpatch-0.4.0 → matchpatch-0.6.0}/docs/assets/screenshots/parameter-study-setup.png +0 -0
  63. {matchpatch-0.4.0 → matchpatch-0.6.0}/docs/assets/screenshots/timing-tab.png +0 -0
  64. {matchpatch-0.4.0 → matchpatch-0.6.0}/docs/concepts/backends.md +0 -0
  65. {matchpatch-0.4.0 → matchpatch-0.6.0}/docs/concepts/crest-factor.md +0 -0
  66. {matchpatch-0.4.0 → matchpatch-0.6.0}/docs/concepts/lufs-and-loudness.md +0 -0
  67. {matchpatch-0.4.0 → matchpatch-0.6.0}/docs/concepts/measurement-and-adjusted-files.md +0 -0
  68. {matchpatch-0.4.0 → matchpatch-0.6.0}/docs/concepts/reading-results.md +0 -0
  69. {matchpatch-0.4.0 → matchpatch-0.6.0}/docs/concepts/reference-di.md +0 -0
  70. {matchpatch-0.4.0 → matchpatch-0.6.0}/docs/concepts/routing-and-levels.md +0 -0
  71. {matchpatch-0.4.0 → matchpatch-0.6.0}/docs/concepts/snapshots-solos-and-ignored.md +0 -0
  72. {matchpatch-0.4.0 → matchpatch-0.6.0}/docs/concepts/timing.md +0 -0
  73. {matchpatch-0.4.0 → matchpatch-0.6.0}/docs/conf.py +0 -0
  74. {matchpatch-0.4.0 → matchpatch-0.6.0}/docs/dev/file-formats.md +0 -0
  75. {matchpatch-0.4.0 → matchpatch-0.6.0}/docs/dev/release.md +0 -0
  76. {matchpatch-0.4.0 → matchpatch-0.6.0}/docs/faq.md +0 -0
  77. {matchpatch-0.4.0 → matchpatch-0.6.0}/docs/glossary.md +0 -0
  78. {matchpatch-0.4.0 → matchpatch-0.6.0}/docs/index.md +0 -0
  79. {matchpatch-0.4.0 → matchpatch-0.6.0}/docs/workflows/custom-adjustments.md +0 -0
  80. {matchpatch-0.4.0 → matchpatch-0.6.0}/docs/workflows/hardware-measurement.md +0 -0
  81. {matchpatch-0.4.0 → matchpatch-0.6.0}/docs/workflows/manual-editing-and-csv.md +0 -0
  82. {matchpatch-0.4.0 → matchpatch-0.6.0}/docs/workflows/normalize-setlist.md +0 -0
  83. {matchpatch-0.4.0 → matchpatch-0.6.0}/docs/workflows/normalize-single-preset.md +0 -0
  84. {matchpatch-0.4.0 → matchpatch-0.6.0}/docs/workflows/optimize-timing.md +0 -0
  85. {matchpatch-0.4.0 → matchpatch-0.6.0}/docs/workflows/save-and-import.md +0 -0
  86. {matchpatch-0.4.0 → matchpatch-0.6.0}/docs/workflows/select-changed-presets.md +0 -0
  87. {matchpatch-0.4.0 → matchpatch-0.6.0}/docs/workflows/test-without-hardware.md +0 -0
  88. {matchpatch-0.4.0 → matchpatch-0.6.0}/installer/README.md +0 -0
  89. {matchpatch-0.4.0 → matchpatch-0.6.0}/scripts/build-docs.sh +0 -0
  90. {matchpatch-0.4.0 → matchpatch-0.6.0}/scripts/build-windows-installer-from-wsl.sh +0 -0
  91. {matchpatch-0.4.0 → matchpatch-0.6.0}/scripts/build-windows-installer.cmd +0 -0
  92. {matchpatch-0.4.0 → matchpatch-0.6.0}/scripts/build-windows-payload.cmd +0 -0
  93. {matchpatch-0.4.0 → matchpatch-0.6.0}/scripts/check_commit_msg.py +0 -0
  94. {matchpatch-0.4.0 → matchpatch-0.6.0}/scripts/measure-windows-from-wsl.sh +0 -0
  95. {matchpatch-0.4.0 → matchpatch-0.6.0}/scripts/run_hook_with_hint.py +0 -0
  96. {matchpatch-0.4.0 → matchpatch-0.6.0}/scripts/stage-installer-docs.sh +0 -0
  97. {matchpatch-0.4.0 → matchpatch-0.6.0}/scripts/sync-windows-from-wsl.sh +0 -0
  98. {matchpatch-0.4.0 → matchpatch-0.6.0}/scripts/sync-windows.cmd +0 -0
  99. {matchpatch-0.4.0 → matchpatch-0.6.0}/scripts/sync-wsl.sh +0 -0
  100. {matchpatch-0.4.0 → matchpatch-0.6.0}/scripts/test-gui.sh +0 -0
  101. {matchpatch-0.4.0 → matchpatch-0.6.0}/scripts/test-windows-installer-from-wsl.sh +0 -0
  102. {matchpatch-0.4.0 → matchpatch-0.6.0}/scripts/test-windows-installer.cmd +0 -0
  103. {matchpatch-0.4.0 → matchpatch-0.6.0}/src/matchpatch/__init__.py +0 -0
  104. {matchpatch-0.4.0 → matchpatch-0.6.0}/src/matchpatch/analysis.py +0 -0
  105. {matchpatch-0.4.0 → matchpatch-0.6.0}/src/matchpatch/app.py +0 -0
  106. {matchpatch-0.4.0 → matchpatch-0.6.0}/src/matchpatch/audio.py +0 -0
  107. {matchpatch-0.4.0 → matchpatch-0.6.0}/src/matchpatch/custom_adjustments.py +0 -0
  108. {matchpatch-0.4.0 → matchpatch-0.6.0}/src/matchpatch/devices/__init__.py +0 -0
  109. {matchpatch-0.4.0 → matchpatch-0.6.0}/src/matchpatch/devices/base.py +0 -0
  110. {matchpatch-0.4.0 → matchpatch-0.6.0}/src/matchpatch/devices/registry.py +0 -0
  111. {matchpatch-0.4.0 → matchpatch-0.6.0}/src/matchpatch/gui/__init__.py +0 -0
  112. {matchpatch-0.4.0 → matchpatch-0.6.0}/src/matchpatch/gui/app.py +0 -0
  113. {matchpatch-0.4.0 → matchpatch-0.6.0}/src/matchpatch/gui/help.py +0 -0
  114. {matchpatch-0.4.0 → matchpatch-0.6.0}/src/matchpatch/gui/snapshot_header.py +0 -0
  115. {matchpatch-0.4.0 → matchpatch-0.6.0}/src/matchpatch/gui/worker.py +0 -0
  116. {matchpatch-0.4.0 → matchpatch-0.6.0}/src/matchpatch/measurement_optimizer.py +0 -0
  117. {matchpatch-0.4.0 → matchpatch-0.6.0}/src/matchpatch/progress.py +0 -0
  118. {matchpatch-0.4.0 → matchpatch-0.6.0}/src/matchpatch/workflow.py +0 -0
  119. {matchpatch-0.4.0 → matchpatch-0.6.0}/tests/README.md +0 -0
  120. {matchpatch-0.4.0 → matchpatch-0.6.0}/tests/test_analysis.py +0 -0
  121. {matchpatch-0.4.0 → matchpatch-0.6.0}/tests/test_app.py +0 -0
  122. {matchpatch-0.4.0 → matchpatch-0.6.0}/tests/test_audio.py +0 -0
  123. {matchpatch-0.4.0 → matchpatch-0.6.0}/tests/test_check_commit_msg.py +0 -0
  124. {matchpatch-0.4.0 → matchpatch-0.6.0}/tests/test_devices.py +0 -0
  125. {matchpatch-0.4.0 → matchpatch-0.6.0}/tests/test_gui_app.py +0 -0
  126. {matchpatch-0.4.0 → matchpatch-0.6.0}/tests/test_gui_help.py +0 -0
  127. {matchpatch-0.4.0 → matchpatch-0.6.0}/tests/test_measure.py +0 -0
  128. {matchpatch-0.4.0 → matchpatch-0.6.0}/tests/test_measurement_optimizer.py +0 -0
  129. {matchpatch-0.4.0 → matchpatch-0.6.0}/tests/test_preset_handling.py +0 -0
  130. {matchpatch-0.4.0 → matchpatch-0.6.0}/tests/test_progress.py +0 -0
@@ -8,6 +8,9 @@ on:
8
8
  permissions:
9
9
  contents: read
10
10
 
11
+ env:
12
+ FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
13
+
11
14
  jobs:
12
15
  publish:
13
16
  name: Publish to PyPI
@@ -26,6 +29,7 @@ jobs:
26
29
  - name: Install uv and Python
27
30
  uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
28
31
  with:
32
+ enable-cache: false
29
33
  python-version: "3.14"
30
34
 
31
35
  - name: Verify tag matches package version
@@ -81,6 +85,7 @@ jobs:
81
85
  - name: Install uv and Python
82
86
  uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
83
87
  with:
88
+ enable-cache: false
84
89
  python-version: "3.12"
85
90
 
86
91
  - name: Resolve package version
@@ -2,6 +2,8 @@ default_install_hook_types:
2
2
  - pre-commit
3
3
  - commit-msg
4
4
  - pre-push
5
+ default_stages:
6
+ - pre-commit
5
7
 
6
8
  repos:
7
9
  - repo: https://github.com/pre-commit/pre-commit-hooks
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: matchpatch
3
- Version: 0.4.0
3
+ Version: 0.6.0
4
4
  Summary: Audio processor preset gain normalization and measurement tools
5
5
  Project-URL: Homepage, https://github.com/noseglasses/MatchPatch
6
6
  Project-URL: Issues, https://github.com/noseglasses/MatchPatch/issues
@@ -223,8 +223,17 @@ inside the GUI.
223
223
 
224
224
  ## Configuration
225
225
 
226
- `matchpatch.config` reads TOML using the standard library. The default path is
227
- `~/.config/matchpatch/config.toml`; `--config` points at another file.
226
+ `matchpatch.config` reads TOML using the standard library. When no `--config`
227
+ is supplied, it checks `default_config_paths()` in order and loads the first
228
+ existing file:
229
+
230
+ - Windows: `%APPDATA%\MatchPatch\config.toml`, then
231
+ `%USERPROFILE%\.config\matchpatch\config.toml`;
232
+ - Linux/WSL/macOS: `$XDG_CONFIG_HOME/matchpatch/config.toml` when
233
+ `XDG_CONFIG_HOME` is set, otherwise `~/.config/matchpatch/config.toml`.
234
+
235
+ `--config` points at one explicit file and raises an error if that file is
236
+ missing.
228
237
 
229
238
  Configuration is layered with command-line values preferred over config file
230
239
  values. For normalize commands, selected environment variables override matching
@@ -112,6 +112,18 @@ Export default config:
112
112
  matchpatch --export-default-config ~/.config/matchpatch/config.toml
113
113
  ```
114
114
 
115
+ Installed Windows builds automatically look first in the roaming profile:
116
+
117
+ ```powershell
118
+ MatchPatch.exe --cli --export-default-config "$env:APPDATA\MatchPatch\config.toml"
119
+ ```
120
+
121
+ When `--config` is omitted, MatchPatch searches for config files in this order:
122
+ `%APPDATA%\MatchPatch\config.toml`, then
123
+ `%USERPROFILE%\.config\matchpatch\config.toml` on Windows; on Linux/WSL/macOS,
124
+ `$XDG_CONFIG_HOME/matchpatch/config.toml` when `XDG_CONFIG_HOME` is set,
125
+ otherwise `~/.config/matchpatch/config.toml`.
126
+
115
127
  Normalize without hardware:
116
128
 
117
129
  ```bash
@@ -136,10 +136,18 @@ When adding a new term, update [Glossary](glossary.md).
136
136
  Most musicians should configure MatchPatch from the GUI. Use TOML only for
137
137
  durable machine defaults, repeated CLI runs, or advanced setup notes.
138
138
 
139
- MatchPatch loads `~/.config/matchpatch/config.toml` automatically when it
140
- exists. Use `--config PATH` to load another file. Command-line options override
141
- the file. The `MATCHPATCH_BACKEND`, `MATCHPATCH_WINDOWS_PYTHON`, and
142
- `MATCHPATCH_REFERENCE_DI` environment variables override matching file values.
139
+ MatchPatch automatically loads the first existing default config file from this
140
+ search path:
141
+
142
+ - Windows: `%APPDATA%\MatchPatch\config.toml`
143
+ - Windows fallback: `%USERPROFILE%\.config\matchpatch\config.toml`
144
+ - Linux/WSL/macOS with `XDG_CONFIG_HOME`: `$XDG_CONFIG_HOME/matchpatch/config.toml`
145
+ - Linux/WSL/macOS fallback: `~/.config/matchpatch/config.toml`
146
+
147
+ Use `--config PATH` or the GUI Files tab to load another file. Command-line
148
+ options override the file. The `MATCHPATCH_BACKEND`,
149
+ `MATCHPATCH_WINDOWS_PYTHON`, and `MATCHPATCH_REFERENCE_DI` environment
150
+ variables override matching file values.
143
151
 
144
152
  Use `--snapshot-count N` to override `policy.measured_snapshots` for one run.
145
153
  Line 6 Helix supports between `1` and `8` measured snapshots; the default is
@@ -151,6 +159,12 @@ Export the default config:
151
159
  matchpatch --export-default-config ~/.config/matchpatch/config.toml
152
160
  ```
153
161
 
162
+ On installed Windows builds, the matching default export target is:
163
+
164
+ ```powershell
165
+ MatchPatch.exe --cli --export-default-config "$env:APPDATA\MatchPatch\config.toml"
166
+ ```
167
+
154
168
  Example config:
155
169
 
156
170
  ```toml
@@ -84,6 +84,23 @@ Use hardware for final rehearsal or gig-ready results.
84
84
 
85
85
  See [Backends](concepts/backends.md).
86
86
 
87
+ ## Saved Configuration
88
+
89
+ Most users can set options directly in the GUI. If you want MatchPatch to load
90
+ the same machine defaults every time, save a TOML configuration file.
91
+
92
+ When no config file is selected in Advanced > Files, MatchPatch automatically
93
+ uses the first config file it finds in this search path:
94
+
95
+ - Windows installed app: `%APPDATA%\MatchPatch\config.toml`
96
+ - Windows compatibility fallback: `%USERPROFILE%\.config\matchpatch\config.toml`
97
+ - Linux/WSL/macOS with `XDG_CONFIG_HOME`: `$XDG_CONFIG_HOME/matchpatch/config.toml`
98
+ - Linux/WSL/macOS fallback: `~/.config/matchpatch/config.toml`
99
+
100
+ To use a different file for one session, open Advanced > Files and choose it in
101
+ the Config field. A selected Config path takes priority over the automatic
102
+ search path.
103
+
87
104
  (help-opening-files)=
88
105
  ## Opening Files
89
106
 
@@ -16,6 +16,9 @@ Helix results, use hardware mode.
16
16
  - Decide which backend to use:
17
17
  - loopback for a safe no-hardware test;
18
18
  - hardware for real Helix measurement.
19
+ - Optional: save a config file if you want the same defaults every time. On
20
+ installed Windows builds, the automatic config path is
21
+ `%APPDATA%\MatchPatch\config.toml`.
19
22
 
20
23
  > Warning:
21
24
  > Keep a backup of your original Helix file before saving changes.
@@ -114,6 +114,33 @@ The file path in Advanced > Files points to a missing or moved WAV.
114
114
 
115
115
  See [Reference DI](concepts/reference-di.md).
116
116
 
117
+ ## Config File Not Applied
118
+
119
+ ### What You See
120
+
121
+ MatchPatch starts with different backend, routing, Reference DI, or timing
122
+ settings than you expected.
123
+
124
+ ### Likely Cause
125
+
126
+ The config file is not in the automatic search path, or another config file is
127
+ selected in Advanced > Files.
128
+
129
+ ### What To Try
130
+
131
+ 1. Open Advanced > Files.
132
+ 2. If the Config field points to a file, confirm it is the file you meant to
133
+ use.
134
+ 3. If the Config field is empty, put your default config in the automatic
135
+ location for your system.
136
+
137
+ Automatic config search path:
138
+
139
+ - Windows installed app: `%APPDATA%\MatchPatch\config.toml`
140
+ - Windows compatibility fallback: `%USERPROFILE%\.config\matchpatch\config.toml`
141
+ - Linux/WSL/macOS with `XDG_CONFIG_HOME`: `$XDG_CONFIG_HOME/matchpatch/config.toml`
142
+ - Linux/WSL/macOS fallback: `~/.config/matchpatch/config.toml`
143
+
117
144
  ## Reference DI Sample Rate Mismatch
118
145
 
119
146
  ### What You See
@@ -11,8 +11,11 @@
11
11
  #endif
12
12
 
13
13
  #define AppName "MatchPatch"
14
+ #define AppUninstallRegistryKey "Software\Microsoft\Windows\CurrentVersion\Uninstall\{15537D18-AE3B-4B79-A046-9B95C60E2DB4}_is1"
14
15
  #define AppPublisher "noseglasses/MatchPatch"
15
16
  #define AppURL "https://github.com/noseglasses/MatchPatch"
17
+ #define UninstallerBaseName "Uninstall-MatchPatch"
18
+ #define UninstallerExeName "Uninstall-MatchPatch.exe"
16
19
 
17
20
  [Setup]
18
21
  AppId={{15537D18-AE3B-4B79-A046-9B95C60E2DB4}
@@ -46,8 +49,66 @@ Source: "{#SourceDir}\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs
46
49
  [Icons]
47
50
  Name: "{group}\MatchPatch"; Filename: "{app}\MatchPatch.exe"; WorkingDir: "{app}"; IconFilename: "{app}\installer-assets\matchpatch.ico"
48
51
  Name: "{group}\MatchPatch Documentation"; Filename: "{app}\docs_html\index.html"; WorkingDir: "{app}\docs_html"
49
- Name: "{group}\Uninstall MatchPatch"; Filename: "{uninstallexe}"
52
+ Name: "{group}\Uninstall MatchPatch"; Filename: "{app}\{#UninstallerExeName}"
50
53
  Name: "{autodesktop}\MatchPatch"; Filename: "{app}\MatchPatch.exe"; WorkingDir: "{app}"; IconFilename: "{app}\installer-assets\matchpatch.ico"; Tasks: desktopicon
51
54
 
52
55
  [Run]
53
56
  Filename: "{app}\MatchPatch.exe"; Description: "{cm:LaunchProgram,MatchPatch}"; Flags: nowait postinstall skipifsilent unchecked
57
+
58
+ [Code]
59
+ const
60
+ UninstallerBaseName = '{#UninstallerBaseName}';
61
+ UninstallerExeName = '{#UninstallerExeName}';
62
+ UninstallRegistryKey = '{#AppUninstallRegistryKey}';
63
+
64
+ procedure RenameUninstallerFile(SourceName: string; TargetName: string);
65
+ var
66
+ SourcePath: string;
67
+ TargetPath: string;
68
+ begin
69
+ SourcePath := ExpandConstant('{app}\') + SourceName;
70
+ TargetPath := ExpandConstant('{app}\') + TargetName;
71
+
72
+ if FileExists(SourcePath) then
73
+ begin
74
+ if FileExists(TargetPath) then
75
+ begin
76
+ DeleteFile(TargetPath);
77
+ end;
78
+
79
+ if not RenameFile(SourcePath, TargetPath) then
80
+ begin
81
+ RaiseException('Could not rename ' + SourcePath + ' to ' + TargetPath);
82
+ end;
83
+ end;
84
+ end;
85
+
86
+ procedure UpdateUninstallRegistry;
87
+ var
88
+ UninstallerPath: string;
89
+ begin
90
+ UninstallerPath := ExpandConstant('{app}\') + UninstallerExeName;
91
+ RegWriteStringValue(
92
+ HKEY_LOCAL_MACHINE,
93
+ UninstallRegistryKey,
94
+ 'UninstallString',
95
+ '"' + UninstallerPath + '"'
96
+ );
97
+ RegWriteStringValue(
98
+ HKEY_LOCAL_MACHINE,
99
+ UninstallRegistryKey,
100
+ 'QuietUninstallString',
101
+ '"' + UninstallerPath + '" /SILENT'
102
+ );
103
+ end;
104
+
105
+ procedure CurStepChanged(CurStep: TSetupStep);
106
+ begin
107
+ if CurStep = ssPostInstall then
108
+ begin
109
+ RenameUninstallerFile('unins000.exe', UninstallerExeName);
110
+ RenameUninstallerFile('unins000.dat', UninstallerBaseName + '.dat');
111
+ RenameUninstallerFile('unins000.msg', UninstallerBaseName + '.msg');
112
+ UpdateUninstallRegistry;
113
+ end;
114
+ end;
@@ -19,6 +19,16 @@ PYINSTALLER_ASSETS_ROOT = PYINSTALLER_WORK_ROOT / "installer-assets"
19
19
  INSTALLER_ASSETS_ROOT = PAYLOAD_ROOT / "installer-assets"
20
20
  ICON_SOURCE = PROJECT_ROOT / "docs" / "assets" / "matchmatch-icon-512.png"
21
21
  LOGO_SOURCE = PROJECT_ROOT / "docs" / "assets" / "matchmatch-logo.png"
22
+ REFERENCE_DI_SOURCE = (
23
+ PROJECT_ROOT / "audio" / "reference-di" / "DI_Strandberg_Boden_Fusion_Bridge_Humbucker.wav"
24
+ )
25
+ PAYLOAD_RUNTIME_FILES = [
26
+ (PROJECT_ROOT / "Python" / "preset_handling.py", Path("Python") / "preset_handling.py"),
27
+ (
28
+ REFERENCE_DI_SOURCE,
29
+ Path("audio") / "reference-di" / "DI_Strandberg_Boden_Fusion_Bridge_Humbucker.wav",
30
+ ),
31
+ ]
22
32
 
23
33
 
24
34
  def project_version() -> str:
@@ -41,6 +51,8 @@ def git_sha() -> str:
41
51
 
42
52
  def asset_datas() -> list[tuple[str, str]]:
43
53
  return [
54
+ (str(PROJECT_ROOT / "Python" / "preset_handling.py"), "Python"),
55
+ (str(REFERENCE_DI_SOURCE), "audio/reference-di"),
44
56
  (str(PROJECT_ROOT / "docs" / "assets" / "matchmatch-icon.png"), "docs/assets"),
45
57
  (str(PROJECT_ROOT / "docs" / "assets" / "matchmatch-icon-512.png"), "docs/assets"),
46
58
  (str(PROJECT_ROOT / "docs" / "assets" / "matchmatch-logo.png"), "docs/assets"),
@@ -95,6 +107,13 @@ def stage_installer_assets(
95
107
  shutil.copytree(source_root, target_root)
96
108
 
97
109
 
110
+ def stage_runtime_files(payload_root: Path = PAYLOAD_ROOT) -> None:
111
+ for source, relative_target in PAYLOAD_RUNTIME_FILES:
112
+ target = payload_root / relative_target
113
+ target.parent.mkdir(parents=True, exist_ok=True)
114
+ shutil.copy2(source, target)
115
+
116
+
98
117
  def write_build_info(payload_root: Path = PAYLOAD_ROOT) -> None:
99
118
  payload_root.mkdir(parents=True, exist_ok=True)
100
119
  build_info = {
@@ -16,6 +16,7 @@ from build_support import (
16
16
  prepare_pyinstaller_paths,
17
17
  stage_installer_assets,
18
18
  stage_docs,
19
+ stage_runtime_files,
19
20
  write_build_info,
20
21
  )
21
22
 
@@ -65,5 +66,6 @@ coll = COLLECT(
65
66
  )
66
67
 
67
68
  stage_installer_assets()
69
+ stage_runtime_files()
68
70
  stage_docs()
69
71
  write_build_info()
@@ -78,10 +78,11 @@ Invoke-SetupProcess -Path $installer -Arguments $installArgs -FailureMessage "In
78
78
  $guiExe = Join-Path $InstallDir "MatchPatch.exe"
79
79
  $docsIndex = Join-Path $InstallDir "docs_html\index.html"
80
80
  $buildInfoPath = Join-Path $InstallDir "build-info.json"
81
- $uninstaller = Join-Path $InstallDir "unins000.exe"
81
+ $uninstaller = Join-Path $InstallDir "Uninstall-MatchPatch.exe"
82
82
  $installerIcon = Join-Path $InstallDir "installer-assets\matchpatch.ico"
83
83
  $wizardLogo = Join-Path $InstallDir "installer-assets\wizard-logo.bmp"
84
84
  $wizardSmallLogo = Join-Path $InstallDir "installer-assets\wizard-small-logo.bmp"
85
+ $referenceDi = Join-Path $InstallDir "audio\reference-di\DI_Strandberg_Boden_Fusion_Bridge_Humbucker.wav"
85
86
 
86
87
  Assert-FileExists $guiExe
87
88
  Assert-FileExists $docsIndex
@@ -90,6 +91,7 @@ Assert-FileExists $uninstaller
90
91
  Assert-FileExists $installerIcon
91
92
  Assert-FileExists $wizardLogo
92
93
  Assert-FileExists $wizardSmallLogo
94
+ Assert-FileExists $referenceDi
93
95
 
94
96
  $buildInfo = Get-Content -LiteralPath $buildInfoPath -Raw | ConvertFrom-Json
95
97
  if ($buildInfo.version -ne $ExpectedVersion) {
@@ -112,7 +114,7 @@ Invoke-SetupProcess -Path $uninstaller -Arguments $uninstallArgs -FailureMessage
112
114
  Start-Sleep -Seconds 2
113
115
  if (Test-Path -LiteralPath $InstallDir) {
114
116
  $leftovers = @(Get-ChildItem -LiteralPath $InstallDir -Force)
115
- $unexpected = @($leftovers | Where-Object { $_.Name -notmatch "^unins\d+\.dat$|^unins\d+\.msg$|^unins\d+\.log$" })
117
+ $unexpected = @($leftovers | Where-Object { $_.Name -notmatch "^Uninstall-MatchPatch\.(dat|msg|log)$" })
116
118
  if ($unexpected.Count -gt 0) {
117
119
  throw "Install directory contains unexpected leftovers after uninstall: $($unexpected.FullName -join ', ')"
118
120
  }
@@ -49,6 +49,7 @@ $buildInfoPath = Join-Path $payload "build-info.json"
49
49
  $installerIcon = Join-Path $payload "installer-assets\matchpatch.ico"
50
50
  $wizardLogo = Join-Path $payload "installer-assets\wizard-logo.bmp"
51
51
  $wizardSmallLogo = Join-Path $payload "installer-assets\wizard-small-logo.bmp"
52
+ $referenceDi = Join-Path $payload "audio\reference-di\DI_Strandberg_Boden_Fusion_Bridge_Humbucker.wav"
52
53
 
53
54
  Assert-FileExists $guiExe
54
55
  Assert-FileExists $docsIndex
@@ -56,6 +57,7 @@ Assert-FileExists $buildInfoPath
56
57
  Assert-FileExists $installerIcon
57
58
  Assert-FileExists $wizardLogo
58
59
  Assert-FileExists $wizardSmallLogo
60
+ Assert-FileExists $referenceDi
59
61
 
60
62
  $buildInfo = Get-Content -LiteralPath $buildInfoPath -Raw | ConvertFrom-Json
61
63
  if ($buildInfo.version -ne $ExpectedVersion) {
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "matchpatch"
3
- version = "0.4.0"
3
+ version = "0.6.0"
4
4
  description = "Audio processor preset gain normalization and measurement tools"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12,<3.15"
@@ -566,7 +566,18 @@ def verify_public_release(version: str, tag: str, notes_file: str | None) -> Non
566
566
  assets = {asset["name"] for asset in release.get("assets", [])}
567
567
  if expected_asset not in assets:
568
568
  raise ReleaseError(f"GitHub Release is missing installer asset: {expected_asset}")
569
- run([sys.executable, "-m", "pip", "index", "versions", "matchpatch"])
569
+ run(
570
+ [
571
+ sys.executable,
572
+ "-c",
573
+ (
574
+ "import json, sys, urllib.request; "
575
+ "data = json.load(urllib.request.urlopen('https://pypi.org/pypi/matchpatch/json')); "
576
+ "sys.exit(0 if sys.argv[1] in data['releases'] else 1)"
577
+ ),
578
+ version,
579
+ ]
580
+ )
570
581
 
571
582
 
572
583
  def parse_args() -> argparse.Namespace:
@@ -29,6 +29,11 @@ def main(argv: list[str] | None = None) -> None:
29
29
 
30
30
  normalize_main(args[1:])
31
31
  return
32
+ if args and args[0] == "measure":
33
+ from matchpatch.measure import main as measure_main
34
+
35
+ measure_main(args[1:])
36
+ return
32
37
 
33
38
  parser = argparse.ArgumentParser(description="Normalize gain across audio processor presets")
34
39
  parser.add_argument(
@@ -2,6 +2,8 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import os
6
+ import sys
5
7
  import tomllib
6
8
  from pathlib import Path
7
9
  from typing import Any
@@ -12,17 +14,39 @@ from matchpatch.devices.base import NormalizationPolicy
12
14
  Config = dict[str, Any]
13
15
 
14
16
 
17
+ def default_config_paths() -> list[Path]:
18
+ home = Path.home()
19
+ legacy_path = home / ".config" / "matchpatch" / "config.toml"
20
+
21
+ if sys.platform == "win32":
22
+ appdata = os.getenv("APPDATA")
23
+ if appdata:
24
+ primary_path = Path(appdata) / "MatchPatch" / "config.toml"
25
+ else:
26
+ primary_path = home / "AppData" / "Roaming" / "MatchPatch" / "config.toml"
27
+ return [primary_path, legacy_path]
28
+
29
+ xdg_config_home = os.getenv("XDG_CONFIG_HOME")
30
+ if xdg_config_home:
31
+ return [Path(xdg_config_home) / "matchpatch" / "config.toml", legacy_path]
32
+ return [legacy_path]
33
+
34
+
15
35
  def default_config_path() -> Path:
16
- return Path.home() / ".config" / "matchpatch" / "config.toml"
36
+ return default_config_paths()[0]
17
37
 
18
38
 
19
39
  def load_config(path: str | Path | None) -> Config:
20
- config_path = Path(path).expanduser() if path is not None else default_config_path()
40
+ if path is None:
41
+ for config_path in default_config_paths():
42
+ if config_path.is_file():
43
+ with config_path.open("rb") as config_file:
44
+ return tomllib.load(config_file)
45
+ return {}
21
46
 
22
- if not config_path.is_file():
23
- if path is None:
24
- return {}
47
+ config_path = Path(path).expanduser()
25
48
 
49
+ if not config_path.is_file():
26
50
  raise ValueError(f"MatchPatch config file does not exist: {config_path}")
27
51
 
28
52
  with config_path.open("rb") as config_file:
@@ -2,8 +2,11 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import contextlib
5
6
  import csv
7
+ import io
6
8
  import json
9
+ import runpy
7
10
  import subprocess
8
11
  import sys
9
12
  import tempfile
@@ -38,6 +41,9 @@ class HelixPatchFileHandler(PatchFileHandler):
38
41
  capture: bool = False,
39
42
  log_output: bool = True,
40
43
  ) -> subprocess.CompletedProcess[str]:
44
+ if getattr(sys, "frozen", False):
45
+ return self._run_in_process(*args, capture=capture, log_output=log_output)
46
+
41
47
  should_capture = capture or self.log_callback is not None
42
48
 
43
49
  try:
@@ -59,6 +65,54 @@ class HelixPatchFileHandler(PatchFileHandler):
59
65
  self._log_output(completed.stderr)
60
66
  return completed
61
67
 
68
+ def _run_in_process(
69
+ self,
70
+ *args: object,
71
+ capture: bool = False,
72
+ log_output: bool = True,
73
+ ) -> subprocess.CompletedProcess[str]:
74
+ should_capture = capture or self.log_callback is not None
75
+ command = [str(self.script), *(str(arg) for arg in args)]
76
+ original_argv = sys.argv
77
+ stdout = io.StringIO()
78
+ stderr = io.StringIO()
79
+ sys.argv = command
80
+ try:
81
+ stdout_context = (
82
+ contextlib.redirect_stdout(stdout) if should_capture else contextlib.nullcontext()
83
+ )
84
+ stderr_context = (
85
+ contextlib.redirect_stderr(stderr) if should_capture else contextlib.nullcontext()
86
+ )
87
+ with stdout_context, stderr_context:
88
+ try:
89
+ runpy.run_path(str(self.script), run_name="__main__")
90
+ returncode = 0
91
+ except SystemExit as exc:
92
+ returncode = exc.code if isinstance(exc.code, int) else 1
93
+ finally:
94
+ sys.argv = original_argv
95
+
96
+ output = stdout.getvalue() if should_capture else None
97
+ error = stderr.getvalue() if should_capture else None
98
+ completed = subprocess.CompletedProcess(command, returncode, stdout=output, stderr=error)
99
+ if completed.returncode:
100
+ exc = subprocess.CalledProcessError(
101
+ completed.returncode,
102
+ command,
103
+ output=completed.stdout,
104
+ stderr=completed.stderr,
105
+ )
106
+ if log_output:
107
+ self._log_output(exc.stdout)
108
+ self._log_output(exc.stderr)
109
+ raise exc
110
+
111
+ if log_output:
112
+ self._log_output(completed.stdout)
113
+ self._log_output(completed.stderr)
114
+ return completed
115
+
62
116
  def _log_output(self, output: str | None) -> None:
63
117
  if self.log_callback is None or not output:
64
118
  return
@@ -72,9 +72,6 @@ class HelixSettingsPanel(QWidget):
72
72
  self.steering_output = QLineEdit()
73
73
  self.steering_channel = QSpinBox()
74
74
  self.steering_channel.setRange(0, 15)
75
- self.preset_wait = QLineEdit()
76
- self.snapshot_wait = QLineEdit()
77
- self.measurement_wait = QLineEdit()
78
75
  steering.addRow(
79
76
  _label("MIDI output", "MIDI port substring used to find the connected Helix."),
80
77
  self.steering_output,
@@ -83,20 +80,6 @@ class HelixSettingsPanel(QWidget):
83
80
  _label("MIDI channel", "Zero-based MIDI channel used for preset and snapshot changes."),
84
81
  self.steering_channel,
85
82
  )
86
- steering.addRow(
87
- _label("Preset wait (s)", "Pause after switching presets before continuing."),
88
- self.preset_wait,
89
- )
90
- steering.addRow(
91
- _label("Snapshot wait (s)", "Pause after switching snapshots before continuing."),
92
- self.snapshot_wait,
93
- )
94
- steering.addRow(
95
- _label(
96
- "Measurement wait (s)", "Pause before capturing loudness after a snapshot change."
97
- ),
98
- self.measurement_wait,
99
- )
100
83
 
101
84
  def populate(self, args: argparse.Namespace) -> None:
102
85
  self.audio_device.setText(_text(args.audio_device or "Helix"))
@@ -106,13 +89,6 @@ class HelixSettingsPanel(QWidget):
106
89
  self.blocksize.setValue(args.blocksize or 0)
107
90
  self.steering_output.setText(_text(args.steering_output or "Helix"))
108
91
  self.steering_channel.setValue(args.steering_channel or 0)
109
- self.preset_wait.setText(_text(args.preset_wait if args.preset_wait is not None else 0.5))
110
- self.snapshot_wait.setText(
111
- _text(args.snapshot_wait if args.snapshot_wait is not None else 0.2)
112
- )
113
- self.measurement_wait.setText(
114
- _text(args.measurement_wait if args.measurement_wait is not None else 0.1)
115
- )
116
92
 
117
93
  def append_arguments(self, argv: list[str]) -> None:
118
94
  _append(argv, "--audio-device", self.audio_device.text())
@@ -122,9 +98,6 @@ class HelixSettingsPanel(QWidget):
122
98
  _append(argv, "--blocksize", self.blocksize.value())
123
99
  _append(argv, "--steering-output", self.steering_output.text())
124
100
  _append(argv, "--steering-channel", self.steering_channel.value())
125
- _append(argv, "--preset-wait", self.preset_wait.text())
126
- _append(argv, "--snapshot-wait", self.snapshot_wait.text())
127
- _append(argv, "--measurement-wait", self.measurement_wait.text())
128
101
 
129
102
 
130
103
  def _append(argv: list[str], name: str, value: object) -> None:
@@ -2,6 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import sys
5
6
  from pathlib import Path
6
7
 
7
8
  from PySide6.QtCore import Qt
@@ -21,7 +22,26 @@ from matchpatch import __version__
21
22
  from matchpatch.gui.help import HelpId, resolve_help_url
22
23
 
23
24
  PROJECT_URL = "https://github.com/noseglasses/MatchPatch"
24
- ASSETS_DIR = Path(__file__).resolve().parents[3] / "docs" / "assets"
25
+ SOURCE_ROOT = Path(__file__).resolve().parents[3]
26
+
27
+
28
+ def resource_path(*parts: str) -> Path:
29
+ relative_path = Path(*parts)
30
+ candidates: list[Path] = []
31
+ meipass = getattr(sys, "_MEIPASS", None)
32
+ if getattr(sys, "frozen", False):
33
+ if meipass:
34
+ candidates.append(Path(meipass) / relative_path)
35
+ candidates.append(Path(sys.executable).resolve().parent / relative_path)
36
+ candidates.append(SOURCE_ROOT / relative_path)
37
+
38
+ for candidate in candidates:
39
+ if candidate.exists():
40
+ return candidate
41
+ return candidates[0]
42
+
43
+
44
+ ASSETS_DIR = resource_path("docs", "assets")
25
45
 
26
46
 
27
47
  def _about_icon_blue(size: int) -> QColor:
@@ -101,7 +101,14 @@ from PySide6.QtWidgets import (
101
101
  QWidget,
102
102
  )
103
103
 
104
- from matchpatch.config import Config, config_value, default_config, export_config, load_config
104
+ from matchpatch.config import (
105
+ Config,
106
+ config_value,
107
+ default_config,
108
+ default_config_path,
109
+ export_config,
110
+ load_config,
111
+ )
105
112
  from matchpatch.custom_adjustments import CustomAdjustments, load_custom_adjustments_file
106
113
  from matchpatch.devices import get_device_profile, list_device_profiles
107
114
  from matchpatch.devices.base import (
@@ -2646,7 +2653,7 @@ class MainWindow(QMainWindow):
2646
2653
  dialog.setAcceptMode(QFileDialog.AcceptMode.AcceptSave)
2647
2654
  dialog.setFileMode(QFileDialog.FileMode.AnyFile)
2648
2655
  dialog.setNameFilter("TOML (*.toml)")
2649
- dialog.selectFile(str(Path(self.config_path.text().strip() or "matchpatch.toml")))
2656
+ dialog.selectFile(str(Path(self.config_path.text().strip() or default_config_path())))
2650
2657
  dialog.setLabelText(QFileDialog.DialogLabel.Accept, "Save")
2651
2658
  save_default = QCheckBox("Save default configuration", dialog)
2652
2659
  save_default.setChecked(False)