Repository navigation
Releases: privacyfreak84/ScorePrep
Release list
v1.2.7
[1.2.7]
Fixed
-
--grid tripletmade the optimizer far too reluctant to sustain
notes, sounding rushed/choppy ("accelerated").artic_weight's cost
is documented as being per sixteenth-equivalent of invented duration,
but was actually applied per raw grid unit -- fine under the default
straight grid (a grid unit is a sixteenth there), but triplet mode's
grid unit is 3x finer, so the same absolute extension counted as 3x
more units and cost 3x more, making the optimizer choose far more
short/truncated notes with visible rests instead of natural sustain,
for no musical reason -- purely an accident of which grid happened to
be selected. Fixed by normalizing the fabrication cost by the grid's
scale factor. Found via a real piece: total invented-sustain duration
under--grid tripletwent from being ~12x lower than the equivalent
--grid straightrun (should be roughly comparable) to ~2x lower,
with the remaining gap explained by the finer grid legitimately being
able to close gaps more precisely rather than any remaining bias.
Confirmed byte-identical no-op for--grid straight(the default),
since its scale factor is 1 -- verified across both benchmark pieces
and the full tie-temperature/experimental-flag matrix. -
Non-384-PPQ sources were silently misquantized. Every tick position
was read straight off the source file and fed into grid/bar math that
assumesTICKS_PER_BEAT=384, with no actual rescaling -- the existing
NOTE: ... results may be offwarning undersold it badly. For a
480-PPQ source (a common DAW/export default) this meant a ~25% timing
distortion that compounds over the piece, since grid snapping doesn't
fail gracefully: what should be one beat gets treated as 1.25 beats,
every single onset. Found via a real piece (480 PPQ) that came out
"slow in some places, fast in others, not on time at all" -- root
cause wasn't tuplets as first suspected, it was this. Fixed by
rescaling every extracted tick (notes, sustain-pedal CC64 intervals)
byTICKS_PER_BEAT / source_ticks_per_beatright at load time, so
everything downstream operates in a consistent internal tick space
regardless of the source's own PPQ. Confirmed on the real piece: beat
count and real-world playback length now match the source to within
quantization rounding (previously off by 46%). Zero effect on
already-384-PPQ sources -- confirmed byte-identical against prior
output across both benchmark pieces, full TT sweep, all experimental
flag combinations. -
Removed
fix_same_pitch_overlapsand the redundant duration-optimizer
pass that followed it.optimize_staff_durationsalready bounds every
note's ceiling by its own next same-pitch onset, so same-pitch overlaps
were never actually possible after its first pass -- the "fix" step was
misfiring on true unisons (two notes of the same pitch starting at the
same instant) instead, truncating one of them to a single grid tick,
which the following optimizer pass then silently overwrote back to the
correct value. Net effect on real output: none -- confirmed
byte-identical against the prior behavior across both benchmark pieces,
a synthetic unison-heavy stress file, the full tie-temperature sweep,
and every experimental flag combination. Slightly faster (one fewer
full pass per staff) and removes a dead-code trap for future changes.
Added
-
--tuplet-detection auto|off(defaultoff, same experimental/
unvalidated status as--melody-preservation/--dynamic-split/
--hand-assignment). Finds local n-tuplet bursts (quintuplets,
septuplets, nontuplets, etc. against a 1- or 2-beat span) directly in
the source, independent of--grid, and snaps just those onsets to
exact even division instead of letting each one round independently
to the outer straight/triplet grid. Doubles as the auto-detection this
was originally scoped alongside: choosing the right subdivision locally
per onset-cluster is a strictly better fit than one global grid choice
for the whole piece, and directly replaces most of the reason to reach
for--grid tripletmanually. Full design:docs/tuplet-detection-design.md.Motivated by, and validated against, a real piece's ~9-onset burst that
used to quantize to jagged durations (64/64/96/64/96/96/128/96/128/160
ticks) despite the overall span being correct -- with detection on, the
first 5 of those 9 onsets now resolve to a clean, even ~77 ticks each.
Calibration note: an early version's fit-tolerance scaled with
span/N, which structurally favors smaller N (more absolute slack for
the same span) regardless of what's actually in the piece -- caught
empirically when quintuplets dominated detections even on pieces with
no independent evidence of being tuplet-heavy. Replaced with a fixed
24-tick tolerance, calibrated against real data: a piece independently
confirmed tuplet-dense (via its own notated score) shows 38 detected
groups at this threshold, versus 7 and 0 on two pieces with no such
evidence -- the best calibration signal available without ground-truth
tuplet annotations for every benchmark piece.benchmark_experimental.py
extended with a fourthtupletfeature for real A/B validation.
Confirmedoff(default) is byte-identical to pre-existing output;
autoruns clean across the full TT/grid/experimental-flag matrix.
Known v1 limitation: greedy left-to-right matching can settle for a
partial group (e.g. 5-of-9) instead of holding out for a better
whole-group fit -- named in the design doc, not a new surprise. -
Full tempo-track passthrough. Previously only the first
set_tempo
message in the source was read, and the output always got one flat
tempo marking for the whole piece -- any rubato/local tempo changes in
the source were silently discarded (contributing to the same "not on
time" symptom above, alongside the PPQ bug). Now, when--tempoisn't
given explicitly and the source has more than one genuine tempo event,
the full curve is preserved in the output instead of flattened.
Explicit--tempostill means one flat tempo, as before, to avoid
ambiguity about what "override one value out of a dozen" should mean.
v1.2.6
[1.2.6]
Added
- The diagnostic report now ends with a "Confidence Warnings" section
listing the bars where the experimental classifiers are least sure of
themselves -- combining low-confidence melody/accompaniment calls with
any hand-span violations from--hand-assignmentinto one "here's
where I might be wrong" view, worst first. No switch to turn on --
it's pure diagnostics, always shown when there's something worth
flagging, and can't change engraving output.
Fixed
hand_warningscould be referenced before assignment when
--hand-assignmentwas off, a latent bug from when that feature was
added -- caught while wiring up the confidence-warnings section above,
no observable effect before now since nothing read it in that path.--split-window-bars 0(or negative) crashed with a raw
ZeroDivisionError instead of a clear error message. Negative
--max-hand-span/--hand-ambiguity-zonewere silently accepted
instead of rejected. All three now validate up front with a proper
error message; found via stress-testing the new experimental features
against invalid/boundary inputs before shipping them.- The duration optimizer was undercounting how many tied noteheads a note
crossing a barline would actually need, for any tie-temperature above
0.0. It assumed a barline split always costs exactly one extra
notehead, but a split forces the pre-barline segment to whatever's
left until the barline -- which is often not a clean note value even
when the note's full duration would have been, and that remainder can
need extra noteheads of its own. In practice this meant some candidate
durations looked cheaper than they really were, so the optimizer
occasionally chose more cross-bar-tied notation than your
tie-temperature setting actually called for. Fixed by having it
properly split the note at each barline and count each resulting
piece on its own. Net effect on a real test piece: noticeably fewer
cross-bar ties at the same tie-temperature (e.g. TT 0.15: 230->178 and
286->199 across the two staves), i.e. cleaner notation than before at
the same settings.
Added
--hand-assignment on[experimental] reconsiders notes sitting close
to the treble/bass split point within each chord, instead of assigning
every note purely by which side of the line it falls on. A boundary
note moves to the other hand if it's actually closer to that hand's
recent position than to its naively-assigned hand's (e.g. "the left
hand is already busy an octave lower"), and only if doing so doesn't
stretch either hand's chord past--max-hand-span(default 16
semitones, a 10th). Notes not near the boundary are never touched.
Composes with either a fixed--split-pitchor--dynamic-split--
refines whichever boundary is already in place rather than replacing
it. Onsets that still need more than one hand's reach even after
reassignment are reported as a real, unavoidable stretch rather than
guessed at further. Off by default; available in interactive mode's
advanced options too.--dynamic-split onre-estimates the treble/bass split point every
--split-window-barsbars (default 8) instead of using one fixed
split for the whole piece, so the split follows the music's register
drifting over time (e.g. a verse sitting lower than a chorus). Off by
default. A sparse window falls back to the previous window's split
rather than a fresh guess, and the per-window change is capped at 4
semitones, so it drifts instead of jumping around. Available in
interactive mode's advanced options too.
v1.2.5
[1.2.5]
Added
--melody-preservation on[experimental] biases the duration
optimizer's weights per note using the voice-role classifier below:
melody gets cheaper ties and costlier rests (protect its continuity),
accompaniment gets the opposite (declutter more freely). Off by
default -- reuses the existing cost-minimizing optimizer rather than
adding a second decision path, so it's purely a weight bias, not a new
algorithm. Available in interactive mode's advanced options too. The
report's "Voice Roles" section says outright whether it actually
influenced a given run.- New internal
classify_voice_roles()pass tags every note as likely
"melody" or "accompaniment" with a confidence score, using a
skyline-plus-smoothing heuristic (highest simultaneous pitch, chord
density, note length, and relative velocity). This is groundwork for
future features (melody preservation, smarter hand assignment,
voice-aware quantization, confidence warnings), not a feature by
itself yet -- nothing about the actual engraving output changes.
Verified byte-identical output before/after on a real test file. A new
experimental "Voice Roles" section in the diagnostic report shows the
melody/accompaniment split and average confidence per staff, so the
heuristic can be checked against real pieces before anything is built
on top of it. --profile {readable,balanced,faithful}sets tie-temperature, pedal
mode, grid, and duration style all at once instead of tuning them
individually. "readable" favors fewest ties for sight-reading/practice,
"balanced" is the benchmark-tested sweet spot, "faithful" sticks
closest to the original performance. Available in interactive mode too.
Any of the four flags given explicitly still overrides the profile's
value for that flag.report()now ends with a "Measures Needing Attention" section listing
the measures with the most combined issues (chord conflicts, rests,
heavy tie chains, cross-bar ties, invented sustain), so you can jump
straight to the spots actually worth a manual look instead of scanning
the whole score. Diagnostics only -- doesn't change engraving output.
Decided
- Closed out the question of whether the duration optimizer needs a
voice-role-aware redesign (treating melody and accompaniment
differently) for a future v1.3. A benchmark suite spanning 10 pieces
across genres -- Bollywood, classical, jazz, film/anime, ambient --
found no case where melody read well but accompaniment stayed
cluttered in a way a global tie-temperature couldn't fix. The current
global-tie-temperature optimizer stays as-is; no code changes.
v1.2.4
Further improved statistics after running, which were introduced in v1.2.3
[1.2.4]
Added
report()now prints an===== Optimizer Decisions =====section per
staff: why each note's written length was chosen, as 4 mutually
exclusive categories (exact/no rest, rest kept, extended-but-truthful,
invented sustain). Required actually instrumenting
optimize_staff_durations's decision loop (not just reformatting an
existing report) -- added an optionalstatsdict parameter, populated
additively from values the loop already computes; the note-selection
logic itself is completely unchanged. Verified byte-identical output
MIDI before/after on a real test file. Only collected on each staff's
final optimization pass (after the same-pitch-overlap re-harmonize),
so the counts reflect the actual final decision, not an intermediate
one. Caught and fixed a real bug: fabrication is
computed as a sum across an entire chord, but crediting a whole
chord's worth of notes as "invented sustain" whenever that sum was
nonzero over-counted -- a 2-note chord where only one note actually
needed invented sustain was crediting both. Now categorized per
individual note within the event.
v1.2.3
Added better report statistics
[1.2.3]
Changed
- Reorganized the per-staff diagnostic report (
report()) into clearly
labeled sections (===== Staff Split =====,===== Notation (TREBLE/ BASS) =====) with a tie-count breakdown (none / one tie / two+ ties)
instead of one dense summary line. Pure diagnostics --report()only
reads already-finalized note data and prints, it doesn't mutate
anything, so this has no effect on the actual engraving decisions or
output MIDI (verified byte-identical before/after on a real test file).
Also fixed an off-by-one in the new tie-count buckets during
development:true_tie_countreturns notehead count (minimum 1, even
for a plain untied note), not tie count -- actual ties = noteheads - 1.
v1.2.2
Fixed custom tempo controls:
[1.2.2]
Fixed
- Changing
--tempoaway from the detected value silently had no
effect on real playback speed.rescale_notes_to_tempowas built
for raw MT3-style transcriptions, where ticks are laid out against a
meaningless placeholder tempo and a user-supplied tempo is meant to
correct that -- so ticks get stretched to compensate, keeping
real-world duration locked to the original audio regardless of the
number typed. That's correct for genuine MT3 input, but wrong for
already-accurate sources, where a
different--tempois a deliberate request to actually speed up or
slow down playback -- previously the stretch-and-relabel exactly
canceled out, so the result played at identical speed no matter what
tempo was entered. Added--tempo-rescale {preserve-duration, change-speed}(also prompted interactively when the entered tempo
differs from the detected one). Defaultpreserve-durationkeeps the
old MT3-oriented behavior;change-speedleaves ticks untouched so
the tempo change is real.
v1.2.1
v1.2.1
Fixed
Fixed a flaw in the v1.2 duration optimizer where ties required to truthfully notate real sustained notes were incorrectly penalized. This could cause the optimizer to truncate genuine note lengths simply to avoid ties, replacing them with unnecessary rests.
Ties required for a note's real, tie-budget-respecting duration are now treated as free. Only additional ties introduced to fabricate sustain beyond the original transcription carry a cost.
Re-verified tie-budget and barline invariants across the full temperature range and multiple time signatures (3/4, 4/4, 5/4, 6/8), with zero violations.
Tested with:
- Bollywood pop song (Kalank)
- Classical piece (Prelude in C# Minor)
Testing confirms significantly more faithful notation while preserving clean engraving and identical playback.
v1.2
Release notes — v1.2
Smarter duration engraving
Every note's written length used to be decided in three separate patch passes — snap to the largest clean value, force chords to agree, then flat-threshold-fill small gaps. It's now one decision: for each note (or chord), ScorePrep picks the duration that best balances ties, visible rests, and invented sustain (writing a note longer than it actually rang, to avoid a rest).
--tie-temperature still works exactly as before as your one main dial — low values avoid ties and favor small, cheap extensions over a rest; high values favor exact fidelity to the real timing, using ties freely instead of fabricating anything.
If you want more control, three new advanced flags let you tune each cost independently: --tie-weight, --rest-weight, --articulation-weight. Available in interactive mode too, tucked under advanced options.
--max-silent-gap is gone — the new optimizer replaces it with something that reasons per-note about the actual tradeoff, instead of one flat threshold for the whole piece.
Fixed
Notes spanning a barline need an extra tied notehead no matter how "clean" their duration is — MuseScore can't draw one notehead across a barline. That wasn't being counted before. Now it is, everywhere tie budgets are checked.
Additional
The per-staff summary now shows extended=N (X sixteenths invented), so you can see exactly how much the optimizer fabricated to clean up the page.
v1.1
ScorePrep v1.1
Improvements
- Excessive rest fragmentation — note duration previously ignored the next note's onset entirely, turning every small natural gap into a rest. New
--max-silent-gap N(default 2 grid units) closes small gaps by extending the note instead, capped so it can never create an overlap or violate the tie-temperature bar-span limit.
(only temporary workaround, will be improved in future)
New
- Multi-track merging —
--tracknow acceptsN,M,...orallto merge multiple tracks in one pass (e.g. separate right-hand/left-hand sources), not just a single index. - Remembered paths — interactive mode saves the last input/output paths (
~/.config/scoreprep/config.json) and offers them as defaults; reusing the same input file offers the previous output path too. --max-silent-gapin interactive mode, with an explicit warning about when raising it won't help (see below).rests=Nadded to the per-staff report, alongsideneeds-tie=N— the other half of the tie/rest tradeoff, now visible without a manual sweep.
Known tradeoff, now documented
- At low
--tie-temperature, raising--max-silent-gaphas limited effect — the tie budget itself is usually the real bottleneck. Raising--tie-temperatureslightly (e.g. 0.0 → 0.1) is often far more effective at cutting rests, sometimes at zero tie cost. New FAQ entries in the README explain how to use therests/needs-tienumbers to find that point for a given piece.
ScorePrep v1.0 Release
ScorePrep v1.0.0
First stable public release.
ScorePrep transforms messy piano transcription MIDI (from tools such as ByteDance Piano Transcription, MT3, Magenta, etc.) into clean, notation-friendly grand staff MIDI for MuseScore and other notation software while preserving the original performance as faithfully as practical.
Highlights
🎼 Cleaner engraving
- Automatic treble/bass grand staff generation
- Configurable tie reduction via Tie Temperature
- Chord synchronization to prevent fractured chords
- Optional straight or triplet quantization grids
- Configurable clean duration styles (dotted or powers-of-two)
🎧 Improved playback fidelity
- Playback sustain independent of engraved notation
- Correct tempo/tick rescaling when changing score tempo
- Sustain pedal reflection support
- Velocity filtering, normalization, and scaling
🎹 Better handling of real-world MIDI
- Automatic tempo estimation with ambiguity detection
- Automatic track selection with optional manual track/channel overrides
- Leading silence detection and automatic score rebasing
- Noise filtering for spurious transcription artifacts
🛠 User experience
- Interactive mode
- Comprehensive command-line interface
- Clear diagnostics and explanatory messages
Philosophy
ScorePrep is designed as a piano transcription cleanup tool, not a general-purpose MIDI editor.
Its goal is to automate the tedious cleanup normally required after AI transcription so that the resulting MIDI imports cleanly into MuseScore (or similar notation software) while remaining musically faithful to the original performance.
Feedback
Bug reports, feature requests, and suggestions are always welcome. If ScorePrep saves you time, I'd love to hear about it.