repedal 0.1.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.
@@ -0,0 +1,16 @@
1
+ # Changelog
2
+
3
+ All notable changes to repedal are documented here. The project follows
4
+ [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
5
+
6
+ ## [0.1.0] - 2026-09-02
7
+
8
+ Initial public release.
9
+
10
+ - Reconstruct damper-pedal and optional sostenuto data from over-held piano MIDI notes.
11
+ - Offer legato, voice-tracking, hand-span, and fixed-duration articulation models.
12
+ - Preserve unmanaged MIDI events and verify sounding ends before writing atomically.
13
+ - Select channels and tracks while keeping channel-wide pedal safety intact.
14
+ - Preview conversions, compare articulation models, and emit versioned JSON summaries.
15
+
16
+ [0.1.0]: https://github.com/ssmall256/repedal/releases/tag/v0.1.0
repedal-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ssmall256
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ include CHANGELOG.md
repedal-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,347 @@
1
+ Metadata-Version: 2.4
2
+ Name: repedal
3
+ Version: 0.1.0
4
+ Summary: Turn manually over-held MIDI piano notes into real CC64 sustain-pedal data
5
+ Author: ssmall256
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/ssmall256/repedal
8
+ Project-URL: Repository, https://github.com/ssmall256/repedal
9
+ Project-URL: Issues, https://github.com/ssmall256/repedal/issues
10
+ Project-URL: Changelog, https://github.com/ssmall256/repedal/blob/main/CHANGELOG.md
11
+ Keywords: midi,piano,sustain,pedal,cc64,articulation
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: End Users/Desktop
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Topic :: Multimedia :: Sound/Audio :: MIDI
22
+ Requires-Python: >=3.9
23
+ Description-Content-Type: text/markdown
24
+ License-File: LICENSE
25
+ Requires-Dist: mido>=1.2
26
+ Dynamic: license-file
27
+
28
+ # repedal
29
+
30
+ Most DAWs can flatten sustain-pedal (CC64) data into long MIDI note lengths. Going the
31
+ other way—recovering plausible key releases and real pedal data from those long notes—usually
32
+ requires a custom script. `repedal` does that conversion for piano MIDI.
33
+
34
+ It infers when the player's fingers could have left each key, shortens the MIDI notes to those
35
+ times, and writes damper-pedal events that preserve the input's sounding ends. The assembled
36
+ result is parsed again and simulated before anything is written.
37
+
38
+ ```bash
39
+ python -m pip install repedal
40
+ repedal gymnopedie.mid
41
+ ```
42
+
43
+ ```text
44
+ gymnopedie.mid -> gymnopedie.pedaled.mid
45
+ notes 282 (125 shortened, 157 left as written)
46
+ finger-sustain moved to the pedal: median 857ms per note, 159.3s total
47
+ pedal presses 71 damper (CC64)
48
+ pedal down ch0 43%, ch1 89% of the 121s piece
49
+ handover 21ms minimum before a key release
50
+ verification worst sounding-end shift 0ms, 0 note(s) over the 120ms tolerance; exact
51
+ ```
52
+
53
+ Python 3.9+ is required; [`mido`](https://mido.readthedocs.io) is installed automatically.
54
+ From a source checkout, use `python -m pip install .` to install the command and importable
55
+ module. Installing `mido` alone is enough to run `python repedal.py` directly.
56
+
57
+ Repedal is a public beta. The Python API and versioned JSON schema are intended for integration;
58
+ the prose reports printed for people may become clearer over time and should not be parsed by
59
+ software.
60
+
61
+ ## Preview before writing
62
+
63
+ Pedal reconstruction is underdetermined: a long note might have been held by a finger or by a
64
+ pedal. Preview the chosen interpretation before committing to it:
65
+
66
+ ```bash
67
+ repedal piece.mid --dry-run --report
68
+ ```
69
+
70
+ `--dry-run` prints the same verification summary as a real conversion, and `--report` adds
71
+ individual presses and rejection reasons. It never writes a file. To compare the three
72
+ inference models that make musical judgements:
73
+
74
+ ```bash
75
+ repedal piece.mid --compare-articulations
76
+ ```
77
+
78
+ ```text
79
+ piece.mid [articulation comparison; no file written]
80
+ mode shortened damper sostenuto rejected worst shift status
81
+ legato 125 71 0 0 0ms exact
82
+ voices 33 33 0 0 0ms exact
83
+ hands 26 26 0 0 0ms exact
84
+ ```
85
+
86
+ ### Machine-readable output
87
+
88
+ Use `--json` when another program needs the result:
89
+
90
+ ```bash
91
+ repedal piece.mid --dry-run --json
92
+ ```
93
+
94
+ The command prints exactly one JSON object after its arguments have parsed successfully. Schema
95
+ version 1 has stable field names; fields may be added compatibly, while a removal or rename will
96
+ use a new `schema_version`. Warnings are included in the object instead of being printed
97
+ separately. `--json` and the prose-oriented `--report` cannot be combined.
98
+
99
+ ```json
100
+ {
101
+ "input": "piece.mid",
102
+ "mode": "dry-run",
103
+ "output": null,
104
+ "result": {
105
+ "notes": {"shortened": 125, "total": 282, "unchanged": 157},
106
+ "pedal": {"damper_presses": 71, "rejected_presses": 0, "sostenuto_presses": 0},
107
+ "verification": {
108
+ "base_tolerance_ms": 120.0,
109
+ "exact": true,
110
+ "fixed_event_errors": 0,
111
+ "out_of_tolerance_notes": 0,
112
+ "unmatched_notes": 0,
113
+ "within_tolerance": true,
114
+ "worst_shift_ms": 0.0
115
+ },
116
+ "warnings": []
117
+ },
118
+ "schema_version": 1,
119
+ "status": "ok",
120
+ "written": false
121
+ }
122
+ ```
123
+
124
+ For example, a subprocess consumer can replace regular-expression parsing with:
125
+
126
+ ```python
127
+ import json
128
+ import subprocess
129
+
130
+ completed = subprocess.run(
131
+ ["repedal", "piece.mid", "--dry-run", "--json"],
132
+ capture_output=True,
133
+ check=False,
134
+ text=True,
135
+ )
136
+ payload = json.loads(completed.stdout)
137
+ shortened = payload["result"]["notes"]["shortened"]
138
+ presses = payload["result"]["pedal"]["damper_presses"]
139
+ ```
140
+
141
+ Exit status 0 means the requested operation completed, 1 means an input, configuration, or write
142
+ error, and 2 means verification was unsafe and no output was written. Articulation comparisons
143
+ still exit 0 when every requested analysis completes; inspect their JSON `status` fields for
144
+ safety. A deliberate `--allow-lossy` write also exits 0 and reports `"status": "unsafe"`.
145
+
146
+ ## Choosing an articulation model
147
+
148
+ `--articulation` controls how finger releases are inferred:
149
+
150
+ | Mode | Behaviour |
151
+ | --- | --- |
152
+ | `legato` *(default)* | Releases an attack group when the next selected attack group arrives. This recovers the most pedal from ordinary homophonic piano writing. |
153
+ | `voices` | Tracks non-crossing melodic streams and releases a note when its own stream moves on. A melody held over a moving bass therefore stays finger-held longer. |
154
+ | `hands` | Releases notes only when the held keys exceed `--max-fingers` or `--hand-span`. This is the most conservative musical model. |
155
+ | `fixed` | Uses only `--max-hold-beats` and/or `--max-hold-ms`. With neither cap, it is a no-op articulation model. |
156
+
157
+ `voices` uses a self-contained, pitch-ordered dynamic program; it does not require `music21`.
158
+ All attack grouping and hand/voice inference is limited to the selected notes.
159
+
160
+ ## Selecting channels and tracks
161
+
162
+ Real MIDI files often contain drums, orchestration, or reference tracks that should not affect
163
+ piano articulation. Selection indexes are zero-based, matching `mido` and repedal's reports:
164
+
165
+ ```bash
166
+ repedal arrangement.mid --channels 0,1
167
+ repedal arrangement.mid --tracks 2,3
168
+ ```
169
+
170
+ - `--channels` chooses the MIDI channels whose notes may be shortened and whose CC64 stream is
171
+ regenerated. Other channels and their pedal events remain unchanged.
172
+ - `--tracks` chooses which tracks supply notes eligible for shortening. Because MIDI pedal is
173
+ channel-wide, unselected notes on the same channel still participate in safety simulation.
174
+ - General MIDI percussion channel 9 is ignored by default and reported as a warning. Use
175
+ `--include-percussion`, or select channel 9 explicitly, to process it.
176
+ - A selected channel declaring a non-piano General MIDI program is reported as a warning. The
177
+ conversion continues because program maps are conventions, not proof of instrumentation.
178
+
179
+ MIDI type 0 and type 1 files are supported. Type 2 files contain independent sequences with no
180
+ single shared timeline, so repedal rejects them; split them into separate type 0 or type 1 files
181
+ first.
182
+
183
+ ## Pedal scope and existing pedal
184
+
185
+ CC64 is a per-channel controller. `--pedal-scope channel` (the default) plans each selected
186
+ channel independently, allowing a bass channel to be pedalled without smearing a melody
187
+ channel. `--pedal-scope global` derives one shared pedal plan and copies it to the selected
188
+ channels, which is appropriate when those channels will be flattened onto one instrument.
189
+
190
+ A global plan cannot preserve different existing pedal streams on different channels. Repedal
191
+ rejects that combination unless `--existing-pedal ignore` is used or channel scope is selected.
192
+
193
+ `--existing-pedal` controls existing CC64 on processed channels:
194
+
195
+ | Value | Behaviour |
196
+ | --- | --- |
197
+ | `keep` *(default)* | Treats existing pedal as authoritative, keeps its sounding result, and adds any required coverage. |
198
+ | `replace` | Reconstructs pedal from what the input currently sounds like. |
199
+ | `ignore` | Discards existing CC64 on processed channels and interprets written note lengths literally. |
200
+
201
+ Existing CC66 is always part of the input sound model. A newly proposed sostenuto span is
202
+ rejected if it overlaps existing CC66, because a piano has only one middle pedal.
203
+
204
+ ## Verification and write safety
205
+
206
+ Repedal verifies two things after assembling the output:
207
+
208
+ 1. Every note is paired with its input note and its simulated sounding end is compared in real
209
+ time, through tempo changes.
210
+ 2. Every event outside the documented transformation surface retains its track, absolute tick,
211
+ payload, and same-tick order. Note-off timing/velocity and CC64/CC66 on processed channels are
212
+ the only managed event classes.
213
+
214
+ The default `--blur-tol-ms 120` permits a musically bounded amount of over-ring when exact pedal
215
+ expression is impossible. This means “within tolerance” is not necessarily tick-exact.
216
+ `--strict` sets that tolerance to zero and requires exact sounding ends.
217
+
218
+ If verification exceeds the configured tolerance, the CLI prints the analysis, exits with
219
+ status 2, and does **not** write an output file. Library saves behave the same way. Output is
220
+ written through a temporary file and atomically moved into place only after validation.
221
+
222
+ `--allow-lossy` (or `Result.save(..., allow_lossy=True)`) is the explicit escape hatch for an
223
+ analysed result that the caller has decided to accept.
224
+
225
+ ## Using it as a library
226
+
227
+ The CLI is a wrapper over the public API:
228
+
229
+ ```python
230
+ from repedal import Options, convert, convert_file
231
+
232
+ result = convert("in.mid", Options(articulation="voices", channels=(0, 1)))
233
+ print(result.summary())
234
+ if result.exact:
235
+ result.save("out.mid")
236
+
237
+ # Converts, verifies, and atomically writes. Raises ValueError if verification fails.
238
+ result = convert_file("in.mid", "out.mid")
239
+ ```
240
+
241
+ `convert` also accepts an already loaded `mido.MidiFile`. It reads but does not modify that
242
+ object:
243
+
244
+ ```python
245
+ import mido
246
+ from repedal import convert
247
+
248
+ source = mido.MidiFile("in.mid")
249
+ result = convert(source)
250
+ extra_track = mido.MidiTrack()
251
+ result.midi.tracks.append(extra_track)
252
+ result.save("out.mid")
253
+ ```
254
+
255
+ A `Result` exposes both the file and the evidence behind it:
256
+
257
+ | Attribute | Contents |
258
+ | --- | --- |
259
+ | `midi` | Finished `mido.MidiFile` in memory. |
260
+ | `within_tolerance` / `lossless` | Whether verification satisfies the configured tolerance. `lossless` remains as a compatibility alias. |
261
+ | `exact` | Whether every sounding end is tick-exact and all fixed events match. |
262
+ | `presses` / `rejected` | Written and abandoned `Press` objects; rejected presses carry a `.reason`. |
263
+ | `presses[i].kind` | `"damper"` (CC64) or `"sostenuto"` (CC66). |
264
+ | `notes` / `shortened` | Parsed `Note` objects with `start`, `end`, `sound_end`, `key_end`, and `new_end`. |
265
+ | `deviations` / `out_of_tolerance` | `Deviation` objects containing `.ticks`, `.seconds`, and `.note`. |
266
+ | `event_errors` | Any change found outside managed note-off and pedal events. |
267
+ | `warnings` | Input preflight or planning warnings. |
268
+ | `min_handover_seconds` | Smallest press-to-key-release margin. |
269
+ | `summary()` / `report()` | The concise and detailed CLI-style reports. |
270
+
271
+ For analysis without output assembly, use `read_score` and `plan`:
272
+
273
+ ```python
274
+ from repedal import Options, plan, read_score
275
+
276
+ warnings = []
277
+ score = read_score("in.mid")
278
+ target, scopes = plan(score, Options(channels=(0,)), warnings)
279
+ for note in score.notes[:5]:
280
+ print(note.pitch, note.start, note.sound_end, "->", note.key_end)
281
+ ```
282
+
283
+ ## Sostenuto for pedal points
284
+
285
+ Damper pedal sustains every released key on its channel. It cannot express a long bass pedal
286
+ point while upper chords on that channel remain detached. `--sostenuto` allows repedal to move
287
+ eligible cases to CC66, which captures only keys held when the middle pedal is pressed:
288
+
289
+ ```text
290
+ input 0:on36 240:on60,64,67 440:off60,64,67 1920:off36
291
+ output 0:on36 0:CC66=127 240:off36 240:on60,64,67 1920:CC66=0
292
+ ```
293
+
294
+ The capture set, existing CC66, and other generated spans are included in the final simulation.
295
+ Use this only with instruments that implement CC66.
296
+
297
+ ## Pedal timing and physical controls
298
+
299
+ `--pedal-lag-ms 50` delays a press after its associated attack, reproducing syncopated
300
+ pedalling: strings are struck with the dampers down, and the foot follows. The requested lag
301
+ shrinks when necessary to catch the first key release safely.
302
+
303
+ `--min-handover-ms` (default 20) is the minimum intended margin between a press and the key
304
+ release it catches. The margin protects frame-sampled renderers from quantising both events
305
+ into one update. The achieved minimum is reported; dense textures can leave less room than
306
+ requested.
307
+
308
+ `--change-gap-ms` adds time between a lift and the next press. `--damping-ramp-ms` replaces an
309
+ instantaneous lift with four sub-threshold values (63 → 42 → 21 → 0) for modelled pianos that
310
+ follow continuous CC64. Switch-style instruments still see the lift at the first value below
311
+ 64. `--release-velocity` changes the note-off velocity only for shortened keys.
312
+
313
+ Every millisecond option is converted through the complete tempo map rather than sampled at one
314
+ tempo. A 50 ms setting therefore remains 50 ms across accelerando or ritardando even though its
315
+ tick length changes.
316
+
317
+ `--blur-tol-alpha` can make the over-ring tolerance stricter in the bass and looser in the
318
+ treble. With a 120 ms base and alpha 1.5, the tolerance is approximately 15 ms at C2, 42 ms at
319
+ C3, and 339 ms at C5. `--min-note-ms`, `--min-shorten-ms`, and `--min-pedal-ms` suppress
320
+ implausibly short notes, edits, and presses.
321
+
322
+ Run `repedal --help` for the complete option list.
323
+
324
+ ## Development
325
+
326
+ The regression suite covers event-order preservation, generated/existing sostenuto conflicts,
327
+ safe atomic writes, mixed-channel selection, percussion handling, type-2 rejection, preview
328
+ commands, and randomized no-op files:
329
+
330
+ ```bash
331
+ python -m unittest discover -s tests -v
332
+ ```
333
+
334
+ The same suite runs in CI on every Python version from 3.9 through 3.13. CI also builds and checks
335
+ the wheel and source distribution, then installs each into a clean environment.
336
+
337
+ ## Versioning and license
338
+
339
+ Repedal follows semantic versioning. During the 0.x public beta, minor releases may revise the
340
+ Python API; changes are documented in the
341
+ [changelog](https://github.com/ssmall256/repedal/blob/main/CHANGELOG.md). The JSON interface is
342
+ independently versioned by its `schema_version` as described above.
343
+
344
+ Repedal is released under the
345
+ [MIT License](https://github.com/ssmall256/repedal/blob/main/LICENSE). Source, issues, and
346
+ releases are hosted at
347
+ [github.com/ssmall256/repedal](https://github.com/ssmall256/repedal).