repedal
Most DAWs can flatten sustain-pedal (CC64) data into long MIDI note lengths. Going the
other way—recovering plausible key releases and real pedal data from those long notes—usually
requires a custom script. repedal does that conversion for piano MIDI.
It infers when the player's fingers could have left each key, shortens the MIDI notes to those times, and writes damper-pedal events that preserve the input's sounding ends. The assembled result is parsed again and simulated before anything is written.
python -m pip install repedal
repedal gymnopedie.mid
gymnopedie.mid -> gymnopedie.pedaled.mid
notes 282 (125 shortened, 157 left as written)
finger-sustain moved to the pedal: median 857ms per note, 159.3s total
pedal presses 71 damper (CC64)
pedal down ch0 43%, ch1 89% of the 121s piece
handover 21ms minimum before a key release
verification worst sounding-end shift 0ms, 0 note(s) over the 120ms tolerance; exact
Python 3.9+ is required; mido is installed automatically.
From a source checkout, use python -m pip install . to install the command and importable
module. Installing mido alone is enough to run python repedal.py directly.
Repedal is a public beta. The Python API and versioned JSON schema are intended for integration; the prose reports printed for people may become clearer over time and should not be parsed by software.
Preview before writing
Pedal reconstruction is underdetermined: a long note might have been held by a finger or by a pedal. Preview the chosen interpretation before committing to it:
repedal piece.mid --dry-run --report
--dry-run prints the same verification summary as a real conversion, and --report adds
individual presses and rejection reasons. It never writes a file. To compare the three
inference models that make musical judgements:
repedal piece.mid --compare-articulations
piece.mid [articulation comparison; no file written]
mode shortened damper sostenuto rejected worst shift status
legato 125 71 0 0 0ms exact
voices 33 33 0 0 0ms exact
hands 26 26 0 0 0ms exact
Machine-readable output
Use --json when another program needs the result:
repedal piece.mid --dry-run --json
The command prints exactly one JSON object after its arguments have parsed successfully. Schema
version 1 has stable field names; fields may be added compatibly, while a removal or rename will
use a new schema_version. Warnings are included in the object instead of being printed
separately. --json and the prose-oriented --report cannot be combined.
{
"input": "piece.mid",
"mode": "dry-run",
"output": null,
"result": {
"notes": {"shortened": 125, "total": 282, "unchanged": 157},
"pedal": {"damper_presses": 71, "rejected_presses": 0, "sostenuto_presses": 0},
"verification": {
"base_tolerance_ms": 120.0,
"exact": true,
"fixed_event_errors": 0,
"out_of_tolerance_notes": 0,
"unmatched_notes": 0,
"within_tolerance": true,
"worst_shift_ms": 0.0
},
"warnings": []
},
"schema_version": 1,
"status": "ok",
"written": false
}
For example, a subprocess consumer can replace regular-expression parsing with:
import json
import subprocess
completed = subprocess.run(
["repedal", "piece.mid", "--dry-run", "--json"],
capture_output=True,
check=False,
text=True,
)
payload = json.loads(completed.stdout)
shortened = payload["result"]["notes"]["shortened"]
presses = payload["result"]["pedal"]["damper_presses"]
Exit status 0 means the requested operation completed, 1 means an input, configuration, or write
error, and 2 means verification was unsafe and no output was written. Articulation comparisons
still exit 0 when every requested analysis completes; inspect their JSON status fields for
safety. A deliberate --allow-lossy write also exits 0 and reports "status": "unsafe".
Choosing an articulation model
--articulation controls how finger releases are inferred:
| Mode | Behaviour |
|---|---|
legato (default) |
Releases an attack group when the next selected attack group arrives. This recovers the most pedal from ordinary homophonic piano writing. |
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. |
hands |
Releases notes only when the held keys exceed --max-fingers or --hand-span. This is the most conservative musical model. |
fixed |
Uses only --max-hold-beats and/or --max-hold-ms. With neither cap, it is a no-op articulation model. |
voices uses a self-contained, pitch-ordered dynamic program; it does not require music21.
All attack grouping and hand/voice inference is limited to the selected notes.
Selecting channels and tracks
Real MIDI files often contain drums, orchestration, or reference tracks that should not affect
piano articulation. Selection indexes are zero-based, matching mido and repedal's reports:
repedal arrangement.mid --channels 0,1
repedal arrangement.mid --tracks 2,3
--channelschooses the MIDI channels whose notes may be shortened and whose CC64 stream is regenerated. Other channels and their pedal events remain unchanged.--trackschooses which tracks supply notes eligible for shortening. Because MIDI pedal is channel-wide, unselected notes on the same channel still participate in safety simulation.- General MIDI percussion channel 9 is ignored by default and reported as a warning. Use
--include-percussion, or select channel 9 explicitly, to process it. - A selected channel declaring a non-piano General MIDI program is reported as a warning. The conversion continues because program maps are conventions, not proof of instrumentation.
MIDI type 0 and type 1 files are supported. Type 2 files contain independent sequences with no single shared timeline, so repedal rejects them; split them into separate type 0 or type 1 files first.
Pedal scope and existing pedal
CC64 is a per-channel controller. --pedal-scope channel (the default) plans each selected
channel independently, allowing a bass channel to be pedalled without smearing a melody
channel. --pedal-scope global derives one shared pedal plan and copies it to the selected
channels, which is appropriate when those channels will be flattened onto one instrument.
A global plan cannot preserve different existing pedal streams on different channels. Repedal
rejects that combination unless --existing-pedal ignore is used or channel scope is selected.
--existing-pedal controls existing CC64 on processed channels:
| Value | Behaviour |
|---|---|
keep (default) |
Treats existing pedal as authoritative, keeps its sounding result, and adds any required coverage. |
replace |
Reconstructs pedal from what the input currently sounds like. |
ignore |
Discards existing CC64 on processed channels and interprets written note lengths literally. |
Existing CC66 is always part of the input sound model. A newly proposed sostenuto span is rejected if it overlaps existing CC66, because a piano has only one middle pedal.
Verification and write safety
Repedal verifies two things after assembling the output:
- Every note is paired with its input note and its simulated sounding end is compared in real time, through tempo changes.
- Every event outside the documented transformation surface retains its track, absolute tick, payload, and same-tick order. Note-off timing/velocity and CC64/CC66 on processed channels are the only managed event classes.
The default --blur-tol-ms 120 permits a musically bounded amount of over-ring when exact pedal
expression is impossible. This means “within tolerance” is not necessarily tick-exact.
--strict sets that tolerance to zero and requires exact sounding ends.
If verification exceeds the configured tolerance, the CLI prints the analysis, exits with status 2, and does not write an output file. Library saves behave the same way. Output is written through a temporary file and atomically moved into place only after validation.
--allow-lossy (or Result.save(..., allow_lossy=True)) is the explicit escape hatch for an
analysed result that the caller has decided to accept.
Using it as a library
The CLI is a wrapper over the public API:
from repedal import Options, convert, convert_file
result = convert("in.mid", Options(articulation="voices", channels=(0, 1)))
print(result.summary())
if result.exact:
result.save("out.mid")
# Converts, verifies, and atomically writes. Raises ValueError if verification fails.
result = convert_file("in.mid", "out.mid")
convert also accepts an already loaded mido.MidiFile. It reads but does not modify that
object:
import mido
from repedal import convert
source = mido.MidiFile("in.mid")
result = convert(source)
extra_track = mido.MidiTrack()
result.midi.tracks.append(extra_track)
result.save("out.mid")
A Result exposes both the file and the evidence behind it:
| Attribute | Contents |
|---|---|
midi |
Finished mido.MidiFile in memory. |
within_tolerance / lossless |
Whether verification satisfies the configured tolerance. lossless remains as a compatibility alias. |
exact |
Whether every sounding end is tick-exact and all fixed events match. |
presses / rejected |
Written and abandoned Press objects; rejected presses carry a .reason. |
presses[i].kind |
"damper" (CC64) or "sostenuto" (CC66). |
notes / shortened |
Parsed Note objects with start, end, sound_end, key_end, and new_end. |
deviations / out_of_tolerance |
Deviation objects containing .ticks, .seconds, and .note. |
event_errors |
Any change found outside managed note-off and pedal events. |
warnings |
Input preflight or planning warnings. |
min_handover_seconds |
Smallest press-to-key-release margin. |
summary() / report() |
The concise and detailed CLI-style reports. |
For analysis without output assembly, use read_score and plan:
from repedal import Options, plan, read_score
warnings = []
score = read_score("in.mid")
target, scopes = plan(score, Options(channels=(0,)), warnings)
for note in score.notes[:5]:
print(note.pitch, note.start, note.sound_end, "->", note.key_end)
Sostenuto for pedal points
Damper pedal sustains every released key on its channel. It cannot express a long bass pedal
point while upper chords on that channel remain detached. --sostenuto allows repedal to move
eligible cases to CC66, which captures only keys held when the middle pedal is pressed:
input 0:on36 240:on60,64,67 440:off60,64,67 1920:off36
output 0:on36 0:CC66=127 240:off36 240:on60,64,67 1920:CC66=0
The capture set, existing CC66, and other generated spans are included in the final simulation. Use this only with instruments that implement CC66.
Pedal timing and physical controls
--pedal-lag-ms 50 delays a press after its associated attack, reproducing syncopated
pedalling: strings are struck with the dampers down, and the foot follows. The requested lag
shrinks when necessary to catch the first key release safely.
--min-handover-ms (default 20) is the minimum intended margin between a press and the key
release it catches. The margin protects frame-sampled renderers from quantising both events
into one update. The achieved minimum is reported; dense textures can leave less room than
requested.
--change-gap-ms adds time between a lift and the next press. --damping-ramp-ms replaces an
instantaneous lift with four sub-threshold values (63 → 42 → 21 → 0) for modelled pianos that
follow continuous CC64. Switch-style instruments still see the lift at the first value below
64. --release-velocity changes the note-off velocity only for shortened keys.
Every millisecond option is converted through the complete tempo map rather than sampled at one tempo. A 50 ms setting therefore remains 50 ms across accelerando or ritardando even though its tick length changes.
--blur-tol-alpha can make the over-ring tolerance stricter in the bass and looser in the
treble. With a 120 ms base and alpha 1.5, the tolerance is approximately 15 ms at C2, 42 ms at
C3, and 339 ms at C5. --min-note-ms, --min-shorten-ms, and --min-pedal-ms suppress
implausibly short notes, edits, and presses.
Run repedal --help for the complete option list.
Development
The regression suite covers event-order preservation, generated/existing sostenuto conflicts, safe atomic writes, mixed-channel selection, percussion handling, type-2 rejection, preview commands, and randomized no-op files:
python -m unittest discover -s tests -v
The same suite runs in CI on every Python version from 3.9 through 3.13. CI also builds and checks the wheel and source distribution, then installs each into a clean environment.
Versioning and license
Repedal follows semantic versioning. During the 0.x public beta, minor releases may revise the
Python API; changes are documented in the
changelog. The JSON interface is
independently versioned by its schema_version as described above.
Repedal is released under the MIT License. Source, issues, and releases are hosted at github.com/ssmall256/repedal.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file repedal-0.1.0.tar.gz.
File metadata
- Download URL: repedal-0.1.0.tar.gz
- Upload date:
- Size: 36.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
79f169099e21109923f666065e33cf3733b92e4ecb780f85d0ec0cbcf08e9a29
|
|
| MD5 |
e4f4864f21a388a2318b419e4c676732
|
|
| BLAKE2b-256 |
8b1d246aebc06548e261b74d050e622444fd4aff449c4af67d25a7fb0da50b49
|
Provenance
The following attestation bundles were made for repedal-0.1.0.tar.gz:
Publisher:
publish.yml on ssmall256/repedal
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
repedal-0.1.0.tar.gz -
Subject digest:
79f169099e21109923f666065e33cf3733b92e4ecb780f85d0ec0cbcf08e9a29 - Sigstore transparency entry: 2683736349
- Sigstore integration time:
-
Permalink:
ssmall256/repedal@8d749452cad63127795bbce5087f6a5b1299aeba -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/ssmall256
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@8d749452cad63127795bbce5087f6a5b1299aeba -
Trigger Event:
release
-
Statement type:
File details
Details for the file repedal-0.1.0-py3-none-any.whl.
File metadata
- Download URL: repedal-0.1.0-py3-none-any.whl
- Upload date:
- Size: 32.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
25ed781bc8fa16afa18c2bd5d09a359346b518052a30b5fce28e9bd1eaa36bb2
|
|
| MD5 |
610d2cf9f7ac161a28c6aaea4f6e2929
|
|
| BLAKE2b-256 |
b09adb0e778fc9ab1cf5600b0ef93dcbb9f7c5184e3356c54bf8395006f41b89
|
Provenance
The following attestation bundles were made for repedal-0.1.0-py3-none-any.whl:
Publisher:
publish.yml on ssmall256/repedal
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
repedal-0.1.0-py3-none-any.whl -
Subject digest:
25ed781bc8fa16afa18c2bd5d09a359346b518052a30b5fce28e9bd1eaa36bb2 - Sigstore transparency entry: 2683736383
- Sigstore integration time:
-
Permalink:
ssmall256/repedal@8d749452cad63127795bbce5087f6a5b1299aeba -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/ssmall256
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@8d749452cad63127795bbce5087f6a5b1299aeba -
Trigger Event:
release
-
Statement type: