Skip to main content

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
  • --channels chooses the MIDI channels whose notes may be shortened and whose CC64 stream is regenerated. Other channels and their pedal events remain unchanged.
  • --tracks chooses 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:

  1. Every note is paired with its input note and its simulated sounding end is compared in real time, through tempo changes.
  2. 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

repedal-0.1.0.tar.gz (36.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

repedal-0.1.0-py3-none-any.whl (32.8 kB view details)

Uploaded Python 3

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

Hashes for repedal-0.1.0.tar.gz
Algorithm Hash digest
SHA256 79f169099e21109923f666065e33cf3733b92e4ecb780f85d0ec0cbcf08e9a29
MD5 e4f4864f21a388a2318b419e4c676732
BLAKE2b-256 8b1d246aebc06548e261b74d050e622444fd4aff449c4af67d25a7fb0da50b49

See more details on using hashes here.

Provenance

The following attestation bundles were made for repedal-0.1.0.tar.gz:

Publisher: publish.yml on ssmall256/repedal

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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

Hashes for repedal-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 25ed781bc8fa16afa18c2bd5d09a359346b518052a30b5fce28e9bd1eaa36bb2
MD5 610d2cf9f7ac161a28c6aaea4f6e2929
BLAKE2b-256 b09adb0e778fc9ab1cf5600b0ef93dcbb9f7c5184e3356c54bf8395006f41b89

See more details on using hashes here.

Provenance

The following attestation bundles were made for repedal-0.1.0-py3-none-any.whl:

Publisher: publish.yml on ssmall256/repedal

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page