Skip to content

Releases: privacyfreak84/ScorePrep

v1.2.7

Choose a tag to compare

@privacyfreak84 privacyfreak84 released this 05 Sep 13:40

[1.2.7]

Fixed

  • --grid triplet made 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 triplet went from being ~12x lower than the equivalent
    --grid straight run (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
    assumes TICKS_PER_BEAT=384, with no actual rescaling -- the existing
    NOTE: ... results may be off warning 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)
    by TICKS_PER_BEAT / source_ticks_per_beat right 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_overlaps and the redundant duration-optimizer
    pass that followed it. optimize_staff_durations already 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 (default off, 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 triplet manually. 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 fourth tuplet feature for real A/B validation.
    Confirmed off (default) is byte-identical to pre-existing output;
    auto runs 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 --tempo isn'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 --tempo still means one flat tempo, as before, to avoid
    ambiguity about what "override one value out of a dozen" should mean.

Full CHANGELOG

v1.2.6

Choose a tag to compare

@privacyfreak84 privacyfreak84 released this 30 Aug 11:18

[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-assignment into 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_warnings could be referenced before assignment when
    --hand-assignment was 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-zone were 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-pitch or --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 on re-estimates the treble/bass split point every
    --split-window-bars bars (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.

Full Changelog

v1.2.5

Choose a tag to compare

@privacyfreak84 privacyfreak84 released this 29 Aug 22:01

[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.

Full Changelog

v1.2.4

Choose a tag to compare

@privacyfreak84 privacyfreak84 released this 27 Jul 12:26

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 optional stats dict 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.

Full Changelog

v1.2.3

Choose a tag to compare

@privacyfreak84 privacyfreak84 released this 25 Jul 20:49

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_count returns notehead count (minimum 1, even
    for a plain untied note), not tie count -- actual ties = noteheads - 1.

Full Changelog

v1.2.2

Choose a tag to compare

@privacyfreak84 privacyfreak84 released this 25 Jul 17:21

Fixed custom tempo controls:

[1.2.2]

Fixed

  • Changing --tempo away from the detected value silently had no
    effect on real playback speed.
    rescale_notes_to_tempo was 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 --tempo is 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). Default preserve-duration keeps the
    old MT3-oriented behavior; change-speed leaves ticks untouched so
    the tempo change is real.

Full Changelog

v1.2.1

Choose a tag to compare

@privacyfreak84 privacyfreak84 released this 20 Jul 16:50

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:

  1. Bollywood pop song (Kalank)
  2. Classical piece (Prelude in C# Minor)
    Testing confirms significantly more faithful notation while preserving clean engraving and identical playback.

v1.2

Choose a tag to compare

@privacyfreak84 privacyfreak84 released this 20 Jul 00:19

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

Choose a tag to compare

@privacyfreak84 privacyfreak84 released this 19 Jul 15:13

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 — --track now accepts N,M,... or all to 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-gap in interactive mode, with an explicit warning about when raising it won't help (see below).
  • rests=N added to the per-staff report, alongside needs-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-gap has limited effect — the tie budget itself is usually the real bottleneck. Raising --tie-temperature slightly (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 the rests/needs-tie numbers to find that point for a given piece.

ScorePrep v1.0 Release

Choose a tag to compare

@privacyfreak84 privacyfreak84 released this 19 Jul 09:23

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.