From f5f0ab5eec82de3664839ec5ed259aac1a9e4446 Mon Sep 17 00:00:00 2001 From: khodowany Date: Wed, 30 Sep 2026 03:17:06 -0500 Subject: [PATCH] Scale Patterns --- .gitea/workflows/ci.yml | 2 +- README.md | 2 +- TECHNICAL_GUIDE.md | 12 ++ cmake/runtime_assets.cmake | 2 +- cmake/zero.cmake | 3 +- conanfile.py | 1 + data/expected/performance_summary.json | 6 +- .../harmonic-minor-scale-modes/0003.xml | 16 +- data/songs/autumn-leaves.xml | 2 +- generator/__main__.py | 5 +- generator/arrange/generate.py | 25 ++- generator/engraving/audit.py | 8 +- generator/engraving/layout.py | 2 + generator/engraving/notation.py | 3 + generator/engraving/scales.py | 14 ++ generator/engraving/spans.py | 6 +- generator/model.py | 9 + generator/scales/__init__.py | 0 generator/scales/library.py | 92 +++++++++ generator/scales/mapping.py | 32 +++ generator/scales/report.py | 44 ++++ generator/scales/selection.py | 109 ++++++++++ generator/scales/timeline.py | 39 ++++ tests/scale_fixtures.py | 8 + tests/test_engraving_cache.py | 4 +- tests/test_guitar.py | 2 + tests/test_harmony.py | 2 +- tests/test_music_model.py | 2 + tests/test_parts.py | 2 + tests/test_scales.py | 192 ++++++++++++++++++ tests/test_score_bundle.py | 12 +- 31 files changed, 626 insertions(+), 32 deletions(-) create mode 100644 generator/engraving/scales.py create mode 100644 generator/scales/__init__.py create mode 100644 generator/scales/library.py create mode 100644 generator/scales/mapping.py create mode 100644 generator/scales/report.py create mode 100644 generator/scales/selection.py create mode 100644 generator/scales/timeline.py create mode 100644 tests/scale_fixtures.py create mode 100644 tests/test_scales.py diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 3bbabab..71a0a7f 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -112,7 +112,7 @@ jobs: uses: actions/cache/restore@v4 with: path: build/build_common/engraving-cache/${{ steps.renderer.outputs.fingerprint }} - key: product-vamp-pages-${{ steps.renderer.outputs.fingerprint }}-${{ hashFiles('generator/**/*.py', 'generator/*.py', 'data/songs/*.xml', 'data/rhythms/**/*.xml') }} + key: product-vamp-pages-${{ steps.renderer.outputs.fingerprint }}-${{ hashFiles('generator/**/*.py', 'generator/*.py', 'data/songs/*.xml', 'data/rhythms/**/*.xml', 'data/scales/**/*.xml') }} restore-keys: product-vamp-pages-${{ steps.renderer.outputs.fingerprint }}- - name: Build native release run: python3 modulus.py build --target x86_64_gcc12_release diff --git a/README.md b/README.md index 8f951fd..6f3d0fa 100644 --- a/README.md +++ b/README.md @@ -90,7 +90,7 @@ Uncompressed MusicXML 3.1/4.0 scores exported from Soundslice or MuseScore live `data/songs` supplies the authored music. `data/rhythms///` supplies the accompaniment rhythm library for 2/4, 3/4, 4/4, and 5/4. All three series are active: the original quarter/eighth patterns plus series `3` with half, dotted-half, and whole notes/rests. Rhythm snippets are build inputs, not separate catalog entries or runtime files. Generation loads the library once and shares it across parts. -`data/scales` is reserved for unfinished scale sources. These files are not parsed, validated, generated, or packaged, and editing them does not trigger asset regeneration. +`data/scales` supplies the 21 C-centered modes of major, melodic minor, and harmonic minor. The Python build transposes these patterns internally to label the acoustic and electric solo passages; it never transposes the songs or stores scale assignments in song XML. Scale changes appear above the existing solo chord symbols, with detailed build-only reports in `generated-assets/analysis/`. Editing a scale regenerates assets, and invalid patterns fail before replacing the previous output. To inspect generated assets without building the full package, run: diff --git a/TECHNICAL_GUIDE.md b/TECHNICAL_GUIDE.md index ec33fa6..9eaf6e5 100644 --- a/TECHNICAL_GUIDE.md +++ b/TECHNICAL_GUIDE.md @@ -171,6 +171,18 @@ At runtime the shared on-demand score loader serves both display and audio. A ba `--once` additionally validates referenced audio without opening display/audio hardware. Runtime packages contain WAVs alongside pages and the SQLite catalog; the source XML and Python dependencies remain build-only. +### Solo Scale Maps + +`generator/scales/` loads the 21 MusicXML patterns in `data/scales` once per build. Each file spells an ascending C-centered octave; seven distinct degrees define its intervals, including spellings such as B double-flat. The seven files in each family must be rotations of that family's first mode. The library transposes patterns internally, not song notes, and searches the 36 distinct pitch collections formed by the three families in twelve roots. No assignments or overrides are stored in song XML. + +The analyzer requires a declared tonic and mode in the song's MusicXML key. A one-flat signature with `minor` means D minor, not F major. Exact rational harmony offsets are read after arrangement and pickup overlap handling. Full chord semantics, explicit extensions and alterations, degree omissions, and slash basses constrain compatibility; reduced guitar grips never remove these requirements. Existing chord syntax is unchanged: `7alt` shorthand is still rejected, while explicit alterations are supported. + +Selection first preserves every required pitch. Recognized ii-V pairs, resolving dominants, and leading-tone diminished resolutions establish local destinations; the declared key provides the default context elsewhere. Minor cadences prefer harmonic minor and major cadences prefer the destination's major collection when compatible. The arrival chord is evaluated afresh, so a plain D-minor tonic need not retain a preceding cadence's raised seventh. Among equally appropriate choices, a deterministic dynamic program minimizes collection changes, then contextual pitch differences. Equivalent modes share a pitch collection rather than forcing new boundaries. This is a conservative harmonic heuristic, not a claim that ambiguous harmony has only one interpretation. + +Only the soloing part receives compact `scale_spans`, with absolute rational start/end offsets, labels, and matched status. `N.C.` and solo boundaries stop annotations. Accompaniment, melody, pitches, TAB, and playback are unchanged; no solo notes are synthesized. Original melody notes are not constraints on these slash-only solo passages. Unmatched chords print `No exact scale match` and emit a source/measure/beat warning without changing the harmony. Invalid scale data or missing tonal metadata instead fail generation before publication. + +Scale labels appear above chord symbols at collection changes and system starts. Detailed parent collections, source and arranged positions, chord-root modes, degrees, chord-tone targets, and selection reasons are written to `generated-assets/analysis/.txt`. These reports are published transactionally with the build output but excluded from both installation and Docker runtime assets. The C++ runtime only displays the generated pages; it does not load the scale library or run harmonic analysis. + ### Engraving Boundary `generator/engraving/` is a custom Python engraver, not a wrapper around MuseScore or another notation engine. `generator.engraving.layout.layout_score(document, style, fonts)` produces immutable layout pages with exact source intervals, notation anchors and drawing primitives. `generator.engraving.raster.rasterize_page(page, fonts)` draws one page at four-times resolution and downsamples it. `generator.engraving.pipeline.engrave_tree(root, fonts)` is the asset-pipeline entry point. `fonts.Fonts`, `types.Style`, and `types.EngravingError` live in their respective engraving modules; page constants and `check_page` live in `validation`. Package initializers do not re-export these APIs. `generator/musicxml_writer.py` remains a round-trip/export utility and is not involved in engraving. diff --git a/cmake/runtime_assets.cmake b/cmake/runtime_assets.cmake index 34e4910..5c9f257 100644 --- a/cmake/runtime_assets.cmake +++ b/cmake/runtime_assets.cmake @@ -1,4 +1,4 @@ if(NOT EXISTS "${SOURCE}/catalog.db") message(FATAL_ERROR "Missing generated catalog: ${SOURCE}/catalog.db") endif() -file(COPY "${SOURCE}/" DESTINATION "${DESTINATION}" PATTERN "*.json" EXCLUDE) +file(COPY "${SOURCE}/" DESTINATION "${DESTINATION}" PATTERN "*.json" EXCLUDE PATTERN "analysis" EXCLUDE) diff --git a/cmake/zero.cmake b/cmake/zero.cmake index 5924b04..55494ea 100644 --- a/cmake/zero.cmake +++ b/cmake/zero.cmake @@ -139,7 +139,6 @@ file(GLOB_RECURSE VAMP_GENERATOR_INPUTS CONFIGURE_DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/package/assets/fonts/* ${CMAKE_CURRENT_SOURCE_DIR}/package/assets/fonts/**/* ) -list(FILTER VAMP_GENERATOR_INPUTS EXCLUDE REGEX "/data/scales/") set(VAMP_GENERATED_ASSETS_DIR ${CMAKE_CURRENT_BINARY_DIR}/generated-assets) set(VAMP_GENERATED_ASSETS_STAMP ${CMAKE_CURRENT_BINARY_DIR}/generated-assets.stamp) set(VAMP_ENGRAVING_JOBS 2 CACHE STRING "Maximum parallel Python engraving workers (1-16)") @@ -180,7 +179,7 @@ install(TARGETS vamp-zero ) install(FILES package/config/vamp.example.json DESTINATION zero/config) install(DIRECTORY ${VAMP_GENERATED_ASSETS_DIR}/ DESTINATION zero/share/product-vamp - PATTERN "*.json" EXCLUDE) + PATTERN "*.json" EXCLUDE PATTERN "analysis" EXCLUDE) install(PROGRAMS package/zero/install-zero.sh package/zero/start-ap.sh diff --git a/conanfile.py b/conanfile.py index 5e814e4..f1b8aa4 100644 --- a/conanfile.py +++ b/conanfile.py @@ -32,6 +32,7 @@ class product_vamp_conan(conan.ConanFile): "data/expected/*.json", "data/songs/*.xml", "data/rhythms/**/*.xml", + "data/scales/**/*.xml", "tests/fixtures/*", "tests/fixtures/**/*", "generator/*.py", diff --git a/data/expected/performance_summary.json b/data/expected/performance_summary.json index f8bbcbd..b05788a 100644 --- a/data/expected/performance_summary.json +++ b/data/expected/performance_summary.json @@ -2,7 +2,7 @@ "autumn-leaves-acoustic.json": { "title": "autumn-leaves", "composer": "", - "key_signature": "F", + "key_signature": "D minor", "part_type": "acoustic", "measure_count": 176, "section_labels": [ @@ -2074,7 +2074,7 @@ "autumn-leaves-electric.json": { "title": "autumn-leaves", "composer": "", - "key_signature": "F", + "key_signature": "D minor", "part_type": "electric", "measure_count": 176, "section_labels": [ @@ -4146,7 +4146,7 @@ "autumn-leaves-vocal.json": { "title": "autumn-leaves", "composer": "", - "key_signature": "F", + "key_signature": "D minor", "part_type": "vocal", "measure_count": 176, "section_labels": [ diff --git a/data/scales/harmonic-minor-scale-modes/0003.xml b/data/scales/harmonic-minor-scale-modes/0003.xml index 1fb4079..f65a1fa 100644 --- a/data/scales/harmonic-minor-scale-modes/0003.xml +++ b/data/scales/harmonic-minor-scale-modes/0003.xml @@ -66,14 +66,6 @@ - - - G - 4 - - 4 - quarter - G @@ -84,6 +76,14 @@ quarter sharp + + + A + 4 + + 4 + quarter + B diff --git a/data/songs/autumn-leaves.xml b/data/songs/autumn-leaves.xml index 9f1a483..7e12a5f 100644 --- a/data/songs/autumn-leaves.xml +++ b/data/songs/autumn-leaves.xml @@ -20,7 +20,7 @@ 4 -1 - major + minor