Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fe42d811fa | |||
| 44e100985a |
+6
-1
@@ -1,5 +1,10 @@
|
||||
.git
|
||||
build
|
||||
build/*
|
||||
!build/build_tools/
|
||||
build/build_tools/*
|
||||
!build/build_tools/docker/
|
||||
build/build_tools/docker/*
|
||||
!build/build_tools/docker/format-requirements.txt
|
||||
dist
|
||||
node_modules
|
||||
*.log
|
||||
|
||||
@@ -98,6 +98,10 @@ jobs:
|
||||
restore-keys: product-vamp-conan-${{ runner.os }}-
|
||||
- name: Configure native release
|
||||
run: python3 modulus.py configure --target x86_64_gcc12_release
|
||||
- name: Check code style
|
||||
run: |
|
||||
python3 -m pip install --no-cache-dir -r build/build_tools/docker/format-requirements.txt
|
||||
python3 modulus.py format --check
|
||||
- name: Build native release
|
||||
run: python3 modulus.py build --target x86_64_gcc12_release
|
||||
- name: Test native release
|
||||
|
||||
@@ -83,7 +83,9 @@ python3 -m generator --data-dir data --output-dir /tmp/product-vamp-generated --
|
||||
The `--check` path compares generation invariants against `data/expected/performance_summary.json`.
|
||||
The pinned build-only dependencies are in `tools/requirements.txt`. Configure/build automatically refresh the project build image when this file changes; direct builds must install it in their Python environment.
|
||||
|
||||
The build-time pipeline is MuseScore -> `.musicxml` -> importer -> arranger/transposer -> schema-v2 JSON. The Pi reads only JSON. Performance sources produce acoustic, electric, and vocal parts; melody exercises produce twelve tonic variants, and chord exercises keep their authored key.
|
||||
The build-time pipeline is MuseScore -> `.xml` (uncompressed MusicXML) -> importer -> arranger/transposer -> schema-v2 JSON. The Pi reads only JSON. Performance sources produce acoustic, electric, and vocal parts; melody exercises produce twelve tonic variants, and chord exercises keep their authored key.
|
||||
|
||||
Code style is managed by Modulus. Run `./modulus.py format` to format C++ and Python, or `./modulus.py format --check` to check formatting and prohibited aliases. The project-local Neovim integration routes Conform formatting through the same container tools, including format on save and existing LazyVim toggles. Run configure first when the image or exported tooling needs updating. Use `tooling.editor.clang_format` for C++ overrides and `tooling.format.exclude` for project-relative exclusion globs; do not edit generated `.clang-format` files.
|
||||
|
||||
See [Score Generation](TECHNICAL_GUIDE.md#score-generation) for supported MusicXML notation, Vamp properties, schema details, and diagnostics. There is no compact-JSON source format or legacy runtime adapter. Unsupported notation fails generation before existing assets are replaced.
|
||||
|
||||
|
||||
+3
-2
@@ -37,7 +37,7 @@ Petaluma music symbols are placed from SMuFL bounding boxes and stem/flag anchor
|
||||
|
||||
The build runs `python3 -m generator` against `data/` and writes build-only assets under the CMake binary tree. Performance source songs become acoustic, electric, and vocal arrangements. Melody practice sources are emitted in all 12 keys at build time. Chord practice sources are emitted once in their authored key. Generated JSON keeps notation metadata while removing generator-only optional chord markers; guitar playability logic is used only inside the arrangement generator.
|
||||
|
||||
The source pipeline is MuseScore -> uncompressed `.musicxml` -> `generator/musicxml.py` -> arranger/transposer -> schema-v2 JSON. Export MusicXML 3.1 or 4.0, `score-partwise`, with one part, one staff, and one rhythmic voice. Chord tones share an onset but retain their own spelling, written duration, and tie links. Separate voices/staves must not be flattened into the source voice. The schema keeps staff/voice ownership explicit for a future polyphonic renderer.
|
||||
The source pipeline is MuseScore -> uncompressed `.xml` -> `generator/musicxml.py` -> arranger/transposer -> schema-v2 JSON. Export MusicXML 3.1 or 4.0, `score-partwise`, with one part, one staff, and one rhythmic voice. Chord tones share an onset but retain their own spelling, written duration, and tie links. Separate voices/staves must not be flattened into the source voice. The schema keeps staff/voice ownership explicit for a future polyphonic renderer.
|
||||
|
||||
### MusicXML Contract
|
||||
|
||||
@@ -52,6 +52,7 @@ The source pipeline is MuseScore -> uncompressed `.musicxml` -> `generator/music
|
||||
| Meter | Simple, compound, additive, mixed-denominator, and unmetered timelines. A partial measure must be marked `implicit="yes"`; an initial implicit measure is the arrangement pickup. Leading rests are retained. |
|
||||
| Harmony | Structured roots, kinds, inversions, slash basses, and degree additions/alterations/subtractions. Harmony events have their own offsets, including inside held notes. Their positions never split or shorten notes. |
|
||||
| Text | Title, composer, part name, one lyric verse with syllabification and extenders, rehearsal labels at measure starts, and Vamp directives. |
|
||||
| Tempo | Metronome directions and positive quarter-note BPM from `<sound tempo="...">`, either within a direction or directly in a measure. Playback/navigation attributes on `sound` are rejected. |
|
||||
|
||||
Microtones, independent voices, multiple staves/parts, percussion/tab staves, repeats/navigation, slurs, dynamics, articulations, and expressive ornaments are rejected. Unknown musical elements also fail, rather than disappearing. Custom key-octave placement is not supported. Resource limits are explicit: serialized times use reduced signed-64-bit numerators/positive denominators; pitches use octaves -4 through 16 and alterations -3 through 3; clef/octave shifts span at most three octaves; meter counts are 1-1024 with denominator units 1-4096. These limits produce diagnostics, not clamping or truncation.
|
||||
|
||||
@@ -83,7 +84,7 @@ The renderer uses written values for glyphs, meter-aware/multilevel beams, per-p
|
||||
|
||||
### Validation And Migration
|
||||
|
||||
Generation validates sources and complete staged output before replacing the previous asset directory. CMake tracks the input list as well as file contents, so additions and removals regenerate the catalogue. Runtime packages contain JSON and OTF fonts, not MusicXML or Python importer dependencies. Python test discovery runs all generator/importer tests; `tests/fixtures/notation.musicxml` exercises the broader renderer contract. The optional `vamp-render-score` test target captures a page using a desktop-capable Raylib build.
|
||||
Generation validates sources and complete staged output before replacing the previous asset directory. CMake tracks the input list as well as file contents, so additions and removals regenerate the catalogue. Runtime packages contain JSON and OTF fonts, not MusicXML or Python importer dependencies. Python test discovery runs all generator/importer tests; `tests/fixtures/notation.xml` exercises the broader renderer contract. The optional `vamp-render-score` test target captures a page using a desktop-capable Raylib build.
|
||||
|
||||
Diagnostics identify the file, part, measure, and unsupported element or invalid relationship. Examples include `part P1, measure 4: unsupported musical element <backup>`, `tie stop has no contiguous matching start`, and `optional-chord marker must identify exactly one harmony`.
|
||||
|
||||
|
||||
+1
-1
@@ -115,7 +115,7 @@ set(VAMP_MANIFEST ${VAMP_BUNDLE_DIR}/manifest.json)
|
||||
configure_file(package/manifest.json.in ${VAMP_MANIFEST} @ONLY)
|
||||
|
||||
file(GLOB_RECURSE VAMP_GENERATOR_INPUTS CONFIGURE_DEPENDS
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/data/*.musicxml
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/data/*.xml
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/data/*.json
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/data/**/*.json
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/generator/*.py
|
||||
|
||||
+3
-13
@@ -27,8 +27,8 @@ class product_vamp_conan(conan.ConanFile):
|
||||
"cmake/*.cmake",
|
||||
"data/*.json",
|
||||
"data/**/*.json",
|
||||
"data/*.musicxml",
|
||||
"data/**/*.musicxml",
|
||||
"data/*.xml",
|
||||
"data/**/*.xml",
|
||||
"tests/fixtures/*",
|
||||
"tests/fixtures/**/*",
|
||||
"generator/*.py",
|
||||
@@ -59,7 +59,6 @@ class product_vamp_conan(conan.ConanFile):
|
||||
"tools/*.py",
|
||||
"tools/requirements.txt",
|
||||
)
|
||||
|
||||
def requirements(self):
|
||||
self.requires(
|
||||
str(self.python_requires["modulus"].ref),
|
||||
@@ -77,12 +76,7 @@ class product_vamp_conan(conan.ConanFile):
|
||||
"luma/latest",
|
||||
"sonus/latest",
|
||||
):
|
||||
self.requires(
|
||||
reference,
|
||||
transitive_headers=True,
|
||||
transitive_libs=True,
|
||||
)
|
||||
|
||||
self.requires(reference, transitive_headers=True, transitive_libs=True)
|
||||
def generate(self):
|
||||
dependencies = conan.tools.cmake.CMakeDeps(self)
|
||||
dependencies.generate()
|
||||
@@ -91,20 +85,16 @@ class product_vamp_conan(conan.ConanFile):
|
||||
toolchain.user_presets_path = False
|
||||
toolchain.variables["BUILD_TESTING"] = bool(self.options.with_tests)
|
||||
toolchain.generate()
|
||||
|
||||
def build(self):
|
||||
cmake = conan.tools.cmake.CMake(self)
|
||||
cmake.configure()
|
||||
cmake.build()
|
||||
if self.options.with_tests and conan.tools.build.can_run(self):
|
||||
cmake.test(target="check")
|
||||
|
||||
def package_id(self):
|
||||
self.info.options.rm_safe("with_tests")
|
||||
|
||||
def package(self):
|
||||
cmake = conan.tools.cmake.CMake(self)
|
||||
cmake.install()
|
||||
|
||||
def package_info(self):
|
||||
self.cpp_info.bindirs = ["zero/bin"]
|
||||
|
||||
+149
-149
File diff suppressed because it is too large
Load Diff
+156
-155
@@ -1,12 +1,13 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<?xml version='1.0' encoding='UTF-8'?>
|
||||
<!DOCTYPE score-partwise PUBLIC "-//Recordare//DTD MusicXML 3.0 Partwise//EN" "http://www.musicxml.org/dtds/partwise.dtd">
|
||||
<score-partwise version="4.0">
|
||||
<work>
|
||||
<work-title>02 The Diatonic Scales</work-title>
|
||||
</work>
|
||||
<identification>
|
||||
<miscellaneous>
|
||||
<miscellaneous-field name="vamp:style">Medium Swing</miscellaneous-field>
|
||||
</miscellaneous>
|
||||
<encoding>
|
||||
<supports element="accidental" type="yes"/>
|
||||
<supports element="transpose" type="yes"/>
|
||||
<software>Soundslice MusicXML exporter</software>
|
||||
<encoding-date>2026-09-14</encoding-date>
|
||||
</encoding>
|
||||
</identification>
|
||||
<part-list>
|
||||
<score-part id="P1">
|
||||
@@ -16,7 +17,7 @@
|
||||
<part id="P1">
|
||||
<measure number="1">
|
||||
<attributes>
|
||||
<divisions>12</divisions>
|
||||
<divisions>8</divisions>
|
||||
<key>
|
||||
<fifths>0</fifths>
|
||||
<mode>major</mode>
|
||||
@@ -30,77 +31,78 @@
|
||||
<line>2</line>
|
||||
</clef>
|
||||
</attributes>
|
||||
<sound tempo="120"/>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>C</step>
|
||||
<octave>4</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">begin</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>D</step>
|
||||
<octave>4</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">continue</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>E</step>
|
||||
<octave>4</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">continue</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>D</step>
|
||||
<octave>4</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">end</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>E</step>
|
||||
<octave>4</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">begin</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>F</step>
|
||||
<octave>4</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">continue</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>E</step>
|
||||
<octave>4</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">continue</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>F</step>
|
||||
<octave>4</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">end</beam>
|
||||
</note>
|
||||
</measure>
|
||||
<measure number="2">
|
||||
@@ -109,72 +111,72 @@
|
||||
<step>G</step>
|
||||
<octave>4</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">begin</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>F</step>
|
||||
<octave>4</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">continue</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>G</step>
|
||||
<octave>4</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">continue</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>A</step>
|
||||
<octave>4</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">end</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>G</step>
|
||||
<octave>4</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">begin</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>A</step>
|
||||
<octave>4</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">continue</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>B</step>
|
||||
<octave>4</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">continue</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>A</step>
|
||||
<octave>4</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">end</beam>
|
||||
</note>
|
||||
</measure>
|
||||
<measure number="3">
|
||||
@@ -183,72 +185,72 @@
|
||||
<step>B</step>
|
||||
<octave>4</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">begin</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>C</step>
|
||||
<octave>5</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">continue</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>B</step>
|
||||
<octave>4</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">continue</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>C</step>
|
||||
<octave>5</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">end</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>D</step>
|
||||
<octave>6</octave>
|
||||
<octave>5</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">begin</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>C</step>
|
||||
<octave>5</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">continue</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>D</step>
|
||||
<octave>5</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">continue</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>E</step>
|
||||
<octave>5</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">end</beam>
|
||||
</note>
|
||||
</measure>
|
||||
<measure number="4">
|
||||
@@ -257,72 +259,72 @@
|
||||
<step>D</step>
|
||||
<octave>5</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">begin</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>E</step>
|
||||
<octave>5</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">continue</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>F</step>
|
||||
<octave>5</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">continue</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>E</step>
|
||||
<octave>5</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">end</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>F</step>
|
||||
<octave>5</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">begin</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>G</step>
|
||||
<octave>5</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">continue</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>F</step>
|
||||
<octave>5</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">continue</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>G</step>
|
||||
<octave>5</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">end</beam>
|
||||
</note>
|
||||
</measure>
|
||||
<measure number="5">
|
||||
@@ -331,72 +333,72 @@
|
||||
<step>A</step>
|
||||
<octave>5</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">begin</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>G</step>
|
||||
<octave>5</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">continue</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>A</step>
|
||||
<octave>5</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">continue</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>B</step>
|
||||
<octave>5</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">end</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>A</step>
|
||||
<octave>5</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">begin</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>B</step>
|
||||
<octave>5</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">continue</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>C</step>
|
||||
<octave>6</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">continue</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>B</step>
|
||||
<octave>5</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">end</beam>
|
||||
</note>
|
||||
</measure>
|
||||
<measure number="6">
|
||||
@@ -405,72 +407,72 @@
|
||||
<step>C</step>
|
||||
<octave>6</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">begin</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>D</step>
|
||||
<octave>6</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">continue</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>C</step>
|
||||
<octave>6</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">continue</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>B</step>
|
||||
<octave>5</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">end</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>C</step>
|
||||
<octave>6</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">begin</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>B</step>
|
||||
<octave>5</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">continue</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>A</step>
|
||||
<octave>5</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">continue</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>B</step>
|
||||
<octave>5</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">end</beam>
|
||||
</note>
|
||||
</measure>
|
||||
<measure number="7">
|
||||
@@ -479,72 +481,72 @@
|
||||
<step>A</step>
|
||||
<octave>5</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">begin</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>G</step>
|
||||
<octave>5</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">continue</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>A</step>
|
||||
<octave>5</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">continue</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>G</step>
|
||||
<octave>5</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">end</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>F</step>
|
||||
<octave>5</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">begin</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>G</step>
|
||||
<octave>5</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">continue</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>F</step>
|
||||
<octave>5</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">continue</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>E</step>
|
||||
<octave>5</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">end</beam>
|
||||
</note>
|
||||
</measure>
|
||||
<measure number="8">
|
||||
@@ -553,72 +555,72 @@
|
||||
<step>F</step>
|
||||
<octave>5</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">begin</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>E</step>
|
||||
<octave>5</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">continue</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>D</step>
|
||||
<octave>5</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">continue</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>E</step>
|
||||
<octave>5</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">end</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>D</step>
|
||||
<octave>5</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">begin</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>C</step>
|
||||
<octave>5</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">continue</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>D</step>
|
||||
<octave>5</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">continue</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>C</step>
|
||||
<octave>5</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">end</beam>
|
||||
</note>
|
||||
</measure>
|
||||
<measure number="9">
|
||||
@@ -627,72 +629,72 @@
|
||||
<step>B</step>
|
||||
<octave>4</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">begin</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>C</step>
|
||||
<octave>5</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">continue</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>B</step>
|
||||
<octave>4</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">continue</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>A</step>
|
||||
<octave>4</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">end</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>G</step>
|
||||
<octave>4</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">begin</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>F</step>
|
||||
<octave>4</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">continue</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>E</step>
|
||||
<octave>4</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">continue</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>D</step>
|
||||
<octave>4</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">end</beam>
|
||||
</note>
|
||||
</measure>
|
||||
<measure number="10">
|
||||
@@ -701,8 +703,7 @@
|
||||
<step>C</step>
|
||||
<octave>4</octave>
|
||||
</pitch>
|
||||
<duration>48</duration>
|
||||
<voice>1</voice>
|
||||
<duration>32</duration>
|
||||
<type>whole</type>
|
||||
</note>
|
||||
</measure>
|
||||
+171
-170
@@ -1,12 +1,13 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<?xml version='1.0' encoding='UTF-8'?>
|
||||
<!DOCTYPE score-partwise PUBLIC "-//Recordare//DTD MusicXML 3.0 Partwise//EN" "http://www.musicxml.org/dtds/partwise.dtd">
|
||||
<score-partwise version="4.0">
|
||||
<work>
|
||||
<work-title>03 The Diatonic Scales</work-title>
|
||||
</work>
|
||||
<identification>
|
||||
<miscellaneous>
|
||||
<miscellaneous-field name="vamp:style">Medium Swing</miscellaneous-field>
|
||||
</miscellaneous>
|
||||
<encoding>
|
||||
<supports element="accidental" type="yes"/>
|
||||
<supports element="transpose" type="yes"/>
|
||||
<software>Soundslice MusicXML exporter</software>
|
||||
<encoding-date>2026-09-14</encoding-date>
|
||||
</encoding>
|
||||
</identification>
|
||||
<part-list>
|
||||
<score-part id="P1">
|
||||
@@ -16,7 +17,7 @@
|
||||
<part id="P1">
|
||||
<measure number="1">
|
||||
<attributes>
|
||||
<divisions>12</divisions>
|
||||
<divisions>8</divisions>
|
||||
<key>
|
||||
<fifths>0</fifths>
|
||||
<mode>major</mode>
|
||||
@@ -30,77 +31,78 @@
|
||||
<line>2</line>
|
||||
</clef>
|
||||
</attributes>
|
||||
<sound tempo="120"/>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>B</step>
|
||||
<octave>3</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">begin</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>C</step>
|
||||
<octave>4</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">continue</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>D</step>
|
||||
<octave>4</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">continue</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>E</step>
|
||||
<octave>4</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">end</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>F</step>
|
||||
<octave>4</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">begin</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>E</step>
|
||||
<octave>4</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">continue</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>F</step>
|
||||
<octave>4</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">continue</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>G</step>
|
||||
<octave>4</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">end</beam>
|
||||
</note>
|
||||
</measure>
|
||||
<measure number="2">
|
||||
@@ -109,72 +111,72 @@
|
||||
<step>F</step>
|
||||
<octave>4</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">begin</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>E</step>
|
||||
<octave>4</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">continue</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>F</step>
|
||||
<octave>4</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">continue</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>G</step>
|
||||
<octave>4</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">end</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>A</step>
|
||||
<octave>4</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">begin</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>B</step>
|
||||
<octave>4</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">continue</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>C</step>
|
||||
<octave>5</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">continue</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>B</step>
|
||||
<octave>4</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">end</beam>
|
||||
</note>
|
||||
</measure>
|
||||
<measure number="3">
|
||||
@@ -183,72 +185,72 @@
|
||||
<step>A</step>
|
||||
<octave>4</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">begin</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>B</step>
|
||||
<octave>4</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">continue</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>C</step>
|
||||
<octave>5</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">continue</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>D</step>
|
||||
<octave>5</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">end</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>E</step>
|
||||
<octave>5</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">begin</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>F</step>
|
||||
<octave>5</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">continue</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>E</step>
|
||||
<octave>5</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">continue</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>D</step>
|
||||
<octave>5</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">end</beam>
|
||||
</note>
|
||||
</measure>
|
||||
<measure number="4">
|
||||
@@ -257,72 +259,72 @@
|
||||
<step>C</step>
|
||||
<octave>5</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">begin</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>B</step>
|
||||
<octave>4</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">continue</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>C</step>
|
||||
<octave>5</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">continue</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>D</step>
|
||||
<octave>5</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">end</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>E</step>
|
||||
<octave>5</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">begin</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>F</step>
|
||||
<octave>5</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">continue</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>G</step>
|
||||
<octave>5</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">continue</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>A</step>
|
||||
<octave>5</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">end</beam>
|
||||
</note>
|
||||
</measure>
|
||||
<measure number="5">
|
||||
@@ -331,72 +333,72 @@
|
||||
<step>B</step>
|
||||
<octave>5</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">begin</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>C</step>
|
||||
<octave>6</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">continue</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>B</step>
|
||||
<octave>5</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">continue</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>A</step>
|
||||
<octave>5</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">end</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>G</step>
|
||||
<octave>5</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">begin</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>A</step>
|
||||
<octave>5</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">continue</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>B</step>
|
||||
<octave>5</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">continue</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>C</step>
|
||||
<octave>6</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">end</beam>
|
||||
</note>
|
||||
</measure>
|
||||
<measure number="6">
|
||||
@@ -405,72 +407,72 @@
|
||||
<step>D</step>
|
||||
<octave>6</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">begin</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>E</step>
|
||||
<octave>6</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">continue</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>D</step>
|
||||
<octave>6</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">continue</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>C</step>
|
||||
<octave>6</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">end</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>B</step>
|
||||
<octave>5</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">begin</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>C</step>
|
||||
<octave>6</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">continue</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>B</step>
|
||||
<octave>5</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">continue</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>A</step>
|
||||
<octave>5</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">end</beam>
|
||||
</note>
|
||||
</measure>
|
||||
<measure number="7">
|
||||
@@ -479,72 +481,72 @@
|
||||
<step>G</step>
|
||||
<octave>5</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">begin</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>A</step>
|
||||
<octave>5</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">continue</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>B</step>
|
||||
<octave>5</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">continue</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>A</step>
|
||||
<octave>5</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">end</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>G</step>
|
||||
<octave>5</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">begin</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>F</step>
|
||||
<octave>5</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">continue</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>E</step>
|
||||
<octave>5</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">continue</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>F</step>
|
||||
<octave>5</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">end</beam>
|
||||
</note>
|
||||
</measure>
|
||||
<measure number="8">
|
||||
@@ -553,72 +555,72 @@
|
||||
<step>G</step>
|
||||
<octave>5</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">begin</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>F</step>
|
||||
<octave>5</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">continue</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>E</step>
|
||||
<octave>5</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">continue</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>D</step>
|
||||
<octave>5</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">end</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>E</step>
|
||||
<octave>5</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">begin</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>F</step>
|
||||
<octave>5</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">continue</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>E</step>
|
||||
<octave>5</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">continue</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>D</step>
|
||||
<octave>5</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">end</beam>
|
||||
</note>
|
||||
</measure>
|
||||
<measure number="9">
|
||||
@@ -627,72 +629,72 @@
|
||||
<step>C</step>
|
||||
<octave>5</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">begin</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>B</step>
|
||||
<octave>4</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">continue</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>A</step>
|
||||
<octave>4</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">continue</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>G</step>
|
||||
<octave>4</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">end</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>A</step>
|
||||
<octave>4</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">begin</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>B</step>
|
||||
<octave>4</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">continue</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>A</step>
|
||||
<octave>4</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">continue</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>G</step>
|
||||
<octave>4</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">end</beam>
|
||||
</note>
|
||||
</measure>
|
||||
<measure number="10">
|
||||
@@ -701,72 +703,72 @@
|
||||
<step>F</step>
|
||||
<octave>4</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">begin</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>E</step>
|
||||
<octave>4</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">continue</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>D</step>
|
||||
<octave>4</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">continue</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>C</step>
|
||||
<octave>4</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">end</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>B</step>
|
||||
<octave>3</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">begin</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>C</step>
|
||||
<octave>4</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">continue</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>D</step>
|
||||
<octave>4</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">continue</beam>
|
||||
</note>
|
||||
<note>
|
||||
<pitch>
|
||||
<step>E</step>
|
||||
<octave>4</octave>
|
||||
</pitch>
|
||||
<duration>6</duration>
|
||||
<voice>1</voice>
|
||||
<duration>4</duration>
|
||||
<type>eighth</type>
|
||||
<beam number="1">end</beam>
|
||||
</note>
|
||||
</measure>
|
||||
<measure number="11">
|
||||
@@ -775,8 +777,7 @@
|
||||
<step>F</step>
|
||||
<octave>4</octave>
|
||||
</pitch>
|
||||
<duration>48</duration>
|
||||
<voice>1</voice>
|
||||
<duration>32</duration>
|
||||
<type>whole</type>
|
||||
</note>
|
||||
</measure>
|
||||
@@ -1,2 +1 @@
|
||||
"""Build-time score generation for Product Vamp."""
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@ from generator.musicxml import load_musicxml
|
||||
|
||||
RUNTIME_FONT_FILES = ("Petaluma.otf", "PetalumaScript.otf", "OFL.txt")
|
||||
|
||||
|
||||
def copy_runtime_fonts(static_assets_dir: Path, output_dir: Path) -> None:
|
||||
source = static_assets_dir / "fonts" / "petaluma"
|
||||
destination = output_dir / "fonts" / "petaluma"
|
||||
@@ -21,7 +20,6 @@ def copy_runtime_fonts(static_assets_dir: Path, output_dir: Path) -> None:
|
||||
for name in RUNTIME_FONT_FILES:
|
||||
shutil.copy2(source / name, destination / name)
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description="Generate Product Vamp package score assets.")
|
||||
parser.add_argument("--data-dir", type=Path, required=True)
|
||||
@@ -31,15 +29,12 @@ def parse_args() -> argparse.Namespace:
|
||||
parser.add_argument("--check", action="store_true")
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def generate_assets(args: argparse.Namespace) -> None:
|
||||
data_dir = args.data_dir.resolve()
|
||||
output_dir = args.output_dir.resolve()
|
||||
static_assets_dir = args.static_assets_dir.resolve()
|
||||
|
||||
for source in sorted(data_dir.rglob('*.musicxml')):
|
||||
for source in sorted(data_dir.rglob('*.xml')):
|
||||
load_musicxml(source)
|
||||
|
||||
output_dir.parent.mkdir(parents=True, exist_ok=True)
|
||||
with tempfile.TemporaryDirectory(prefix='.vamp-assets-', dir=output_dir.parent) as temporary:
|
||||
staged = Path(temporary) / 'assets'
|
||||
@@ -71,6 +66,5 @@ def main() -> int:
|
||||
generate_assets(parse_args())
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
||||
@@ -1,2 +1 @@
|
||||
"""Performance arrangement generation."""
|
||||
|
||||
|
||||
@@ -5,17 +5,13 @@ from typing import Literal
|
||||
|
||||
from generator.score import body_start_measure_index
|
||||
|
||||
SectionRole = Literal["rhythm", "accompaniment", "melody", "vocal", "rest", "slash"]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ArrangementSection:
|
||||
label: str
|
||||
source_measure_index: int
|
||||
measure_count: int
|
||||
section_index: int
|
||||
role: SectionRole
|
||||
|
||||
role: Literal["rhythm", "accompaniment", "melody", "vocal", "rest", "slash"]
|
||||
|
||||
def phrase_aware_edge_measure_count(body_measure_count: int) -> int:
|
||||
if body_measure_count >= 8:
|
||||
@@ -24,8 +20,7 @@ def phrase_aware_edge_measure_count(body_measure_count: int) -> int:
|
||||
return 4
|
||||
return body_measure_count
|
||||
|
||||
|
||||
def section_role(part_type: str, section_index: int) -> SectionRole:
|
||||
def section_role(part_type: str, section_index: int) -> Literal["rhythm", "accompaniment", "melody", "vocal", "rest", "slash"]:
|
||||
if part_type == "acoustic":
|
||||
return "slash" if section_index == 3 else "rhythm"
|
||||
if part_type == "electric":
|
||||
@@ -38,7 +33,6 @@ def section_role(part_type: str, section_index: int) -> SectionRole:
|
||||
return "rest"
|
||||
return "vocal" if section_index in {1, 5, 6} else "rest"
|
||||
|
||||
|
||||
def full_arrangement_sections(measures: list[dict], part_type: str) -> list[ArrangementSection]:
|
||||
if not measures:
|
||||
return []
|
||||
@@ -62,4 +56,3 @@ def full_arrangement_sections(measures: list[dict], part_type: str) -> list[Arra
|
||||
for label, start, count, index in specs
|
||||
if count > 0
|
||||
]
|
||||
|
||||
|
||||
@@ -10,17 +10,14 @@ from generator.model import NATURAL
|
||||
STANDARD_TUNING = ((6, 40), (5, 45), (4, 50), (3, 55), (2, 59), (1, 64))
|
||||
WRITTEN_GUITAR_OCTAVE_OFFSET = 12
|
||||
|
||||
|
||||
class TabPosition(NamedTuple):
|
||||
string_number: int
|
||||
fret: int
|
||||
midi: int
|
||||
|
||||
|
||||
def chord_pitch_classes(symbol: str | dict) -> tuple[int, ...]:
|
||||
return harmony.pitch_classes(harmony.parse_symbol(symbol) if isinstance(symbol, str) else symbol)
|
||||
|
||||
|
||||
def positions_for_string(string_number: int, open_midi: int, pitch_classes: tuple[int, ...]) -> list[TabPosition]:
|
||||
result: list[TabPosition] = []
|
||||
for fret in range(0, 13):
|
||||
@@ -29,7 +26,6 @@ def positions_for_string(string_number: int, open_midi: int, pitch_classes: tupl
|
||||
result.append(TabPosition(string_number, fret, midi))
|
||||
return result
|
||||
|
||||
|
||||
def candidate_score(candidate: tuple[TabPosition, ...], pitch_classes: tuple[int, ...]) -> int:
|
||||
fretted = [position.fret for position in candidate if position.fret > 0]
|
||||
if not fretted:
|
||||
@@ -43,7 +39,6 @@ def candidate_score(candidate: tuple[TabPosition, ...], pitch_classes: tuple[int
|
||||
open_penalty = sum(1 for position in candidate if position.fret == 0)
|
||||
return missing * 1000 + span * 80 + abs(low - 5) * 12 + open_penalty * 8 + sum(position.fret for position in candidate)
|
||||
|
||||
|
||||
def find_playable_voicing(symbol: str | dict) -> list[TabPosition]:
|
||||
definition = harmony.parse_symbol(symbol) if isinstance(symbol, str) else symbol
|
||||
semantic_degrees = harmony.degrees(definition)
|
||||
@@ -63,7 +58,6 @@ def find_playable_voicing(symbol: str | dict) -> list[TabPosition]:
|
||||
pitch_classes += (bass_pc,)
|
||||
if not pitch_classes:
|
||||
return []
|
||||
|
||||
string_sets = ((6, 4, 3, 2), (5, 4, 3, 2), (4, 3, 2, 1), (6, 5, 4, 3), (5, 3, 2, 1))
|
||||
by_string = {string_number: positions_for_string(string_number, open_midi, pitch_classes) for string_number, open_midi in STANDARD_TUNING}
|
||||
best: tuple[TabPosition, ...] | None = None
|
||||
@@ -87,7 +81,6 @@ def find_playable_voicing(symbol: str | dict) -> list[TabPosition]:
|
||||
best_score = score
|
||||
return sorted(best or [], key=lambda item: item.midi)
|
||||
|
||||
|
||||
def pitches_for_voicing(symbol: str | dict) -> list[dict[str, int | str]]:
|
||||
definition = harmony.parse_symbol(symbol) if isinstance(symbol, str) else symbol
|
||||
from generator.transposition import shift_pitch
|
||||
|
||||
@@ -15,7 +15,6 @@ from generator.score import copy_source_voice, make_arranged_document, make_meas
|
||||
PART_TYPES = ('acoustic', 'electric', 'vocal')
|
||||
FINAL_SECTION_INDEX = 6
|
||||
|
||||
|
||||
def propagated_chords(measures: list[dict], *, include_optional: bool = True) -> list[dict | None]:
|
||||
result = []
|
||||
current = None
|
||||
@@ -26,7 +25,6 @@ def propagated_chords(measures: list[dict], *, include_optional: bool = True) ->
|
||||
current = h
|
||||
return result
|
||||
|
||||
|
||||
def active_chord(measure: dict, onset: Fraction, previous: dict | None) -> dict | None:
|
||||
active = previous
|
||||
for h in measure['harmonies']:
|
||||
@@ -35,7 +33,6 @@ def active_chord(measure: dict, onset: Fraction, previous: dict | None) -> dict
|
||||
active = h
|
||||
return active if active and active['kind'] != 'none' else None
|
||||
|
||||
|
||||
def generated_voice(measure: dict, role: str, previous: dict | None) -> list[dict]:
|
||||
length = fraction(measure['duration'])
|
||||
if role == 'rest':
|
||||
@@ -85,7 +82,6 @@ def generated_voice(measure: dict, role: str, previous: dict | None) -> list[dic
|
||||
events.append(make_event(f'rest-{len(events)}', cursor, length - cursor, 'rest'))
|
||||
return events
|
||||
|
||||
|
||||
def arrange_document(document: dict, part_type: str) -> dict:
|
||||
if part_type not in PART_TYPES:
|
||||
raise ValueError(f'unknown arrangement part: {part_type}')
|
||||
@@ -167,12 +163,11 @@ def arrange_document(document: dict, part_type: str) -> dict:
|
||||
result['metadata'].setdefault('tempo', 124)
|
||||
return result
|
||||
|
||||
|
||||
def generate_performance_scores(source_dir: Path, output_dir: Path) -> int:
|
||||
if not source_dir.exists():
|
||||
raise ValueError(f'performance source directory not found: {source_dir}')
|
||||
count = 0
|
||||
for path in sorted(source_dir.glob('*.musicxml')):
|
||||
for path in sorted(source_dir.glob('*.xml')):
|
||||
document = load_musicxml(path)
|
||||
for part in PART_TYPES:
|
||||
write_json(output_dir / f'{slugify(path.stem)}-{part}.json', arrange_document(document, part))
|
||||
|
||||
@@ -25,7 +25,6 @@ for family, prefix in (('dominant',''), ('major','maj'), ('minor','-')):
|
||||
for suffix, number in (('ninth', '9'), ('11th', '11'), ('13th', '13')):
|
||||
SUFFIXES[f'{family}-{suffix}'] = prefix + number
|
||||
|
||||
|
||||
def degrees(harmony: dict) -> dict[int, int]:
|
||||
kind = harmony['kind']
|
||||
if kind not in KINDS:
|
||||
@@ -53,11 +52,9 @@ def degrees(harmony: dict) -> dict[int, int]:
|
||||
raise ValueError('harmony contains no pitch information')
|
||||
return result
|
||||
|
||||
|
||||
def degree_semitones(degree: int, alter: int) -> int:
|
||||
return 12 * ((degree - 1) // 7) + (0, 2, 4, 5, 7, 9, 11)[(degree - 1) % 7] + alter
|
||||
|
||||
|
||||
def pitch_classes(harmony: dict) -> tuple[int, ...]:
|
||||
if harmony['kind'] == 'none':
|
||||
return ()
|
||||
@@ -68,7 +65,6 @@ def pitch_classes(harmony: dict) -> tuple[int, ...]:
|
||||
values.append((NATURAL[bass['letter']] + bass['alter']) % 12)
|
||||
return tuple(dict.fromkeys(values))
|
||||
|
||||
|
||||
def symbol(harmony: dict) -> str:
|
||||
if harmony['kind'] == 'none':
|
||||
return 'N.C.'
|
||||
@@ -82,7 +78,6 @@ def symbol(harmony: dict) -> str:
|
||||
result += '/' + pitch_name(bass)
|
||||
return result
|
||||
|
||||
|
||||
def parse_symbol(text: str) -> dict:
|
||||
import re
|
||||
match = re.fullmatch(r'([A-Ga-g][#b]*)([^/]*)(?:/([A-Ga-g][#b]*))?', text.strip())
|
||||
|
||||
@@ -12,14 +12,12 @@ NATURAL = {'c': 0, 'd': 2, 'e': 4, 'f': 5, 'g': 7, 'a': 9, 'b': 11}
|
||||
LETTERS = 'cdefgab'
|
||||
MODE_STEPS = {'major': 0, 'ionian': 0, 'dorian': 1, 'phrygian': 2, 'lydian': 3, 'mixolydian': 4, 'minor': 5, 'aeolian': 5, 'locrian': 6}
|
||||
|
||||
|
||||
def rational(value: Fraction | int) -> dict[str, int]:
|
||||
value = Fraction(value)
|
||||
if abs(value.numerator) > MAX_INTEGER or value.denominator > MAX_INTEGER:
|
||||
raise ValueError('musical time exceeds the supported 64-bit rational range')
|
||||
return {'numerator': value.numerator, 'denominator': value.denominator}
|
||||
|
||||
|
||||
def fraction(value: Any) -> Fraction:
|
||||
if not isinstance(value, dict) or set(value) != {'numerator', 'denominator'}:
|
||||
raise ValueError('musical time must be a numerator/denominator object')
|
||||
@@ -31,14 +29,12 @@ def fraction(value: Any) -> Fraction:
|
||||
raise ValueError('musical time must be reduced')
|
||||
return result
|
||||
|
||||
|
||||
def written_length(written: dict) -> Fraction:
|
||||
kind, dots = written['type'], written.get('dots', 0)
|
||||
if kind not in TYPE_LENGTHS or type(dots) is not int or not 0 <= dots <= 16:
|
||||
raise ValueError('invalid written duration or augmentation-dot count (maximum 16)')
|
||||
return TYPE_LENGTHS[kind] * (2 - Fraction(1, 2 ** dots))
|
||||
|
||||
|
||||
def written_for(length: Fraction) -> dict:
|
||||
for name, base in TYPE_LENGTHS.items():
|
||||
for dots in range(17):
|
||||
@@ -48,7 +44,6 @@ def written_for(length: Fraction) -> dict:
|
||||
base = next((name for name, size in TYPE_LENGTHS.items() if size <= length), '1024th')
|
||||
return {'type': base, 'dots': 0, 'ratio': rational(length / TYPE_LENGTHS[base])}
|
||||
|
||||
|
||||
def metric_length(written: dict) -> Fraction:
|
||||
ratio = fraction(written.get('ratio', rational(1)))
|
||||
if ratio <= 0:
|
||||
@@ -57,7 +52,6 @@ def metric_length(written: dict) -> Fraction:
|
||||
written_length(written['normal'])
|
||||
return written_length(written) * ratio
|
||||
|
||||
|
||||
def pitch_number(pitch: dict) -> int:
|
||||
letter = pitch['letter'].lower()
|
||||
alter, octave = pitch.get('alter', 0), pitch['octave']
|
||||
@@ -65,18 +59,15 @@ def pitch_number(pitch: dict) -> int:
|
||||
raise ValueError('invalid spelled pitch')
|
||||
return 12 * (octave + 1) + NATURAL[letter] + alter
|
||||
|
||||
|
||||
def pitch_name(pitch: dict) -> str:
|
||||
alter = pitch.get('alter', 0)
|
||||
return pitch['letter'].upper() + ('#' * alter if alter > 0 else 'b' * -alter)
|
||||
|
||||
|
||||
def parse_pitch_name(name: str) -> dict:
|
||||
if not name or name[0].lower() not in NATURAL or any(c not in '#b' for c in name[1:]):
|
||||
raise ValueError(f'invalid pitch name: {name!r}')
|
||||
return {'letter': name[0].lower(), 'alter': name[1:].count('#') - name[1:].count('b')}
|
||||
|
||||
|
||||
def key_from_fifths(fifths: int, mode: str = 'major') -> dict:
|
||||
if mode not in MODE_STEPS and mode != 'none':
|
||||
raise ValueError(f'unsupported mode: {mode}')
|
||||
@@ -91,24 +82,20 @@ def key_from_fifths(fifths: int, mode: str = 'major') -> dict:
|
||||
tonic = {'letter': letter, 'alter': signature[LETTERS.index(letter)]}
|
||||
return {'fifths': fifths, 'mode': mode, 'tonic': tonic if mode != 'none' else None, 'alterations': signature}
|
||||
|
||||
|
||||
def key_label(key: dict) -> str:
|
||||
if not key.get('tonic'):
|
||||
return 'No key'
|
||||
label = pitch_name(key['tonic'])
|
||||
return label if key['mode'] in ('major', 'ionian') else label + ' ' + key['mode']
|
||||
|
||||
|
||||
def meter_length(meter: dict) -> Fraction:
|
||||
return sum((Fraction(sum(group['beats']) * 4, group['unit']) for group in meter.get('groups', [])), Fraction())
|
||||
|
||||
|
||||
def meter_label(meter: dict) -> str:
|
||||
if meter.get('unmetered'):
|
||||
return 'senza misura'
|
||||
return '+'.join('+'.join(map(str, group['beats'])) + '/' + str(group['unit']) for group in meter['groups'])
|
||||
|
||||
|
||||
def beat_lengths(meter: dict) -> list[Fraction]:
|
||||
result = []
|
||||
for group in meter.get('groups', []):
|
||||
@@ -121,7 +108,6 @@ def beat_lengths(meter: dict) -> list[Fraction]:
|
||||
result.extend([Fraction(4, unit)] * beats[0])
|
||||
return result
|
||||
|
||||
|
||||
def validate_meter(meter: dict) -> None:
|
||||
groups = meter.get('groups', [])
|
||||
if not groups and not meter.get('unmetered'):
|
||||
@@ -132,7 +118,6 @@ def validate_meter(meter: dict) -> None:
|
||||
if any(type(b) is not int or not 0 < b <= 1024 for b in group['beats']):
|
||||
raise ValueError('meter count must be between 1 and 1024')
|
||||
|
||||
|
||||
def make_event(identifier: str, onset: Fraction, duration: Fraction, kind: str, pitches: list[dict] | None = None, written: dict | None = None) -> dict:
|
||||
if duration <= 0:
|
||||
raise ValueError('generated event duration must be positive')
|
||||
@@ -142,7 +127,6 @@ def make_event(identifier: str, onset: Fraction, duration: Fraction, kind: str,
|
||||
event['pitches'].append({**copy.deepcopy(p), 'id': f'{identifier}-p{i + 1}', 'duration': rational(duration), 'written': copy.deepcopy(notation)})
|
||||
return event
|
||||
|
||||
|
||||
def validate_document(document: dict) -> None:
|
||||
if document.get('schema_version') != SCHEMA_VERSION:
|
||||
raise ValueError('unsupported score schema; regenerate score assets with the current generator')
|
||||
|
||||
+12
-27
@@ -4,26 +4,19 @@ import copy
|
||||
from fractions import Fraction
|
||||
from pathlib import Path
|
||||
|
||||
from defusedxml import ElementTree as XML
|
||||
from defusedxml import ElementTree
|
||||
|
||||
from generator import harmony
|
||||
from generator.model import (
|
||||
SCHEMA_VERSION, TYPE_LENGTHS, fraction, key_from_fifths, key_label,
|
||||
meter_label, meter_length, pitch_number, rational, validate_document,
|
||||
written_for, written_length,
|
||||
)
|
||||
from generator.model import (SCHEMA_VERSION, TYPE_LENGTHS, fraction, key_from_fifths, key_label, meter_label, meter_length, pitch_number, rational, validate_document, written_for, written_length, )
|
||||
from generator.styles import canonical_style
|
||||
|
||||
|
||||
class MusicXMLError(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
# Ignoring a container never authorizes ignoring its musical descendants.
|
||||
LAYOUT = {'default-x', 'default-y', 'relative-x', 'relative-y', 'color', 'font-family', 'font-size', 'font-style', 'font-weight', 'halign', 'valign', 'justify', 'placement', 'id'}
|
||||
IGNORED_TOP = {'defaults', 'credit', 'work', 'movement-number', 'movement-title', 'identification', 'part-list'}
|
||||
|
||||
|
||||
class Importer:
|
||||
def __init__(self, path: Path) -> None:
|
||||
self.path = path
|
||||
@@ -44,20 +37,16 @@ class Importer:
|
||||
self.lyric_number = None
|
||||
self.absolute = Fraction()
|
||||
self.tremolo_start = None
|
||||
|
||||
def fail(self, text: str):
|
||||
raise MusicXMLError(f'{self.location}: {text}')
|
||||
|
||||
def children(self, node, allowed: set[str]) -> None:
|
||||
for item in node:
|
||||
if item.tag not in allowed:
|
||||
self.fail(f'unsupported musical element <{item.tag}> in <{node.tag}>')
|
||||
|
||||
def attrs(self, node, allowed=()) -> None:
|
||||
for name in node.attrib:
|
||||
if name not in LAYOUT and name not in allowed:
|
||||
self.fail(f'unsupported {node.tag} attribute {name!r}')
|
||||
|
||||
def number(self, value: str | None, *, integer: bool = False, positive: bool = False) -> Fraction | int:
|
||||
try:
|
||||
result = Fraction(value or '')
|
||||
@@ -67,10 +56,8 @@ class Importer:
|
||||
return int(result) if integer else result
|
||||
except (ValueError, ZeroDivisionError, OverflowError):
|
||||
self.fail(f'invalid {"integer" if integer else "number"}: {value!r}')
|
||||
|
||||
def offset(self, node, cursor: Fraction) -> Fraction:
|
||||
return cursor + self.number(node.findtext('offset', '0')) / self.divisions
|
||||
|
||||
def identity(self, node) -> None:
|
||||
voice = node.findtext('voice', '1' if node.tag in ('note', 'forward') else self.voice_id)
|
||||
staff = node.findtext('staff', '1')
|
||||
@@ -82,7 +69,6 @@ class Importer:
|
||||
self.voice_id = voice
|
||||
if voice != self.voice_id:
|
||||
self.fail('multiple rhythmic voices are not supported')
|
||||
|
||||
def attributes(self, node, onset: Fraction) -> dict:
|
||||
self.children(node, {'divisions', 'key', 'time', 'clef', 'staves', 'transpose', 'instruments'})
|
||||
result = {'onset': rational(onset)}
|
||||
@@ -164,7 +150,6 @@ class Importer:
|
||||
self.transpose['diatonic'] = (1 if semitones >= 0 else -1) * (7 * (magnitude // 12) + (0, 0, 1, 2, 2, 3, 3, 4, 5, 5, 6, 6)[magnitude % 12])
|
||||
result['transpose'] = copy.deepcopy(self.transpose)
|
||||
return result
|
||||
|
||||
def read_harmony(self, node, cursor: Fraction) -> dict:
|
||||
self.attrs(node, {'print-object', 'print-frame', 'type'})
|
||||
self.children(node, {'root', 'kind', 'degree', 'bass', 'inversion', 'offset', 'staff'})
|
||||
@@ -227,7 +212,6 @@ class Importer:
|
||||
result['bass'] = {'letter': bass_letter, 'alter': target - (12 * (letter_index // 7) + NATURAL[bass_letter])}
|
||||
result['symbol'] = harmony.symbol(result)
|
||||
return result
|
||||
|
||||
def direction(self, node, cursor: Fraction, measure: dict, optional: list[Fraction]) -> None:
|
||||
self.attrs(node, {'print-object', 'directive'})
|
||||
self.children(node, {'direction-type', 'offset', 'staff', 'voice', 'sound'})
|
||||
@@ -271,11 +255,12 @@ class Importer:
|
||||
measure.setdefault('tempos', []).append({'onset': rational(onset), 'written': written, 'bpm': rational(bpm)})
|
||||
sound = node.find('sound')
|
||||
if sound is not None:
|
||||
self.attrs(sound, {'tempo'})
|
||||
self.children(sound, set())
|
||||
if sound.get('tempo') and not any(fraction(t['onset'])==onset for t in measure.get('tempos', [])):
|
||||
measure.setdefault('tempos', []).append({'onset':rational(onset), 'written':{'type':'quarter','dots':0}, 'bpm':rational(self.number(sound.get('tempo'), positive=True))})
|
||||
|
||||
self.sound(sound, onset, measure)
|
||||
def sound(self, node, onset: Fraction, measure: dict) -> None:
|
||||
self.attrs(node, {'tempo'})
|
||||
self.children(node, set())
|
||||
if node.get('tempo') and not any(fraction(t['onset']) == onset for t in measure.get('tempos', [])):
|
||||
measure.setdefault('tempos', []).append({'onset': rational(onset), 'written': {'type': 'quarter', 'dots': 0}, 'bpm': rational(self.number(node.get('tempo'), positive=True))})
|
||||
def read_note(self, node, cursor: Fraction, previous: dict | None) -> tuple[dict, Fraction, bool]:
|
||||
self.attrs(node, {'print-object', 'print-dot', 'print-spacing', 'default-x', 'dynamics', 'end-dynamics'})
|
||||
if node.get('dynamics') or node.get('end-dynamics'):
|
||||
@@ -454,9 +439,8 @@ class Importer:
|
||||
self.fail('tuplet stop without start')
|
||||
self.active_tuplets.pop(number)['end'] = note['id']
|
||||
return note, cursor if is_chord else cursor + duration, is_chord
|
||||
|
||||
def read(self) -> dict:
|
||||
root = XML.parse(self.path, forbid_entities=True, forbid_external=True).getroot()
|
||||
root = ElementTree.parse(self.path, forbid_entities=True, forbid_external=True).getroot()
|
||||
if root.tag != 'score-partwise' or root.get('version') not in ('3.1', '4.0'):
|
||||
self.fail('expected uncompressed score-partwise MusicXML 3.1 or 4.0')
|
||||
self.children(root, IGNORED_TOP | {'part'})
|
||||
@@ -479,7 +463,7 @@ class Importer:
|
||||
self.children(parts[0], {'measure'})
|
||||
for index, bar in enumerate(parts[0]):
|
||||
self.location = f'{self.path}: part {parts[0].get("id")}, measure {bar.get("number", str(index + 1))}'
|
||||
self.children(bar, {'attributes','note','direction','harmony','forward','backup','barline','print'})
|
||||
self.children(bar, {'attributes', 'note', 'direction', 'sound', 'harmony', 'forward', 'backup', 'barline', 'print'})
|
||||
self.attrs(bar, {'number', 'implicit', 'non-controlling', 'width', 'text'})
|
||||
cursor = Fraction()
|
||||
events = []
|
||||
@@ -502,6 +486,8 @@ class Importer:
|
||||
measure['harmonies'].append(self.read_harmony(item, cursor))
|
||||
elif item.tag == 'direction':
|
||||
self.direction(item, cursor, measure, optional)
|
||||
elif item.tag == 'sound':
|
||||
self.sound(item, cursor, measure)
|
||||
elif item.tag == 'backup':
|
||||
self.fail('backward timeline movement requires unsupported independent voices')
|
||||
elif item.tag == 'forward':
|
||||
@@ -551,7 +537,6 @@ class Importer:
|
||||
validate_document(result)
|
||||
return result
|
||||
|
||||
|
||||
def load_musicxml(path: Path) -> dict:
|
||||
importer = Importer(Path(path))
|
||||
try:
|
||||
|
||||
@@ -1,2 +1 @@
|
||||
"""Practice score generation."""
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ from generator.score import copy_source_voice, make_arranged_document, make_meas
|
||||
from generator.musicxml import load_musicxml
|
||||
from generator.transposition import TARGET_KEYS, transpose_document
|
||||
|
||||
|
||||
def practice_document(document: dict) -> dict:
|
||||
staff = source_staff(document)
|
||||
source_measures = staff.get("measures")
|
||||
@@ -36,11 +35,9 @@ def practice_document(document: dict) -> dict:
|
||||
octave_shifts=staff.get('octave_shifts', []),
|
||||
)
|
||||
|
||||
|
||||
def key_file_name(key: str) -> str:
|
||||
return slugify(key.replace("#", "sharp")) + ".json"
|
||||
|
||||
|
||||
def generate_chord_practice(source_path: Path, source_root: Path, output_dir: Path) -> int:
|
||||
document = practice_document(load_musicxml(source_path))
|
||||
metadata = document.get("metadata", {})
|
||||
@@ -51,7 +48,6 @@ def generate_chord_practice(source_path: Path, source_root: Path, output_dir: Pa
|
||||
write_json(output_dir / "chord" / lesson_group / exercise_id / key_file_name(key), document)
|
||||
return 1
|
||||
|
||||
|
||||
def generate_melody_practice(source_path: Path, source_root: Path, output_dir: Path) -> int:
|
||||
base_document = practice_document(load_musicxml(source_path))
|
||||
relative = source_path.relative_to(source_root)
|
||||
@@ -63,17 +59,16 @@ def generate_melody_practice(source_path: Path, source_root: Path, output_dir: P
|
||||
count += 1
|
||||
return count
|
||||
|
||||
|
||||
def generate_practice_scores(source_dir: Path, output_dir: Path) -> int:
|
||||
if not source_dir.exists():
|
||||
return 0
|
||||
count = 0
|
||||
chord_root = source_dir / "chord"
|
||||
if chord_root.exists():
|
||||
for path in sorted(chord_root.rglob("*.musicxml")):
|
||||
for path in sorted(chord_root.rglob("*.xml")):
|
||||
count += generate_chord_practice(path, chord_root, output_dir)
|
||||
melody_root = source_dir / "melody"
|
||||
if melody_root.exists():
|
||||
for path in sorted(melody_root.rglob("*.musicxml")):
|
||||
for path in sorted(melody_root.rglob("*.xml")):
|
||||
count += generate_melody_practice(path, melody_root, output_dir)
|
||||
return count
|
||||
|
||||
@@ -7,51 +7,41 @@ from pathlib import Path
|
||||
|
||||
from generator.model import SCHEMA_VERSION, validate_document
|
||||
|
||||
|
||||
def load_json(path: Path) -> dict:
|
||||
document = json.loads(path.read_text(encoding='utf-8'))
|
||||
validate_document(document)
|
||||
return document
|
||||
|
||||
|
||||
def write_json(path: Path, document: dict) -> None:
|
||||
validate_document(document)
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(json.dumps(document, indent=2, ensure_ascii=False) + '\n', encoding='utf-8')
|
||||
|
||||
|
||||
def slugify(value: str) -> str:
|
||||
return re.sub(r'[^a-z0-9]+', '-', value.strip().lower()).strip('-') or 'score'
|
||||
|
||||
|
||||
def human_label(value: str) -> str:
|
||||
return ' '.join(v.capitalize() for v in value.replace('_', ' ').replace('-', ' ').split()) or 'General'
|
||||
|
||||
|
||||
def source_staff(document: dict) -> dict:
|
||||
if len(document.get('staves', [])) != 1:
|
||||
raise ValueError('exactly one source staff is required')
|
||||
return document['staves'][0]
|
||||
|
||||
|
||||
def first_voice(measure: dict) -> dict:
|
||||
if len(measure.get('voices', [])) != 1:
|
||||
raise ValueError('exactly one source voice is required')
|
||||
return measure['voices'][0]
|
||||
|
||||
|
||||
def measure_notes(measure: dict) -> list[dict]:
|
||||
return first_voice(measure)['notes']
|
||||
|
||||
|
||||
def leading_pickup_measure_index(measures: list[dict]) -> int | None:
|
||||
return 0 if len(measures) > 1 and measures[0].get('implicit') else None
|
||||
|
||||
|
||||
def body_start_measure_index(measures: list[dict]) -> int:
|
||||
return 1 if leading_pickup_measure_index(measures) is not None else 0
|
||||
|
||||
|
||||
def copy_source_voice(measure: dict, *, strip_lyrics: bool) -> dict:
|
||||
voice = copy.deepcopy(first_voice(measure))
|
||||
if strip_lyrics:
|
||||
@@ -60,13 +50,11 @@ def copy_source_voice(measure: dict, *, strip_lyrics: bool) -> dict:
|
||||
note.pop('lyric_notation', None)
|
||||
return voice
|
||||
|
||||
|
||||
def make_measure(measure_id: str, source: dict, notes: list[dict], *, section_label: str = '') -> dict:
|
||||
result = copy.deepcopy(source)
|
||||
result.update({'id': measure_id, 'section_label': section_label, 'voices': [{'id': 'voice-arranged', 'notes': notes}]})
|
||||
return result
|
||||
|
||||
|
||||
def reidentify(measures: list[dict], source_tuplets: list[dict]) -> list[dict]:
|
||||
"""Clone-local links survive; links cut by an arrangement boundary become attacks."""
|
||||
tone_map, event_map = {}, {}
|
||||
@@ -106,15 +94,12 @@ def reidentify(measures: list[dict], source_tuplets: list[dict]) -> list[dict]:
|
||||
tuplets.append(t)
|
||||
return tuplets
|
||||
|
||||
|
||||
def make_arranged_document(source_document: dict, staff: dict, *, staff_id: str, staff_name: str, role: str, arrangement_part_type: str, measures: list[dict], tuplets=None, octave_shifts=None) -> dict:
|
||||
return {'schema_version': SCHEMA_VERSION, 'metadata': copy.deepcopy(source_document['metadata']), 'staves': [{'id': staff_id, 'name': staff_name, 'clef': copy.deepcopy(staff['clef']), 'role': role, 'arranged': True, 'arrangement_part_type': arrangement_part_type, 'measures': measures, 'tuplets': tuplets or [], 'octave_shifts': octave_shifts or []}]}
|
||||
|
||||
|
||||
def count_json_files(root: Path) -> int:
|
||||
return sum(1 for p in root.rglob('*.json') if p.is_file())
|
||||
|
||||
|
||||
def validate_generated_tree(root: Path) -> None:
|
||||
for path in root.rglob('*.json'):
|
||||
load_json(path)
|
||||
|
||||
@@ -66,20 +66,15 @@ STYLE_ALIASES = {
|
||||
"Medium": DEFAULT_STYLE,
|
||||
}
|
||||
|
||||
|
||||
def canonical_style(value: object) -> str:
|
||||
if value is None:
|
||||
return DEFAULT_STYLE
|
||||
|
||||
if not isinstance(value, str):
|
||||
raise ValueError("style must be a string")
|
||||
|
||||
style = value.strip()
|
||||
if not style:
|
||||
return DEFAULT_STYLE
|
||||
|
||||
style = STYLE_ALIASES.get(style, style)
|
||||
if style not in BUILT_IN_STYLE_SET:
|
||||
raise ValueError(f"style {value!r} is not a supported player style")
|
||||
|
||||
return style
|
||||
|
||||
@@ -8,14 +8,12 @@ from generator.score import load_json, measure_notes
|
||||
from generator.transposition import TARGET_KEYS
|
||||
from generator.model import fraction, pitch_name
|
||||
|
||||
|
||||
def _arranged_staff(document: dict[str, Any]) -> dict[str, Any]:
|
||||
staves = document.get("staves")
|
||||
if not isinstance(staves, list) or not staves or not isinstance(staves[0], dict):
|
||||
raise ValueError("generated score document must contain an arranged staff")
|
||||
return staves[0]
|
||||
|
||||
|
||||
def _measure_summary(staff: dict[str, Any], measure: dict[str, Any], measure_number: int) -> dict[str, Any]:
|
||||
notes = measure_notes(measure)
|
||||
summary: dict[str, Any] = {
|
||||
@@ -35,7 +33,6 @@ def _measure_summary(staff: dict[str, Any], measure: dict[str, Any], measure_num
|
||||
]
|
||||
return summary
|
||||
|
||||
|
||||
def performance_document_summary(document: dict[str, Any]) -> dict[str, Any]:
|
||||
metadata = document.get("metadata")
|
||||
if not isinstance(metadata, dict):
|
||||
@@ -63,14 +60,12 @@ def performance_document_summary(document: dict[str, Any]) -> dict[str, Any]:
|
||||
"measures": measure_summaries,
|
||||
}
|
||||
|
||||
|
||||
def performance_tree_summary(root: Path) -> dict[str, Any]:
|
||||
return {
|
||||
path.name: performance_document_summary(load_json(path))
|
||||
for path in sorted(root.glob("*.json"))
|
||||
}
|
||||
|
||||
|
||||
def validate_expected_performance_summary(expected_path: Path, generated_songs_dir: Path) -> None:
|
||||
if not expected_path.exists():
|
||||
raise ValueError(f"missing generator compatibility summary: {expected_path}")
|
||||
@@ -79,20 +74,17 @@ def validate_expected_performance_summary(expected_path: Path, generated_songs_d
|
||||
actual = performance_tree_summary(generated_songs_dir)
|
||||
if actual == expected:
|
||||
return
|
||||
|
||||
expected_names = set(expected.keys()) if isinstance(expected, dict) else set()
|
||||
actual_names = set(actual.keys())
|
||||
missing = sorted(expected_names - actual_names)
|
||||
extra = sorted(actual_names - expected_names)
|
||||
if missing or extra:
|
||||
raise ValueError(f"performance generator drift: missing={missing}, extra={extra}")
|
||||
|
||||
for name in sorted(actual_names):
|
||||
if actual[name] != expected[name]:
|
||||
raise ValueError(f"performance generator drift in {name}; update generator logic or refresh the expected summary intentionally")
|
||||
raise ValueError("performance generator drift")
|
||||
|
||||
|
||||
def validate_practice_variants(generated_practice_dir: Path) -> None:
|
||||
if not generated_practice_dir.exists():
|
||||
return
|
||||
@@ -105,7 +97,6 @@ def validate_practice_variants(generated_practice_dir: Path) -> None:
|
||||
continue
|
||||
if json_names != expected_names:
|
||||
raise ValueError(f"melody practice generator drift in {exercise_dir}: expected {sorted(expected_names)}, found {sorted(json_names)}")
|
||||
|
||||
chord_root = generated_practice_dir / "chord"
|
||||
if chord_root.exists():
|
||||
for exercise_dir in sorted(path for path in chord_root.rglob("*") if path.is_dir()):
|
||||
|
||||
@@ -6,22 +6,18 @@ from dataclasses import dataclass
|
||||
from generator import harmony
|
||||
from generator.model import LETTERS, NATURAL, key_from_fifths, key_label, parse_pitch_name, pitch_number
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class KeyProfile:
|
||||
name: str
|
||||
root: int
|
||||
fifths: int
|
||||
|
||||
|
||||
KEY_PROFILES = tuple(KeyProfile(name, root, fifths) for root, (name, fifths) in enumerate((('C', 0), ('Db', -5), ('D', 2), ('Eb', -3), ('E', 4), ('F', -1), ('Gb', -6), ('G', 1), ('Ab', -4), ('A', 3), ('Bb', -2), ('B', 5))))
|
||||
TARGET_KEYS = tuple(p.name for p in KEY_PROFILES)
|
||||
|
||||
|
||||
def mod12(value: int) -> int:
|
||||
return value % 12
|
||||
|
||||
|
||||
def find_key(name: str) -> KeyProfile:
|
||||
tonic = parse_pitch_name(name)
|
||||
for fifths in range(-14, 15):
|
||||
@@ -30,12 +26,10 @@ def find_key(name: str) -> KeyProfile:
|
||||
return KeyProfile(name, (NATURAL[tonic['letter']] + tonic['alter']) % 12, fifths)
|
||||
raise ValueError(f'unsupported key signature: {name}')
|
||||
|
||||
|
||||
def signed_interval(source: KeyProfile, target: KeyProfile) -> int:
|
||||
value = (target.root - source.root) % 12
|
||||
return value - 12 if value > 6 else value
|
||||
|
||||
|
||||
def interval_between(source: dict, target: dict) -> tuple[int, int]:
|
||||
semitones = (NATURAL[target['letter']] + target['alter'] - NATURAL[source['letter']] - source['alter']) % 12
|
||||
if semitones > 6:
|
||||
@@ -45,7 +39,6 @@ def interval_between(source: dict, target: dict) -> tuple[int, int]:
|
||||
steps -= 7
|
||||
return steps, semitones
|
||||
|
||||
|
||||
def shift_pitch(pitch: dict, steps: int, semitones: int) -> dict:
|
||||
output = copy.deepcopy(pitch)
|
||||
original_octave = pitch.get('octave', 4)
|
||||
@@ -60,7 +53,6 @@ def shift_pitch(pitch: dict, steps: int, semitones: int) -> dict:
|
||||
output['accidental_display']['name'] = {-3: 'triple-flat', -2: 'flat-flat', -1: 'flat', 0: 'natural', 1: 'sharp', 2: 'double-sharp', 3: 'triple-sharp'}.get(output['alter'], 'compound')
|
||||
return output
|
||||
|
||||
|
||||
def shift_key(key: dict, steps: int, semitones: int) -> dict:
|
||||
tonic = shift_pitch(key['tonic'], steps, semitones) if key.get('tonic') else None
|
||||
alterations = [0] * 7
|
||||
@@ -78,7 +70,6 @@ def shift_key(key: dict, steps: int, semitones: int) -> dict:
|
||||
result['custom'] = True
|
||||
return result
|
||||
|
||||
|
||||
def transpose_document(document: dict, target_key_name: str) -> dict:
|
||||
output = copy.deepcopy(document)
|
||||
metadata = output['metadata']
|
||||
@@ -108,7 +99,6 @@ def transpose_document(document: dict, target_key_name: str) -> dict:
|
||||
note['pitches'] = [shift_pitch(p, steps, semitones) for p in note.get('pitches', [])]
|
||||
return output
|
||||
|
||||
|
||||
def concert_document(document: dict) -> dict:
|
||||
"""Normalize MusicXML instrument transposition before concert-pitch arranging."""
|
||||
output = copy.deepcopy(document)
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <modulus/types.hpp>
|
||||
#include <vamp_style.hpp>
|
||||
|
||||
|
||||
namespace vamp_pico {
|
||||
|
||||
constexpr auto display_width = std::int32_t{320};
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <vamp_protocol/page_control.hpp>
|
||||
#include <modulus/types.hpp>
|
||||
|
||||
|
||||
namespace vamp_pico {
|
||||
|
||||
struct remote_display_state {
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
#include <modulus/types.hpp>
|
||||
|
||||
|
||||
namespace vamp_pico {
|
||||
|
||||
struct touch_point {
|
||||
@@ -11,8 +12,7 @@ struct touch_point {
|
||||
std::int32_t y{};
|
||||
};
|
||||
|
||||
class touch_controller {
|
||||
public:
|
||||
struct touch_controller {
|
||||
auto init() -> void;
|
||||
auto read(touch_point &point) -> std::bool_t;
|
||||
};
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <modulus/types.hpp>
|
||||
|
||||
|
||||
namespace vamp_protocol {
|
||||
|
||||
constexpr std::char_t root_path[] = "/";
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
#include <modulus/types.hpp>
|
||||
|
||||
|
||||
namespace vamp_style {
|
||||
|
||||
struct color {
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
#include <modulus/types.hpp>
|
||||
|
||||
|
||||
namespace vamp_style {
|
||||
|
||||
constexpr auto ui_font_path = std::string_view{"/usr/share/product-vamp/fonts/petaluma/PetalumaScript.otf"};
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
#include <modulus/types.hpp>
|
||||
|
||||
|
||||
namespace vamp_zero {
|
||||
|
||||
struct command_line {
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include <vamp_zero/config.hpp>
|
||||
#include <vamp_zero/types.hpp>
|
||||
|
||||
|
||||
namespace vamp_zero {
|
||||
|
||||
class audio_controller {
|
||||
@@ -19,12 +20,9 @@ public:
|
||||
audio_controller(vamp_zero::audio_controller const &) = delete;
|
||||
auto operator=(vamp_zero::audio_controller const &) -> vamp_zero::audio_controller & = delete;
|
||||
~audio_controller();
|
||||
|
||||
auto sync(vamp_zero::app_state const &state) -> void;
|
||||
|
||||
private:
|
||||
auto make_metronome_config(vamp_zero::app_state const &state) const -> sonus::metronome_config;
|
||||
|
||||
vamp_zero::audio_config config_{};
|
||||
sonus::engine engine_;
|
||||
mutable std::mutex mutex_{};
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
#include <vamp_style.hpp>
|
||||
|
||||
|
||||
namespace vamp_zero {
|
||||
|
||||
auto to_luma(vamp_style::color value) -> luma::color;
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
#include <modulus/types.hpp>
|
||||
#include <vamp_zero/types.hpp>
|
||||
|
||||
|
||||
namespace vamp_zero {
|
||||
|
||||
struct runtime_config {
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include <vamp_zero/config.hpp>
|
||||
#include <vamp_zero/types.hpp>
|
||||
|
||||
|
||||
namespace vamp_zero {
|
||||
|
||||
auto async_ensure_database(boost::asio::any_io_executor executor, vamp_zero::database_config config) -> boost::asio::awaitable<std::expected<void, modulus::error>>;
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
#include <modulus.hpp>
|
||||
|
||||
|
||||
namespace vamp_zero {
|
||||
|
||||
enum class error_code : std::int32_t {
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
#include <vamp_zero/types.hpp>
|
||||
|
||||
|
||||
namespace vamp_zero {
|
||||
|
||||
auto selected_group(vamp_zero::app_state const &state) -> vamp_zero::song_group const *;
|
||||
|
||||
@@ -3,10 +3,15 @@
|
||||
#include <expected>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
#include <modulus.hpp>
|
||||
|
||||
#include <vamp_zero/types.hpp>
|
||||
|
||||
|
||||
namespace vamp_zero {
|
||||
|
||||
auto parse_notation_document(std::string_view json, std::string source_path) -> std::expected<score_document, modulus::error>;
|
||||
auto written_quarters(score_written_duration const& written) -> double;
|
||||
auto written_quarters(score_written_duration const &written) -> std::float64_t;
|
||||
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include <vamp_protocol/page_control.hpp>
|
||||
#include <vamp_zero/types.hpp>
|
||||
|
||||
|
||||
namespace vamp_zero {
|
||||
|
||||
auto apply_app_command(vamp_zero::app_state state, vamp_protocol::page_command command) -> vamp_zero::app_state;
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
#pragma once
|
||||
|
||||
#include <charconv>
|
||||
#include <concepts>
|
||||
#include <expected>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <system_error>
|
||||
#include <type_traits>
|
||||
|
||||
#include <modulus/result.hpp>
|
||||
|
||||
#include <vamp_zero/errors.hpp>
|
||||
|
||||
|
||||
namespace vamp_zero {
|
||||
|
||||
template <std::integral VALUE>
|
||||
requires(!std::is_same_v<VALUE, std::bool_t>)
|
||||
auto parse_integer(std::string_view text, std::string_view name) -> std::expected<VALUE, modulus::error> {
|
||||
auto invalid = [&](std::string_view reason) {
|
||||
return modulus::fail<VALUE>(
|
||||
make_error(error_code::INVALID_ARGUMENT, std::string{name} + " " + std::string{reason}));
|
||||
};
|
||||
if (text.empty()) {
|
||||
return invalid("must be an integer");
|
||||
}
|
||||
auto value = VALUE{};
|
||||
auto end = text.data() + text.size();
|
||||
auto parsed = std::from_chars(text.data(), end, value);
|
||||
if (parsed.ec == std::errc::result_out_of_range) {
|
||||
return invalid("is out of range");
|
||||
}
|
||||
if (parsed.ec != std::errc{} || parsed.ptr != end) {
|
||||
return invalid("must be an integer");
|
||||
}
|
||||
return modulus::success(value);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -13,19 +13,18 @@
|
||||
#include <vamp_zero/config.hpp>
|
||||
#include <vamp_zero/types.hpp>
|
||||
|
||||
|
||||
namespace vamp_zero {
|
||||
|
||||
class control_state {
|
||||
public:
|
||||
using observer = std::function<void(vamp_zero::app_state)>;
|
||||
|
||||
control_state(vamp_zero::config config, vamp_zero::app_state state);
|
||||
auto config() const -> vamp_zero::config;
|
||||
auto snapshot() const -> vamp_zero::app_state;
|
||||
auto apply(vamp_protocol::page_command command) -> vamp_zero::app_state;
|
||||
auto heartbeat() -> vamp_zero::app_state;
|
||||
auto refresh_connection() -> std::optional<vamp_zero::app_state>;
|
||||
auto set_observer(vamp_zero::control_state::observer observer) -> void;
|
||||
auto set_observer(std::function<void(vamp_zero::app_state)> observer) -> void;
|
||||
private:
|
||||
auto pico_connected_locked() const -> std::bool_t;
|
||||
auto snapshot_locked() const -> vamp_zero::app_state;
|
||||
@@ -33,7 +32,7 @@ private:
|
||||
vamp_zero::config config_{};
|
||||
vamp_zero::app_state state_{};
|
||||
std::chrono::steady_clock::time_point last_heartbeat_{};
|
||||
vamp_zero::control_state::observer observer_{};
|
||||
std::function<void(vamp_zero::app_state)> observer_{};
|
||||
};
|
||||
|
||||
auto make_router(vamp_zero::control_state &state) -> ligo::service_router;
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
|
||||
#include <vamp_zero/types.hpp>
|
||||
|
||||
|
||||
namespace vamp_zero {
|
||||
|
||||
enum class score_accidental_mark {
|
||||
|
||||
@@ -10,12 +10,15 @@
|
||||
|
||||
#include <modulus/types.hpp>
|
||||
|
||||
|
||||
namespace vamp_zero {
|
||||
|
||||
struct score_time {
|
||||
std::int64_t numerator{};
|
||||
std::int64_t denominator{1};
|
||||
auto quarters() const -> double { return static_cast<double>(numerator) / static_cast<double>(denominator); }
|
||||
auto quarters() const -> double {
|
||||
return static_cast<double>(numerator) / static_cast<double>(denominator);
|
||||
}
|
||||
};
|
||||
|
||||
struct score_written_duration {
|
||||
@@ -181,7 +184,6 @@ struct practice_exercise {
|
||||
std::size_t measure_count{};
|
||||
std::size_t total_pages{};
|
||||
};
|
||||
|
||||
std::string id{};
|
||||
std::string practice_type_id{};
|
||||
std::string practice_type_title{};
|
||||
|
||||
@@ -32,9 +32,7 @@ struct ui_state {
|
||||
|
||||
class zero_ui {
|
||||
public:
|
||||
using command_handler = std::function<void(vamp_protocol::page_command)>;
|
||||
|
||||
zero_ui(boost::asio::any_io_executor executor, luma::application_config config, command_handler command_handler = {});
|
||||
zero_ui(boost::asio::any_io_executor executor, luma::application_config config, std::function<void(vamp_protocol::page_command)> command_handler = {});
|
||||
auto run() -> std::expected<void, modulus::error>;
|
||||
auto stop() -> void;
|
||||
auto publish(vamp_zero::ui_state state) -> std::expected<void, modulus::error>;
|
||||
@@ -48,7 +46,7 @@ private:
|
||||
boost::asio::any_io_executor executor_;
|
||||
boost::asio::steady_timer tap_timer_;
|
||||
luma::application application_;
|
||||
command_handler command_handler_{};
|
||||
std::function<void(vamp_protocol::page_command)> command_handler_{};
|
||||
std::shared_ptr<vamp_zero::score_draw_cache> score_cache_{};
|
||||
mutable std::mutex mutex_{};
|
||||
vamp_zero::ui_state state_{};
|
||||
|
||||
+10
-10
@@ -20,6 +20,7 @@ COMMANDS = (
|
||||
"package",
|
||||
"lsp",
|
||||
"clangd",
|
||||
"format",
|
||||
"run",
|
||||
)
|
||||
COMMON_DIR = pathlib.Path("build") / "build_common"
|
||||
@@ -65,6 +66,9 @@ def runtime_ready(root):
|
||||
and (root / TOOLS_DIR / "run.py").is_file()
|
||||
and (root / RUNTIME_DIR / "modulus" / "cli.py").is_file()
|
||||
and (root / RUNTIME_DIR / "modulus" / "export.py").is_file()
|
||||
and (root / TOOLS_DIR / "format.py").is_file()
|
||||
and (root / RUNTIME_DIR / "modulus" / "formatting.py").is_file()
|
||||
and (root / RUNTIME_DIR / "modulus" / "format_layout.py").is_file()
|
||||
)
|
||||
|
||||
def optional_credentials():
|
||||
@@ -103,13 +107,7 @@ def run(command, input_text=None, capture=False, display_command=None, env=None,
|
||||
def run_with_retries(command, attempts=3, input_text=None, display_command=None, env=None):
|
||||
completed = None
|
||||
for attempt in range(1, attempts + 1):
|
||||
completed = run(
|
||||
command,
|
||||
input_text=input_text,
|
||||
display_command=display_command,
|
||||
env=env,
|
||||
check=False,
|
||||
)
|
||||
completed = run(command, input_text=input_text, display_command=display_command, env=env, check=False)
|
||||
if completed.returncode == 0:
|
||||
return completed
|
||||
if attempt != attempts:
|
||||
@@ -204,9 +202,11 @@ RUN python3 -m venv /opt/modulus-python
|
||||
ENV PATH=/opt/modulus-python/bin:$PATH
|
||||
|
||||
COPY tools/requirements.txt /tmp/vamp-build-requirements.txt
|
||||
ARG MODULUS_TOOLS=build/build_tools
|
||||
COPY ${{MODULUS_TOOLS}}/docker/format-requirements.txt /tmp/modulus-format-requirements.txt
|
||||
|
||||
RUN python -m pip install --no-cache-dir \\
|
||||
conan=={CONAN_VERSION} -r /tmp/vamp-build-requirements.txt
|
||||
conan=={CONAN_VERSION} -r /tmp/vamp-build-requirements.txt -r /tmp/modulus-format-requirements.txt
|
||||
|
||||
LABEL org.khodo.product-vamp.build-requirements="{build_requirements_digest()}"
|
||||
|
||||
@@ -487,9 +487,9 @@ def run_package(argv):
|
||||
|
||||
def delegate(command, argv):
|
||||
root = repo_root()
|
||||
if command == "clangd":
|
||||
if command in {"clangd", "format"}:
|
||||
if not runtime_ready(root):
|
||||
print("Modulus clangd tooling is missing; run ./modulus.py configure first", file=sys.stderr)
|
||||
print(f"Modulus {command} tooling is missing; run ./modulus.py configure first", file=sys.stderr)
|
||||
return 1
|
||||
else:
|
||||
ensure_runtime(root)
|
||||
|
||||
@@ -13,7 +13,6 @@ DEFAULT_REGISTRY_URL = "https://git.thyme-labs.com/api/packages/khodo/generic"
|
||||
DEFAULT_VERSION = "latest"
|
||||
PAGE_LIMIT = 50
|
||||
|
||||
|
||||
def read_config(path):
|
||||
try:
|
||||
data = json.loads(path.read_text(encoding="utf-8"))
|
||||
@@ -33,7 +32,6 @@ def read_config(path):
|
||||
registry_url = DEFAULT_REGISTRY_URL
|
||||
return name, registry_url
|
||||
|
||||
|
||||
def parse_registry_url(registry_url):
|
||||
parsed = urllib.parse.urlparse(registry_url.rstrip("/"))
|
||||
parts = [urllib.parse.unquote(part) for part in parsed.path.split("/") if part]
|
||||
@@ -43,7 +41,6 @@ def parse_registry_url(registry_url):
|
||||
prefix = "/" + "/".join(urllib.parse.quote(part, safe="") for part in parts[:-4]) if parts[:-4] else ""
|
||||
return parsed, prefix, owner
|
||||
|
||||
|
||||
def build_url(parsed, path, query=""):
|
||||
return urllib.parse.urlunparse((
|
||||
parsed.scheme,
|
||||
@@ -54,11 +51,9 @@ def build_url(parsed, path, query=""):
|
||||
"",
|
||||
))
|
||||
|
||||
|
||||
def quote_path(*parts):
|
||||
return "/".join(urllib.parse.quote(part, safe="") for part in parts)
|
||||
|
||||
|
||||
def version_list_url(parsed, prefix, owner, package_name, page):
|
||||
path = f"{prefix}/api/v1/packages/{quote_path(owner, 'generic', package_name)}"
|
||||
query = urllib.parse.urlencode({
|
||||
@@ -67,12 +62,10 @@ def version_list_url(parsed, prefix, owner, package_name, page):
|
||||
})
|
||||
return build_url(parsed, path, query)
|
||||
|
||||
|
||||
def version_delete_url(parsed, prefix, owner, package_name, version):
|
||||
path = f"{prefix}/api/packages/{quote_path(owner, 'generic', package_name, version)}"
|
||||
return build_url(parsed, path)
|
||||
|
||||
|
||||
def file_upload_url(registry_url, package_name, version, path):
|
||||
return "/".join((
|
||||
registry_url.rstrip("/"),
|
||||
@@ -81,12 +74,10 @@ def file_upload_url(registry_url, package_name, version, path):
|
||||
urllib.parse.quote(path.name, safe=""),
|
||||
))
|
||||
|
||||
|
||||
def authorization(user, token):
|
||||
encoded = base64.b64encode(f"{user}:{token}".encode("utf-8")).decode("ascii")
|
||||
return f"Basic {encoded}"
|
||||
|
||||
|
||||
def request(method, url, auth, data=None, headers=None, missing_ok=False):
|
||||
request_headers = {
|
||||
"Authorization": auth,
|
||||
@@ -106,7 +97,6 @@ def request(method, url, auth, data=None, headers=None, missing_ok=False):
|
||||
except urllib.error.URLError as error:
|
||||
raise SystemExit(f"{method} {url} failed: {error.reason}") from error
|
||||
|
||||
|
||||
def list_versions(parsed, prefix, owner, package_name, auth):
|
||||
versions = []
|
||||
page = 1
|
||||
@@ -128,7 +118,6 @@ def list_versions(parsed, prefix, owner, package_name, auth):
|
||||
page += 1
|
||||
return sorted(set(versions))
|
||||
|
||||
|
||||
def delete_version(parsed, prefix, owner, package_name, version, auth):
|
||||
url = version_delete_url(parsed, prefix, owner, package_name, version)
|
||||
response = request("DELETE", url, auth, missing_ok=True)
|
||||
@@ -140,7 +129,6 @@ def delete_version(parsed, prefix, owner, package_name, version, auth):
|
||||
raise SystemExit(f"DELETE {url} returned HTTP {response.status}")
|
||||
print(f"Deleted package version: {package_name}/{version}")
|
||||
|
||||
|
||||
def discover_artifacts(dist_dir, package_name, version):
|
||||
archives = sorted(dist_dir.glob(f"{package_name}-{version}-*.tar.gz"))
|
||||
artifacts = []
|
||||
@@ -156,7 +144,6 @@ def discover_artifacts(dist_dir, package_name, version):
|
||||
raise SystemExit("missing artifact files: " + ", ".join(str(path) for path in missing))
|
||||
return artifacts
|
||||
|
||||
|
||||
def upload_file(registry_url, package_name, version, path, auth):
|
||||
url = file_upload_url(registry_url, package_name, version, path)
|
||||
headers = {
|
||||
@@ -170,7 +157,6 @@ def upload_file(registry_url, package_name, version, path, auth):
|
||||
raise SystemExit(f"PUT {url} returned HTTP {response.status}")
|
||||
print(f"Uploaded package file: {package_name}/{version}/{path.name}")
|
||||
|
||||
|
||||
def parse_args(argv):
|
||||
parser = argparse.ArgumentParser(description="Replace Product Vamp's rolling Gitea generic package.")
|
||||
parser.add_argument("--config", type=pathlib.Path, default=pathlib.Path("modulus.json"))
|
||||
@@ -180,7 +166,6 @@ def parse_args(argv):
|
||||
parser.add_argument("--token", default=None)
|
||||
return parser.parse_args(argv)
|
||||
|
||||
|
||||
def main(argv):
|
||||
args = parse_args(argv)
|
||||
package_name, registry_url = read_config(args.config)
|
||||
@@ -189,30 +174,23 @@ def main(argv):
|
||||
auth = authorization(user, token)
|
||||
parsed, prefix, owner = parse_registry_url(registry_url)
|
||||
artifacts = discover_artifacts(args.dist_dir, package_name, args.version)
|
||||
|
||||
existing_versions = list_versions(parsed, prefix, owner, package_name, auth)
|
||||
|
||||
# Gitea rejects duplicate file names within a generic package version, so
|
||||
# replacing the rolling release requires deleting the old version first.
|
||||
if args.version in existing_versions:
|
||||
delete_version(parsed, prefix, owner, package_name, args.version, auth)
|
||||
|
||||
for artifact in artifacts:
|
||||
upload_file(registry_url, package_name, args.version, artifact, auth)
|
||||
|
||||
for version in existing_versions:
|
||||
if version != args.version:
|
||||
delete_version(parsed, prefix, owner, package_name, version, auth)
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
def os_environ(name):
|
||||
value = os.environ.get(name)
|
||||
if not value:
|
||||
raise SystemExit(f"{name} is required")
|
||||
return value
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main(sys.argv[1:]))
|
||||
|
||||
+14
-21
@@ -1,16 +1,17 @@
|
||||
#include <vamp_pico/display.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cinttypes>
|
||||
#include <cstdio>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
|
||||
#include <hardware/gpio.h>
|
||||
#include <hardware/spi.h>
|
||||
#include <pico/stdlib.h>
|
||||
|
||||
#include <vamp_pico/display.hpp>
|
||||
#include <vamp_style/font.hpp>
|
||||
|
||||
#include "hardware/gpio.h"
|
||||
#include "hardware/spi.h"
|
||||
#include "pico/stdlib.h"
|
||||
|
||||
namespace {
|
||||
|
||||
@@ -53,7 +54,6 @@ auto set_window(std::int32_t x0, std::int32_t y0, std::int32_t x1, std::int32_t
|
||||
y0 = std::clamp(y0, std::int32_t{0}, vamp_pico::display_height - std::int32_t{1});
|
||||
x1 = std::clamp(x1, std::int32_t{0}, vamp_pico::display_width - std::int32_t{1});
|
||||
y1 = std::clamp(y1, std::int32_t{0}, vamp_pico::display_height - std::int32_t{1});
|
||||
|
||||
command(0x2a);
|
||||
auto column = std::array<std::uchar_t, 4>{
|
||||
static_cast<std::uchar_t>((x0 >> 8) & 0xff),
|
||||
@@ -62,7 +62,6 @@ auto set_window(std::int32_t x0, std::int32_t y0, std::int32_t x1, std::int32_t
|
||||
static_cast<std::uchar_t>(x1 & 0xff),
|
||||
};
|
||||
data(column.data(), column.size());
|
||||
|
||||
command(0x2b);
|
||||
auto row = std::array<std::uchar_t, 4>{
|
||||
static_cast<std::uchar_t>((y0 >> 8) & 0xff),
|
||||
@@ -140,12 +139,10 @@ auto vamp_pico::display::init() -> void {
|
||||
gpio_set_dir(pin_lcd_bl, GPIO_OUT);
|
||||
deselect();
|
||||
gpio_put(pin_lcd_bl, 1);
|
||||
|
||||
gpio_put(pin_lcd_rst, 0);
|
||||
sleep_ms(50);
|
||||
gpio_put(pin_lcd_rst, 1);
|
||||
sleep_ms(120);
|
||||
|
||||
command(0x01);
|
||||
sleep_ms(150);
|
||||
command(0x11);
|
||||
@@ -210,18 +207,20 @@ auto vamp_pico::display::draw_text(std::int32_t x, std::int32_t y, std::char_t c
|
||||
cursor += vamp_style::ui_font_pair_adjust(previous, text[i]) * scale;
|
||||
}
|
||||
auto glyph = vamp_style::ui_font_glyph(text[i]);
|
||||
for (auto row = 0; row < vamp_style::ui_font_height; ++row) {
|
||||
for (auto row = std::int32_t{}; row < vamp_style::ui_font_height; ++row) {
|
||||
auto col = std::int32_t{};
|
||||
while (col < static_cast<std::int32_t>(glyph.width)) {
|
||||
auto row_offset = static_cast<std::size_t>(row * vamp_style::ui_font_max_width);
|
||||
while (col < static_cast<std::int32_t>(glyph.width) && glyph.alpha[row_offset + static_cast<std::size_t>(col)] == 0U) {
|
||||
while (col < static_cast<std::int32_t>(glyph.width) &&
|
||||
glyph.alpha[row_offset + static_cast<std::size_t>(col)] == 0U) {
|
||||
++col;
|
||||
}
|
||||
auto start = col;
|
||||
auto alpha = start < static_cast<std::int32_t>(glyph.width)
|
||||
? glyph.alpha[row_offset + static_cast<std::size_t>(start)]
|
||||
: std::uint8_t{};
|
||||
while (col < static_cast<std::int32_t>(glyph.width) && glyph.alpha[row_offset + static_cast<std::size_t>(col)] == alpha) {
|
||||
while (col < static_cast<std::int32_t>(glyph.width) &&
|
||||
glyph.alpha[row_offset + static_cast<std::size_t>(col)] == alpha) {
|
||||
++col;
|
||||
}
|
||||
if (col > start) {
|
||||
@@ -255,13 +254,8 @@ auto vamp_pico::display::show_remote(vamp_style::color status, std::char_t const
|
||||
auto const *title_text = title == nullptr ? "" : title;
|
||||
auto const *part_text = part == nullptr ? "" : part;
|
||||
auto status_changed = !remote_valid_ || !same_color(last_status_, status);
|
||||
auto content_changed = !remote_valid_
|
||||
|| last_active_ != active
|
||||
|| !same_text(last_title_, title_text)
|
||||
|| !same_text(last_part_, part_text)
|
||||
|| last_page_ != page
|
||||
|| last_pages_ != pages;
|
||||
|
||||
auto content_changed = !remote_valid_ || last_active_ != active || !same_text(last_title_, title_text) ||
|
||||
!same_text(last_part_, part_text) || last_page_ != page || last_pages_ != pages;
|
||||
if (!remote_valid_) {
|
||||
fill(vamp_style::rgb565(vamp_style::background));
|
||||
}
|
||||
@@ -275,14 +269,13 @@ auto vamp_pico::display::show_remote(vamp_style::color status, std::char_t const
|
||||
draw_centered_text(*this, 118, part_text, vamp_style::rgb565(vamp_style::muted), 1, vamp_pico::display_width - 24);
|
||||
if (pages != 0U) {
|
||||
auto page_text = std::array<std::char_t, 32>{};
|
||||
auto written = std::snprintf(page_text.data(), page_text.size(), "PAGE %lu/%lu", static_cast<unsigned long>(page), static_cast<unsigned long>(pages));
|
||||
auto written = std::snprintf(page_text.data(), page_text.size(), "PAGE %" PRIu32 "/%" PRIu32, page, pages);
|
||||
if (written > 0) {
|
||||
draw_centered_text(*this, 168, page_text.data(), vamp_style::rgb565(vamp_style::muted), 1, vamp_pico::display_width - 24);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
last_status_ = status;
|
||||
last_active_ = active;
|
||||
copy_text(last_title_, title_text);
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
#include <vamp_pico/http_client.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstdio>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
|
||||
#include "lwip/ip_addr.h"
|
||||
#include "lwip/pbuf.h"
|
||||
#include "lwip/tcp.h"
|
||||
#include "pico/cyw43_arch.h"
|
||||
#include "pico/stdlib.h"
|
||||
#include <lwip/ip_addr.h>
|
||||
#include <lwip/pbuf.h>
|
||||
#include <lwip/tcp.h>
|
||||
#include <pico/cyw43_arch.h>
|
||||
#include <pico/stdlib.h>
|
||||
|
||||
#include <vamp_pico/http_client.hpp>
|
||||
|
||||
|
||||
namespace {
|
||||
|
||||
@@ -43,7 +44,6 @@ auto close_request(request_state& state, std::bool_t success) -> err_t {
|
||||
if (state.pcb == nullptr) {
|
||||
return ERR_OK;
|
||||
}
|
||||
|
||||
auto *pcb = state.pcb;
|
||||
state.pcb = nullptr;
|
||||
clear_callbacks(pcb);
|
||||
@@ -95,7 +95,6 @@ auto handle_connected(void* argument, tcp_pcb* pcb, err_t error) -> err_t {
|
||||
auto closed = close_request(*state, false);
|
||||
return closed == ERR_ABRT ? closed : error;
|
||||
}
|
||||
|
||||
tcp_arg(pcb, state);
|
||||
tcp_recv(pcb, handle_receive);
|
||||
auto written = tcp_write(pcb, state->request, static_cast<std::uint16_t>(state->request_size), TCP_WRITE_FLAG_COPY);
|
||||
@@ -126,38 +125,20 @@ auto run_request(std::char_t const* method, std::char_t const* path, std::char_t
|
||||
auto body_size = body == nullptr ? std::size_t{} : std::strlen(body);
|
||||
auto written = std::int32_t{};
|
||||
if (body_size == 0U) {
|
||||
written = std::snprintf(
|
||||
state.request,
|
||||
sizeof(state.request),
|
||||
"%s %s HTTP/1.1\r\nHost: %s:%d\r\nConnection: close\r\n\r\n",
|
||||
method,
|
||||
path,
|
||||
VAMP_ZERO_HOST,
|
||||
VAMP_ZERO_PORT
|
||||
);
|
||||
written = std::snprintf(state.request, sizeof(state.request), "%s %s HTTP/1.1\r\nHost: %s:%d\r\nConnection: close\r\n\r\n", method, path, VAMP_ZERO_HOST, VAMP_ZERO_PORT);
|
||||
} else {
|
||||
written = std::snprintf(
|
||||
state.request,
|
||||
sizeof(state.request),
|
||||
"%s %s HTTP/1.1\r\nHost: %s:%d\r\nContent-Length: %u\r\nContent-Type: application/json\r\nConnection: close\r\n\r\n%s",
|
||||
method,
|
||||
path,
|
||||
VAMP_ZERO_HOST,
|
||||
VAMP_ZERO_PORT,
|
||||
static_cast<unsigned>(body_size),
|
||||
body
|
||||
);
|
||||
written = std::snprintf(state.request, sizeof(state.request), "%s %s HTTP/1.1\r\nHost: %s:%d\r\nContent-Length: %zu\r\nContent-Type: application/json\r\nConnection: "
|
||||
"close\r\n\r\n%s",
|
||||
method, path, VAMP_ZERO_HOST, VAMP_ZERO_PORT, body_size, body);
|
||||
}
|
||||
if (written <= 0 || static_cast<std::size_t>(written) >= sizeof(state.request)) {
|
||||
return false;
|
||||
}
|
||||
state.request_size = static_cast<std::size_t>(written);
|
||||
|
||||
auto address = ip_addr_t{};
|
||||
if (ipaddr_aton(VAMP_ZERO_HOST, &address) == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
cyw43_arch_lwip_begin();
|
||||
state.pcb = tcp_new_ip_type(IPADDR_TYPE_V4);
|
||||
if (state.pcb == nullptr) {
|
||||
@@ -174,7 +155,6 @@ auto run_request(std::char_t const* method, std::char_t const* path, std::char_t
|
||||
if (connected != ERR_OK) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto deadline = make_timeout_time_ms(timeout_ms);
|
||||
while (!state.done && !time_reached(deadline)) {
|
||||
sleep_ms(5);
|
||||
|
||||
+4
-12
@@ -1,10 +1,11 @@
|
||||
#include <pico/cyw43_arch.h>
|
||||
#include <pico/stdlib.h>
|
||||
|
||||
#include <vamp_pico/display.hpp>
|
||||
#include <vamp_pico/http_client.hpp>
|
||||
#include <vamp_pico/touch.hpp>
|
||||
#include <vamp_protocol/page_control.hpp>
|
||||
|
||||
#include "pico/cyw43_arch.h"
|
||||
#include "pico/stdlib.h"
|
||||
|
||||
namespace {
|
||||
|
||||
@@ -74,19 +75,16 @@ auto update_gesture(gesture_state& gesture, std::bool_t touching, link_status& s
|
||||
constexpr auto hold_us = std::int64_t{700000};
|
||||
constexpr auto double_tap_us = std::int64_t{220000};
|
||||
auto now = get_absolute_time();
|
||||
|
||||
if (touching && !gesture.touching) {
|
||||
gesture.touching = true;
|
||||
gesture.hold_sent = false;
|
||||
gesture.down_time = now;
|
||||
}
|
||||
|
||||
if (touching && !gesture.hold_sent && absolute_time_diff_us(gesture.down_time, now) >= hold_us) {
|
||||
gesture.hold_sent = true;
|
||||
gesture.pending_tap = false;
|
||||
send_command(vamp_protocol::page_command::SELECT, status, remote, display);
|
||||
}
|
||||
|
||||
if (!touching && gesture.touching) {
|
||||
gesture.touching = false;
|
||||
if (!gesture.hold_sent) {
|
||||
@@ -99,7 +97,6 @@ auto update_gesture(gesture_state& gesture, std::bool_t touching, link_status& s
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!touching && gesture.pending_tap && absolute_time_diff_us(gesture.last_up_time, now) > double_tap_us) {
|
||||
gesture.pending_tap = false;
|
||||
send_command(vamp_protocol::page_command::NEXT, status, remote, display);
|
||||
@@ -110,29 +107,24 @@ auto update_gesture(gesture_state& gesture, std::bool_t touching, link_status& s
|
||||
|
||||
auto main() -> int {
|
||||
stdio_init_all();
|
||||
|
||||
auto display = vamp_pico::display{};
|
||||
auto touch = vamp_pico::touch_controller{};
|
||||
display.init();
|
||||
touch.init();
|
||||
|
||||
if (!connect_wifi(display)) {
|
||||
while (true) {
|
||||
sleep_ms(1000);
|
||||
}
|
||||
}
|
||||
|
||||
auto remote = vamp_pico::remote_display_state{};
|
||||
auto status = link_status::RETRYING;
|
||||
auto gesture = gesture_state{};
|
||||
auto last_poll = get_absolute_time();
|
||||
auto poll_now = true;
|
||||
auto poll_now = std::bool_t{true};
|
||||
render(display, status, remote);
|
||||
|
||||
while (true) {
|
||||
auto point = vamp_pico::touch_point{};
|
||||
update_gesture(gesture, touch.read(point), status, remote, display);
|
||||
|
||||
auto now = get_absolute_time();
|
||||
if (poll_now || absolute_time_diff_us(last_poll, now) >= 1000000) {
|
||||
poll_now = false;
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
#include <vamp_pico/http_client.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <charconv>
|
||||
#include <cstring>
|
||||
#include <limits>
|
||||
#include <system_error>
|
||||
|
||||
#include <vamp_pico/http_client.hpp>
|
||||
|
||||
|
||||
namespace {
|
||||
|
||||
auto copy_value(std::string_view value, std::char_t *output, std::size_t capacity) -> void {
|
||||
|
||||
+8
-10
@@ -1,13 +1,12 @@
|
||||
#include <vamp_pico/touch.hpp>
|
||||
|
||||
#include <vamp_pico/display.hpp>
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
|
||||
#include "hardware/gpio.h"
|
||||
#include "hardware/i2c.h"
|
||||
#include "pico/stdlib.h"
|
||||
#include <hardware/gpio.h>
|
||||
#include <hardware/i2c.h>
|
||||
|
||||
#include <vamp_pico/display.hpp>
|
||||
#include <vamp_pico/touch.hpp>
|
||||
|
||||
|
||||
namespace {
|
||||
|
||||
@@ -25,14 +24,12 @@ auto vamp_pico::touch_controller::init() -> void {
|
||||
gpio_set_function(pin_tp_scl, GPIO_FUNC_I2C);
|
||||
gpio_pull_up(pin_tp_sda);
|
||||
gpio_pull_up(pin_tp_scl);
|
||||
|
||||
gpio_init(pin_tp_rst);
|
||||
gpio_set_dir(pin_tp_rst, GPIO_OUT);
|
||||
gpio_put(pin_tp_rst, 0);
|
||||
sleep_ms(10);
|
||||
gpio_put(pin_tp_rst, 1);
|
||||
sleep_ms(50);
|
||||
|
||||
gpio_init(pin_tp_int);
|
||||
gpio_set_dir(pin_tp_int, GPIO_IN);
|
||||
gpio_pull_up(pin_tp_int);
|
||||
@@ -44,7 +41,8 @@ auto vamp_pico::touch_controller::read(vamp_pico::touch_point& point) -> std::bo
|
||||
if (i2c_write_blocking(i2c1, cst816_address, ®, 1, true) != 1) {
|
||||
return false;
|
||||
}
|
||||
if (i2c_read_blocking(i2c1, cst816_address, data.data(), data.size(), false) != static_cast<int>(data.size())) {
|
||||
if (i2c_read_blocking(i2c1, cst816_address, data.data(), data.size(), false) !=
|
||||
static_cast<std::int32_t>(data.size())) {
|
||||
return false;
|
||||
}
|
||||
auto fingers = data[0] & 0x0f;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include <vamp_protocol/page_control.hpp>
|
||||
|
||||
|
||||
auto vamp_protocol::page_command_path(vamp_protocol::page_command command) -> std::char_t const * {
|
||||
switch (command) {
|
||||
case vamp_protocol::page_command::PREVIOUS:
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
|
||||
auto vamp_style::rgb565(vamp_style::color value) -> std::uint16_t {
|
||||
auto r = static_cast<std::uint16_t>((value.r >> 3U) & 0x1fU);
|
||||
auto g = static_cast<std::uint16_t>((value.g >> 2U) & 0x3fU);
|
||||
|
||||
+16
-16
@@ -1,5 +1,3 @@
|
||||
#include <vamp_zero/app.hpp>
|
||||
|
||||
#include <csignal>
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
@@ -26,6 +24,7 @@
|
||||
#include <ligo.hpp>
|
||||
#include <modulus.hpp>
|
||||
|
||||
#include <vamp_zero/app.hpp>
|
||||
#include <vamp_zero/config.hpp>
|
||||
#include <vamp_zero/audio.hpp>
|
||||
#include <vamp_zero/database.hpp>
|
||||
@@ -35,6 +34,7 @@
|
||||
#include <vamp_zero/portal.hpp>
|
||||
#include <vamp_zero/ui.hpp>
|
||||
|
||||
|
||||
namespace {
|
||||
|
||||
auto invalid(std::string message) -> modulus::error {
|
||||
@@ -130,7 +130,8 @@ auto load_initial_state(vamp_zero::config const& config) -> std::expected<vamp_z
|
||||
auto state = vamp_zero::app_state{
|
||||
.performance = std::move(*performance),
|
||||
.practice = std::move(*practice),
|
||||
.audio = {
|
||||
.audio =
|
||||
{
|
||||
.track_enabled = config.audio.track_enabled,
|
||||
.metronome_enabled = config.audio.metronome_enabled,
|
||||
.metronome_bpm = config.audio.metronome_bpm,
|
||||
@@ -151,15 +152,14 @@ auto run_headless(vamp_zero::config const& config, vamp_zero::control_state& sta
|
||||
if (!endpoint) {
|
||||
return std::unexpected(endpoint.error());
|
||||
}
|
||||
auto server = ligo::http::server{io, *endpoint, vamp_zero::make_router(state), ligo::http::server_options_from_config(config.control.endpoint)};
|
||||
auto server = ligo::http::server{
|
||||
io, *endpoint, vamp_zero::make_router(state), ligo::http::server_options_from_config(config.control.endpoint)};
|
||||
auto started = server.start();
|
||||
if (!started) {
|
||||
return std::unexpected(started.error());
|
||||
}
|
||||
if (audio != nullptr) {
|
||||
state.set_observer([audio](vamp_zero::app_state app) {
|
||||
audio->sync(app);
|
||||
});
|
||||
state.set_observer([audio](vamp_zero::app_state app) { audio->sync(app); });
|
||||
audio->sync(state.snapshot());
|
||||
}
|
||||
auto heartbeat = write_heartbeat(config);
|
||||
@@ -168,7 +168,7 @@ auto run_headless(vamp_zero::config const& config, vamp_zero::control_state& sta
|
||||
return std::unexpected(heartbeat.error());
|
||||
}
|
||||
auto signals = boost::asio::signal_set{io, SIGINT, SIGTERM};
|
||||
signals.async_wait([&](auto const&, int) {
|
||||
signals.async_wait([&](auto const &, std::int32_t) {
|
||||
server.stop();
|
||||
io.stop();
|
||||
});
|
||||
@@ -202,7 +202,8 @@ auto run_display(vamp_zero::config const& config, vamp_zero::control_state& stat
|
||||
if (!published) {
|
||||
return std::unexpected(published.error());
|
||||
}
|
||||
auto server = ligo::http::server{io, *endpoint, vamp_zero::make_router(state), ligo::http::server_options_from_config(config.control.endpoint)};
|
||||
auto server = ligo::http::server{
|
||||
io, *endpoint, vamp_zero::make_router(state), ligo::http::server_options_from_config(config.control.endpoint)};
|
||||
auto started = server.start();
|
||||
if (!started) {
|
||||
return std::unexpected(started.error());
|
||||
@@ -226,7 +227,7 @@ auto run_display(vamp_zero::config const& config, vamp_zero::control_state& stat
|
||||
};
|
||||
schedule_connection_refresh();
|
||||
auto signals = boost::asio::signal_set{io, SIGINT, SIGTERM};
|
||||
signals.async_wait([&](auto const&, int) {
|
||||
signals.async_wait([&](auto const &, std::int32_t) {
|
||||
connection_timer.cancel();
|
||||
server.stop();
|
||||
ui.stop();
|
||||
@@ -287,11 +288,11 @@ auto vamp_zero::parse_command_line(std::int32_t argc, std::char_t** argv) -> std
|
||||
return std::unexpected(invalid("unknown argument: " + std::string{argument}));
|
||||
}
|
||||
}
|
||||
auto command_count = static_cast<std::uint32_t>(result.check_config ? 1 : 0)
|
||||
+ static_cast<std::uint32_t>(result.health_check ? 1 : 0)
|
||||
+ static_cast<std::uint32_t>(result.print_version ? 1 : 0)
|
||||
+ static_cast<std::uint32_t>(result.exit_after_startup ? 1 : 0)
|
||||
+ static_cast<std::uint32_t>(result.ensure_database ? 1 : 0);
|
||||
auto command_count = static_cast<std::uint32_t>(result.check_config ? 1 : 0) +
|
||||
static_cast<std::uint32_t>(result.health_check ? 1 : 0) +
|
||||
static_cast<std::uint32_t>(result.print_version ? 1 : 0) +
|
||||
static_cast<std::uint32_t>(result.exit_after_startup ? 1 : 0) +
|
||||
static_cast<std::uint32_t>(result.ensure_database ? 1 : 0);
|
||||
if (command_count > 1U) {
|
||||
return std::unexpected(invalid("only one command flag may be selected"));
|
||||
}
|
||||
@@ -323,7 +324,6 @@ auto vamp_zero::run(vamp_zero::command_line command) -> std::expected<void, modu
|
||||
std::cout << "ok\n";
|
||||
return {};
|
||||
}
|
||||
|
||||
auto ensured = ensure_database(*config);
|
||||
if (!ensured) {
|
||||
return ensured;
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#include <vamp_zero/audio.hpp>
|
||||
|
||||
#include <expected>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
@@ -9,8 +7,10 @@
|
||||
#include <modulus.hpp>
|
||||
#include <sonus.hpp>
|
||||
|
||||
#include <vamp_zero/audio.hpp>
|
||||
#include <vamp_zero/navigation.hpp>
|
||||
|
||||
|
||||
namespace {
|
||||
|
||||
auto score_active(vamp_zero::app_state const &state) -> std::bool_t {
|
||||
@@ -58,7 +58,6 @@ auto vamp_zero::audio_controller::sync(vamp_zero::app_state const& state) -> voi
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
auto metronome = make_metronome_config(state);
|
||||
if (!metronome_running_) {
|
||||
auto started = engine_.start_metronome(metronome);
|
||||
@@ -72,7 +71,6 @@ auto vamp_zero::audio_controller::sync(vamp_zero::app_state const& state) -> voi
|
||||
last_metronome_bpm_ = metronome.bpm;
|
||||
return;
|
||||
}
|
||||
|
||||
if (last_metronome_bpm_ != metronome.bpm) {
|
||||
auto updated = engine_.update_metronome(metronome);
|
||||
if (!updated) {
|
||||
@@ -85,16 +83,15 @@ auto vamp_zero::audio_controller::sync(vamp_zero::app_state const& state) -> voi
|
||||
}
|
||||
}
|
||||
|
||||
auto vamp_zero::create_audio_controller(vamp_zero::audio_config const& config) -> std::expected<std::unique_ptr<vamp_zero::audio_controller>, modulus::error> {
|
||||
auto vamp_zero::create_audio_controller(vamp_zero::audio_config const &config)
|
||||
-> std::expected<std::unique_ptr<vamp_zero::audio_controller>, modulus::error> {
|
||||
if (!config.enabled) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto sonus_config = sonus::audio_config{};
|
||||
sonus_config.sample_rate = 48000;
|
||||
sonus_config.channels = 2;
|
||||
sonus_config.master_volume = config.master_volume;
|
||||
|
||||
auto engine = sonus::engine::create(sonus_config);
|
||||
if (!engine) {
|
||||
return std::unexpected(engine.error());
|
||||
@@ -103,6 +100,5 @@ auto vamp_zero::create_audio_controller(vamp_zero::audio_config const& config) -
|
||||
if (!started) {
|
||||
return std::unexpected(started.error());
|
||||
}
|
||||
|
||||
return std::make_unique<vamp_zero::audio_controller>(config, std::move(*engine));
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include <vamp_zero/color.hpp>
|
||||
|
||||
|
||||
auto vamp_zero::to_luma(vamp_style::color value) -> luma::color {
|
||||
return {
|
||||
.r = value.r,
|
||||
|
||||
+70
-20
@@ -1,10 +1,6 @@
|
||||
#include <vamp_zero/config.hpp>
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <expected>
|
||||
#include <fstream>
|
||||
#include <iterator>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
@@ -14,8 +10,10 @@
|
||||
#include <boost/json/value.hpp>
|
||||
#include <boost/system/error_code.hpp>
|
||||
|
||||
#include <vamp_zero/config.hpp>
|
||||
#include <vamp_zero/errors.hpp>
|
||||
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr auto json_options = modulus::json_config::options{
|
||||
@@ -136,8 +134,20 @@ auto parse_runtime(boost::json::object const& root, vamp_zero::runtime_config fa
|
||||
auto exit_after_startup = optional_bool(**object, "exit_after_startup", fallback.exit_after_startup);
|
||||
auto assets_path = optional_string(**object, "assets_path", fallback.assets_path);
|
||||
auto heartbeat_path = optional_string(**object, "heartbeat_path", fallback.heartbeat_path);
|
||||
if (!hardware_text || !headless || !exit_after_startup || !assets_path || !heartbeat_path) {
|
||||
return std::unexpected(!hardware_text ? hardware_text.error() : (!headless ? headless.error() : (!exit_after_startup ? exit_after_startup.error() : (!assets_path ? assets_path.error() : heartbeat_path.error()))));
|
||||
if (!hardware_text) {
|
||||
return std::unexpected(hardware_text.error());
|
||||
}
|
||||
if (!headless) {
|
||||
return std::unexpected(headless.error());
|
||||
}
|
||||
if (!exit_after_startup) {
|
||||
return std::unexpected(exit_after_startup.error());
|
||||
}
|
||||
if (!assets_path) {
|
||||
return std::unexpected(assets_path.error());
|
||||
}
|
||||
if (!heartbeat_path) {
|
||||
return std::unexpected(heartbeat_path.error());
|
||||
}
|
||||
auto hardware = parse_hardware(*hardware_text);
|
||||
if (!hardware) {
|
||||
@@ -193,8 +203,20 @@ auto parse_control(boost::json::object const& root, vamp_zero::control_config fa
|
||||
auto ap_interface = optional_string(**object, "ap_interface", result.ap_interface);
|
||||
auto zero_ip = optional_string(**object, "zero_ip", result.zero_ip);
|
||||
auto zero_cidr = optional_string(**object, "zero_cidr", result.zero_cidr);
|
||||
if (!ap_ssid || !ap_passphrase || !ap_interface || !zero_ip || !zero_cidr) {
|
||||
return std::unexpected(!ap_ssid ? ap_ssid.error() : (!ap_passphrase ? ap_passphrase.error() : (!ap_interface ? ap_interface.error() : (!zero_ip ? zero_ip.error() : zero_cidr.error()))));
|
||||
if (!ap_ssid) {
|
||||
return std::unexpected(ap_ssid.error());
|
||||
}
|
||||
if (!ap_passphrase) {
|
||||
return std::unexpected(ap_passphrase.error());
|
||||
}
|
||||
if (!ap_interface) {
|
||||
return std::unexpected(ap_interface.error());
|
||||
}
|
||||
if (!zero_ip) {
|
||||
return std::unexpected(zero_ip.error());
|
||||
}
|
||||
if (!zero_cidr) {
|
||||
return std::unexpected(zero_cidr.error());
|
||||
}
|
||||
if (ap_passphrase->size() < 8U) {
|
||||
return std::unexpected(invalid("control.ap_passphrase must be at least 8 characters"));
|
||||
@@ -225,16 +247,35 @@ auto parse_audio(boost::json::object const& root, vamp_zero::audio_config fallba
|
||||
auto beats_per_bar = optional_uint32(**object, "beats_per_bar", fallback.beats_per_bar, 1U, 16U);
|
||||
auto master_volume = optional_float(**object, "master_volume", fallback.master_volume, 0.0F, 1.0F);
|
||||
auto metronome_volume = optional_float(**object, "metronome_volume", fallback.metronome_volume, 0.0F, 1.0F);
|
||||
if (!enabled || !track_enabled || !metronome_enabled || !metronome_bpm || !metronome_min_bpm || !metronome_max_bpm || !metronome_step || !beats_per_bar || !master_volume || !metronome_volume) {
|
||||
return std::unexpected(!enabled ? enabled.error()
|
||||
: (!track_enabled ? track_enabled.error()
|
||||
: (!metronome_enabled ? metronome_enabled.error()
|
||||
: (!metronome_bpm ? metronome_bpm.error()
|
||||
: (!metronome_min_bpm ? metronome_min_bpm.error()
|
||||
: (!metronome_max_bpm ? metronome_max_bpm.error()
|
||||
: (!metronome_step ? metronome_step.error()
|
||||
: (!beats_per_bar ? beats_per_bar.error()
|
||||
: (!master_volume ? master_volume.error() : metronome_volume.error())))))))));
|
||||
if (!enabled) {
|
||||
return std::unexpected(enabled.error());
|
||||
}
|
||||
if (!track_enabled) {
|
||||
return std::unexpected(track_enabled.error());
|
||||
}
|
||||
if (!metronome_enabled) {
|
||||
return std::unexpected(metronome_enabled.error());
|
||||
}
|
||||
if (!metronome_bpm) {
|
||||
return std::unexpected(metronome_bpm.error());
|
||||
}
|
||||
if (!metronome_min_bpm) {
|
||||
return std::unexpected(metronome_min_bpm.error());
|
||||
}
|
||||
if (!metronome_max_bpm) {
|
||||
return std::unexpected(metronome_max_bpm.error());
|
||||
}
|
||||
if (!metronome_step) {
|
||||
return std::unexpected(metronome_step.error());
|
||||
}
|
||||
if (!beats_per_bar) {
|
||||
return std::unexpected(beats_per_bar.error());
|
||||
}
|
||||
if (!master_volume) {
|
||||
return std::unexpected(master_volume.error());
|
||||
}
|
||||
if (!metronome_volume) {
|
||||
return std::unexpected(metronome_volume.error());
|
||||
}
|
||||
if (*metronome_min_bpm > *metronome_max_bpm) {
|
||||
return std::unexpected(invalid("audio.metronome_min_bpm must be less than or equal to audio.metronome_max_bpm"));
|
||||
@@ -275,8 +316,17 @@ auto vamp_zero::parse_config(boost::json::value const& value) -> std::expected<v
|
||||
auto database = parse_database(**root, result.database);
|
||||
auto control = parse_control(**root, result.control);
|
||||
auto audio = parse_audio(**root, result.audio);
|
||||
if (!runtime || !database || !control || !audio) {
|
||||
return std::unexpected(!runtime ? runtime.error() : (!database ? database.error() : (!control ? control.error() : audio.error())));
|
||||
if (!runtime) {
|
||||
return std::unexpected(runtime.error());
|
||||
}
|
||||
if (!database) {
|
||||
return std::unexpected(database.error());
|
||||
}
|
||||
if (!control) {
|
||||
return std::unexpected(control.error());
|
||||
}
|
||||
if (!audio) {
|
||||
return std::unexpected(audio.error());
|
||||
}
|
||||
result.runtime = std::move(*runtime);
|
||||
result.database = std::move(*database);
|
||||
|
||||
+30
-24
@@ -1,6 +1,3 @@
|
||||
#include <vamp_zero/database.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <expected>
|
||||
@@ -16,16 +13,19 @@
|
||||
|
||||
#include <noto.hpp>
|
||||
|
||||
#include <vamp_zero/database.hpp>
|
||||
#include <vamp_zero/errors.hpp>
|
||||
#include <vamp_zero/library.hpp>
|
||||
#include <vamp_zero/score.hpp>
|
||||
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr auto application_id = std::int64_t{0x56414D50};
|
||||
|
||||
auto connection_config(vamp_zero::database_config const &config) -> noto::connection_config {
|
||||
auto result = config.path == ":memory:" ? noto::memory_connection_config("product-vamp") : noto::file_connection_config(config.path, "product-vamp");
|
||||
auto result = config.path == ":memory:" ? noto::memory_connection_config("product-vamp")
|
||||
: noto::file_connection_config(config.path, "product-vamp");
|
||||
result.busy_timeout = std::chrono::seconds{5};
|
||||
result.foreign_keys = true;
|
||||
result.pragmas = {
|
||||
@@ -77,7 +77,6 @@ auto ensure_current_schema(noto::connection& connection) -> boost::asio::awaitab
|
||||
if (!begin) {
|
||||
co_return std::unexpected(begin.error());
|
||||
}
|
||||
|
||||
auto ensured = std::expected<void, modulus::error>{};
|
||||
auto schema_statements = std::vector<std::string>{
|
||||
R"sql(
|
||||
@@ -121,16 +120,15 @@ CREATE TABLE IF NOT EXISTS practice_exercises (
|
||||
)sql",
|
||||
"DROP TABLE IF EXISTS _noto_schema_migrations",
|
||||
};
|
||||
|
||||
for (auto &statement : schema_statements) {
|
||||
if (!ensured) {
|
||||
break;
|
||||
}
|
||||
ensured = co_await execute_schema_statement(connection, std::move(statement));
|
||||
}
|
||||
|
||||
if (ensured) {
|
||||
ensured = co_await ensure_column(connection, "songs", "part", "ALTER TABLE songs ADD COLUMN part TEXT NOT NULL DEFAULT ''");
|
||||
ensured =
|
||||
co_await ensure_column(connection, "songs", "part", "ALTER TABLE songs ADD COLUMN part TEXT NOT NULL DEFAULT ''");
|
||||
}
|
||||
if (ensured) {
|
||||
ensured = co_await ensure_column(connection, "songs", "source_path", "ALTER TABLE songs ADD COLUMN source_path TEXT NOT NULL DEFAULT ''");
|
||||
@@ -142,30 +140,35 @@ CREATE TABLE IF NOT EXISTS practice_exercises (
|
||||
ensured = co_await ensure_column(connection, "songs", "catalog_managed", "ALTER TABLE songs ADD COLUMN catalog_managed INTEGER NOT NULL DEFAULT 1 CHECK(catalog_managed IN (0, 1))");
|
||||
}
|
||||
if (ensured) {
|
||||
ensured = co_await ensure_column(connection, "practice_exercises", "practice_type_id", "ALTER TABLE practice_exercises ADD COLUMN practice_type_id TEXT NOT NULL DEFAULT 'general' CHECK(length(trim(practice_type_id)) BETWEEN 1 AND 120)");
|
||||
ensured = co_await ensure_column(connection, "practice_exercises", "practice_type_id", "ALTER TABLE practice_exercises ADD COLUMN practice_type_id TEXT NOT NULL DEFAULT 'general' "
|
||||
"CHECK(length(trim(practice_type_id)) BETWEEN 1 AND 120)");
|
||||
}
|
||||
if (ensured) {
|
||||
ensured = co_await ensure_column(connection, "practice_exercises", "practice_type_title", "ALTER TABLE practice_exercises ADD COLUMN practice_type_title TEXT NOT NULL DEFAULT 'General Practice' CHECK(length(trim(practice_type_title)) BETWEEN 1 AND 200)");
|
||||
ensured = co_await ensure_column(connection, "practice_exercises", "practice_type_title", "ALTER TABLE practice_exercises ADD COLUMN practice_type_title TEXT NOT NULL DEFAULT 'General Practice' "
|
||||
"CHECK(length(trim(practice_type_title)) BETWEEN 1 AND 200)");
|
||||
}
|
||||
if (ensured) {
|
||||
ensured = co_await ensure_column(connection, "practice_exercises", "lesson_group_id", "ALTER TABLE practice_exercises ADD COLUMN lesson_group_id TEXT NOT NULL DEFAULT 'general' CHECK(length(trim(lesson_group_id)) BETWEEN 1 AND 120)");
|
||||
ensured = co_await ensure_column(connection, "practice_exercises", "lesson_group_id", "ALTER TABLE practice_exercises ADD COLUMN lesson_group_id TEXT NOT NULL DEFAULT 'general' "
|
||||
"CHECK(length(trim(lesson_group_id)) BETWEEN 1 AND 120)");
|
||||
}
|
||||
if (ensured) {
|
||||
ensured = co_await ensure_column(connection, "practice_exercises", "lesson_group_title", "ALTER TABLE practice_exercises ADD COLUMN lesson_group_title TEXT NOT NULL DEFAULT 'General' CHECK(length(trim(lesson_group_title)) BETWEEN 1 AND 200)");
|
||||
ensured = co_await ensure_column(connection, "practice_exercises", "lesson_group_title", "ALTER TABLE practice_exercises ADD COLUMN lesson_group_title TEXT NOT NULL DEFAULT 'General' "
|
||||
"CHECK(length(trim(lesson_group_title)) BETWEEN 1 AND 200)");
|
||||
}
|
||||
if (ensured) {
|
||||
ensured = co_await ensure_column(connection, "practice_exercises", "source_path", "ALTER TABLE practice_exercises ADD COLUMN source_path TEXT NOT NULL DEFAULT ''");
|
||||
}
|
||||
if (ensured) {
|
||||
ensured = co_await ensure_column(connection, "practice_exercises", "catalog_managed", "ALTER TABLE practice_exercises ADD COLUMN catalog_managed INTEGER NOT NULL DEFAULT 1 CHECK(catalog_managed IN (0, 1))");
|
||||
ensured = co_await ensure_column(connection, "practice_exercises", "catalog_managed", "ALTER TABLE practice_exercises ADD COLUMN catalog_managed INTEGER NOT NULL DEFAULT 1 CHECK(catalog_managed IN "
|
||||
"(0, 1))");
|
||||
}
|
||||
|
||||
auto index_statements = std::vector<std::string>{
|
||||
"CREATE INDEX IF NOT EXISTS idx_songs_title ON songs(title COLLATE NOCASE)",
|
||||
"CREATE INDEX IF NOT EXISTS idx_songs_title_part ON songs(title COLLATE NOCASE, part COLLATE NOCASE)",
|
||||
"CREATE INDEX IF NOT EXISTS idx_songs_catalog_managed ON songs(catalog_managed)",
|
||||
"CREATE INDEX IF NOT EXISTS idx_practice_exercises_title ON practice_exercises(title COLLATE NOCASE)",
|
||||
"CREATE INDEX IF NOT EXISTS idx_practice_exercises_hierarchy ON practice_exercises(practice_type_id COLLATE NOCASE, lesson_group_id COLLATE NOCASE, title COLLATE NOCASE)",
|
||||
"CREATE INDEX IF NOT EXISTS idx_practice_exercises_hierarchy ON practice_exercises(practice_type_id COLLATE "
|
||||
"NOCASE, lesson_group_id COLLATE NOCASE, title COLLATE NOCASE)",
|
||||
"CREATE INDEX IF NOT EXISTS idx_practice_exercises_catalog_managed ON practice_exercises(catalog_managed)",
|
||||
};
|
||||
for (auto &statement : index_statements) {
|
||||
@@ -174,7 +177,6 @@ CREATE TABLE IF NOT EXISTS practice_exercises (
|
||||
}
|
||||
ensured = co_await execute_schema_statement(connection, std::move(statement));
|
||||
}
|
||||
|
||||
if (ensured) {
|
||||
auto committed = co_await connection.async_execute("COMMIT");
|
||||
if (!committed) {
|
||||
@@ -183,7 +185,6 @@ CREATE TABLE IF NOT EXISTS practice_exercises (
|
||||
} else {
|
||||
static_cast<void>(co_await connection.async_execute("ROLLBACK"));
|
||||
}
|
||||
|
||||
if (!ensured) {
|
||||
co_return std::unexpected(ensured.error());
|
||||
}
|
||||
@@ -217,7 +218,8 @@ auto logical_practice_exercise_id(std::string const& type_id, std::string const&
|
||||
}
|
||||
|
||||
auto now_ms() -> std::int64_t {
|
||||
return std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch()).count();
|
||||
return std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch())
|
||||
.count();
|
||||
}
|
||||
|
||||
auto execute_delete_stale_catalog_rows(noto::connection &connection, std::string_view table, std::vector<std::string> const &ids) -> boost::asio::awaitable<std::expected<void, modulus::error>> {
|
||||
@@ -261,7 +263,6 @@ auto vamp_zero::async_ensure_database(boost::asio::any_io_executor executor, vam
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto connection = noto::connection{executor, connection_config(config)};
|
||||
auto connected = co_await connection.async_connect();
|
||||
if (!connected) {
|
||||
@@ -290,7 +291,6 @@ auto vamp_zero::async_ensure_database(boost::asio::any_io_executor executor, vam
|
||||
co_return std::unexpected(set.error());
|
||||
}
|
||||
}
|
||||
|
||||
auto ensured = co_await ensure_current_schema(connection);
|
||||
if (!ensured) {
|
||||
auto close = co_await connection.async_close();
|
||||
@@ -457,13 +457,16 @@ WHERE practice_exercises.catalog_managed = 1
|
||||
co_return co_await connection.async_close();
|
||||
}
|
||||
|
||||
auto vamp_zero::async_load_performance_library(boost::asio::any_io_executor executor, vamp_zero::database_config config) -> boost::asio::awaitable<std::expected<vamp_zero::library_state, modulus::error>> {
|
||||
auto vamp_zero::async_load_performance_library(boost::asio::any_io_executor executor, vamp_zero::database_config config)
|
||||
-> boost::asio::awaitable<std::expected<vamp_zero::library_state, modulus::error>> {
|
||||
auto connection = noto::connection{executor, connection_config(config)};
|
||||
auto connected = co_await connection.async_connect();
|
||||
if (!connected) {
|
||||
co_return std::unexpected(connected.error());
|
||||
}
|
||||
auto selected = co_await connection.async_execute("SELECT id, title, composer, part, source_path, source_json, key_signature FROM songs ORDER BY title COLLATE NOCASE, part COLLATE NOCASE");
|
||||
auto selected =
|
||||
co_await connection.async_execute("SELECT id, title, composer, part, source_path, source_json, key_signature FROM "
|
||||
"songs ORDER BY title COLLATE NOCASE, part COLLATE NOCASE");
|
||||
if (!selected) {
|
||||
auto close = co_await connection.async_close();
|
||||
if (!close) {
|
||||
@@ -504,13 +507,16 @@ auto vamp_zero::async_load_performance_library(boost::asio::any_io_executor exec
|
||||
co_return vamp_zero::group_library_parts(std::move(parts));
|
||||
}
|
||||
|
||||
auto vamp_zero::async_load_practice_library(boost::asio::any_io_executor executor, vamp_zero::database_config config) -> boost::asio::awaitable<std::expected<vamp_zero::practice_library, modulus::error>> {
|
||||
auto vamp_zero::async_load_practice_library(boost::asio::any_io_executor executor, vamp_zero::database_config config)
|
||||
-> boost::asio::awaitable<std::expected<vamp_zero::practice_library, modulus::error>> {
|
||||
auto connection = noto::connection{executor, connection_config(config)};
|
||||
auto connected = co_await connection.async_connect();
|
||||
if (!connected) {
|
||||
co_return std::unexpected(connected.error());
|
||||
}
|
||||
auto selected = co_await connection.async_execute("SELECT id, practice_type_id, practice_type_title, lesson_group_id, lesson_group_title, title, composer, key_signature, source_path, source_json FROM practice_exercises ORDER BY practice_type_id COLLATE NOCASE, lesson_group_id COLLATE NOCASE, source_path COLLATE NOCASE, title COLLATE NOCASE, id COLLATE NOCASE");
|
||||
auto selected = co_await connection.async_execute("SELECT id, practice_type_id, practice_type_title, lesson_group_id, lesson_group_title, title, composer, "
|
||||
"key_signature, source_path, source_json FROM practice_exercises ORDER BY practice_type_id COLLATE NOCASE, "
|
||||
"lesson_group_id COLLATE NOCASE, source_path COLLATE NOCASE, title COLLATE NOCASE, id COLLATE NOCASE");
|
||||
if (!selected) {
|
||||
auto close = co_await connection.async_close();
|
||||
if (!close) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include <utility>
|
||||
|
||||
#include <vamp_zero/errors.hpp>
|
||||
|
||||
#include <utility>
|
||||
|
||||
auto vamp_zero::make_error(vamp_zero::error_code code, std::string message, modulus::source_location location) -> modulus::error {
|
||||
return modulus::make_error("product-vamp", static_cast<std::int32_t>(code), std::move(message), location);
|
||||
|
||||
+41
-44
@@ -1,27 +1,20 @@
|
||||
#include <vamp_zero/library.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <expected>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iterator>
|
||||
#include <limits>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <system_error>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include <boost/json/array.hpp>
|
||||
#include <boost/json/object.hpp>
|
||||
#include <boost/json/parse.hpp>
|
||||
#include <boost/json/value.hpp>
|
||||
#include <boost/system/error_code.hpp>
|
||||
|
||||
#include <vamp_zero/errors.hpp>
|
||||
#include <vamp_zero/score.hpp>
|
||||
#include <vamp_zero/library.hpp>
|
||||
#include <vamp_zero/notation.hpp>
|
||||
|
||||
|
||||
namespace {
|
||||
|
||||
auto invalid(std::string message) -> modulus::error {
|
||||
@@ -32,7 +25,7 @@ auto human_label(std::string value) -> std::string {
|
||||
if (value.empty()) {
|
||||
return "Score";
|
||||
}
|
||||
auto capitalize_next = true;
|
||||
auto capitalize_next = std::bool_t{true};
|
||||
for (auto &ch : value) {
|
||||
if (ch == '-' || ch == '_') {
|
||||
ch = ' ';
|
||||
@@ -53,25 +46,32 @@ auto practice_type_label(std::string const& value) -> std::string {
|
||||
|
||||
auto load_score_documents(std::filesystem::path const &directory, std::bool_t required) -> std::expected<std::vector<vamp_zero::score_document>, modulus::error> {
|
||||
auto error = std::error_code{};
|
||||
if (!std::filesystem::exists(directory, error)) {
|
||||
auto exists = std::filesystem::exists(directory, error);
|
||||
if (error) {
|
||||
return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::UNAVAILABLE, "failed to inspect score assets: " + error.message()));
|
||||
}
|
||||
if (!exists) {
|
||||
if (required) {
|
||||
return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::NOT_FOUND, "score asset directory not found: " + directory.string()));
|
||||
}
|
||||
return std::vector<vamp_zero::score_document>{};
|
||||
}
|
||||
|
||||
auto paths = std::vector<std::filesystem::path>{};
|
||||
for (auto const& entry : std::filesystem::directory_iterator{directory, error}) {
|
||||
auto entries = std::filesystem::directory_iterator{directory, error};
|
||||
auto end = std::filesystem::directory_iterator{};
|
||||
while (!error && entries != end) {
|
||||
auto const &entry = *entries;
|
||||
if (entry.is_regular_file(error) && entry.path().extension() == ".json") {
|
||||
paths.push_back(entry.path());
|
||||
}
|
||||
if (!error) {
|
||||
entries.increment(error);
|
||||
}
|
||||
}
|
||||
if (error) {
|
||||
return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::UNAVAILABLE, "failed to scan score assets: " + error.message()));
|
||||
}
|
||||
if (!entry.is_regular_file() || entry.path().extension() != ".json") {
|
||||
continue;
|
||||
}
|
||||
paths.push_back(entry.path());
|
||||
}
|
||||
std::sort(paths.begin(), paths.end());
|
||||
|
||||
auto documents = std::vector<vamp_zero::score_document>{};
|
||||
for (auto const &path : paths) {
|
||||
auto input = std::ifstream{path, std::ios::binary};
|
||||
@@ -137,9 +137,8 @@ auto sync_primary_variant(vamp_zero::practice_exercise& exercise) -> void {
|
||||
}
|
||||
|
||||
auto same_practice_exercise(vamp_zero::practice_exercise const &left, vamp_zero::practice_exercise const &right) -> std::bool_t {
|
||||
return left.id == right.id
|
||||
&& left.practice_type_id == right.practice_type_id
|
||||
&& left.lesson_group_id == right.lesson_group_id;
|
||||
return left.id == right.id && left.practice_type_id == right.practice_type_id &&
|
||||
left.lesson_group_id == right.lesson_group_id;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -155,7 +154,6 @@ auto vamp_zero::group_library_parts(std::vector<vamp_zero::library_part> parts)
|
||||
}
|
||||
return left.part < right.part;
|
||||
});
|
||||
|
||||
auto groups = std::vector<vamp_zero::song_group>{};
|
||||
for (auto const &part : parts) {
|
||||
if (groups.empty() || groups.back().title != part.title || groups.back().composer != part.composer) {
|
||||
@@ -188,27 +186,20 @@ auto vamp_zero::group_practice_exercises(std::vector<vamp_zero::practice_exercis
|
||||
}
|
||||
return left.source_path < right.source_path;
|
||||
});
|
||||
|
||||
auto merged = std::vector<vamp_zero::practice_exercise>{};
|
||||
for (auto &exercise : exercises) {
|
||||
if (!merged.empty() && same_practice_exercise(merged.back(), exercise)) {
|
||||
merged.back().variants.insert(
|
||||
merged.back().variants.end(),
|
||||
std::make_move_iterator(exercise.variants.begin()),
|
||||
std::make_move_iterator(exercise.variants.end())
|
||||
);
|
||||
merged.back().variants.insert(merged.back().variants.end(), std::make_move_iterator(exercise.variants.begin()), std::make_move_iterator(exercise.variants.end()));
|
||||
sync_primary_variant(merged.back());
|
||||
continue;
|
||||
}
|
||||
sync_primary_variant(exercise);
|
||||
merged.push_back(std::move(exercise));
|
||||
}
|
||||
|
||||
auto types = std::vector<vamp_zero::practice_type_group>{};
|
||||
for (auto const &exercise : merged) {
|
||||
auto type = std::find_if(types.begin(), types.end(), [&](auto const& item) {
|
||||
return item.id == exercise.practice_type_id;
|
||||
});
|
||||
auto type =
|
||||
std::find_if(types.begin(), types.end(), [&](auto const &item) { return item.id == exercise.practice_type_id; });
|
||||
if (type == types.end()) {
|
||||
types.push_back({
|
||||
.id = exercise.practice_type_id,
|
||||
@@ -259,22 +250,29 @@ auto vamp_zero::load_performance_library(std::string const& assets_path) -> std:
|
||||
auto vamp_zero::load_practice_library(std::string const &assets_path) -> std::expected<vamp_zero::practice_library, modulus::error> {
|
||||
auto root = std::filesystem::path{assets_path} / "practice";
|
||||
auto error = std::error_code{};
|
||||
if (!std::filesystem::exists(root, error)) {
|
||||
auto exists = std::filesystem::exists(root, error);
|
||||
if (error) {
|
||||
return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::UNAVAILABLE, "failed to inspect practice assets: " + error.message()));
|
||||
}
|
||||
if (!exists) {
|
||||
return vamp_zero::practice_library{};
|
||||
}
|
||||
|
||||
auto paths = std::vector<std::filesystem::path>{};
|
||||
for (auto const& entry : std::filesystem::recursive_directory_iterator{root, error}) {
|
||||
auto entries = std::filesystem::recursive_directory_iterator{root, error};
|
||||
auto end = std::filesystem::recursive_directory_iterator{};
|
||||
while (!error && entries != end) {
|
||||
auto const &entry = *entries;
|
||||
if (entry.is_regular_file(error) && entry.path().extension() == ".json") {
|
||||
paths.push_back(entry.path());
|
||||
}
|
||||
if (!error) {
|
||||
entries.increment(error);
|
||||
}
|
||||
}
|
||||
if (error) {
|
||||
return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::UNAVAILABLE, "failed to scan practice assets: " + error.message()));
|
||||
}
|
||||
if (!entry.is_regular_file() || entry.path().extension() != ".json") {
|
||||
continue;
|
||||
}
|
||||
paths.push_back(entry.path());
|
||||
}
|
||||
std::sort(paths.begin(), paths.end());
|
||||
|
||||
auto exercises = std::vector<vamp_zero::practice_exercise>{};
|
||||
for (auto const &path : paths) {
|
||||
auto relative = std::filesystem::relative(path, root, error);
|
||||
@@ -285,7 +283,6 @@ auto vamp_zero::load_practice_library(std::string const& assets_path) -> std::ex
|
||||
if (parts.size() != 4U) {
|
||||
return std::unexpected(invalid("practice JSON must be stored as practice/<type>/<lesson_group>/<exercise>/<key>.json: " + path.string()));
|
||||
}
|
||||
|
||||
auto input = std::ifstream{path, std::ios::binary};
|
||||
if (!input) {
|
||||
return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::NOT_FOUND, "failed to open practice JSON: " + path.string()));
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
#include <vamp_zero/app.hpp>
|
||||
|
||||
#include <cstdint>
|
||||
#include <exception>
|
||||
#include <iostream>
|
||||
#include <utility>
|
||||
|
||||
#include <vamp_zero/app.hpp>
|
||||
|
||||
|
||||
auto main(std::int32_t argc, std::char_t **argv) -> std::int32_t {
|
||||
try {
|
||||
auto command = vamp_zero::parse_command_line(argc, argv);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include <algorithm>
|
||||
|
||||
#include <vamp_zero/navigation.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace {
|
||||
|
||||
@@ -82,9 +83,9 @@ auto vamp_zero::active_practice(vamp_zero::app_state const& state) -> vamp_zero:
|
||||
if (state.active_practice_id.empty()) {
|
||||
return nullptr;
|
||||
}
|
||||
auto found = std::find_if(state.practice.exercises.begin(), state.practice.exercises.end(), [&](auto const& exercise) {
|
||||
return exercise.id == state.active_practice_id;
|
||||
});
|
||||
auto found = std::find_if(state.practice.exercises.begin(),
|
||||
state.practice.exercises.end(),
|
||||
[&](auto const &exercise) { return exercise.id == state.active_practice_id; });
|
||||
return found == state.practice.exercises.end() ? nullptr : &*found;
|
||||
}
|
||||
|
||||
@@ -126,7 +127,8 @@ auto vamp_zero::practice_key_row_count(vamp_zero::app_state const& state) -> std
|
||||
}
|
||||
|
||||
auto vamp_zero::effective_practice_key(vamp_zero::app_state const &state, vamp_zero::practice_exercise const &exercise) -> std::string {
|
||||
if (auto const* variant = vamp_zero::selected_practice_variant(state, exercise); variant != nullptr && !variant->key_signature.empty()) {
|
||||
if (auto const *variant = vamp_zero::selected_practice_variant(state, exercise);
|
||||
variant != nullptr && !variant->key_signature.empty()) {
|
||||
return variant->key_signature;
|
||||
}
|
||||
return exercise.key_signature.empty() ? std::string{"C"} : exercise.key_signature;
|
||||
|
||||
+862
-268
File diff suppressed because it is too large
Load Diff
@@ -16,6 +16,7 @@
|
||||
#include <vamp_zero/errors.hpp>
|
||||
#include <vamp_zero/navigation.hpp>
|
||||
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr auto main_menu_performance = std::size_t{0};
|
||||
@@ -52,7 +53,8 @@ auto clamp_audio_state(vamp_zero::audio_state audio) -> vamp_zero::audio_state {
|
||||
}
|
||||
|
||||
auto increase_bpm(vamp_zero::audio_state &audio) -> void {
|
||||
audio.metronome_bpm = audio.metronome_bpm > audio.metronome_max_bpm - std::min(audio.metronome_step, audio.metronome_max_bpm)
|
||||
audio.metronome_bpm =
|
||||
audio.metronome_bpm > audio.metronome_max_bpm - std::min(audio.metronome_step, audio.metronome_max_bpm)
|
||||
? audio.metronome_max_bpm
|
||||
: audio.metronome_bpm + audio.metronome_step;
|
||||
}
|
||||
@@ -72,20 +74,21 @@ auto clamp_state(vamp_zero::app_state state) -> vamp_zero::app_state {
|
||||
} else {
|
||||
state.selected_part_index = 0U;
|
||||
}
|
||||
|
||||
state.selected_practice_type_index = std::min(state.selected_practice_type_index, menu_row_count(state.practice.types.size()) - 1U);
|
||||
state.selected_practice_type_index =
|
||||
std::min(state.selected_practice_type_index, menu_row_count(state.practice.types.size()) - 1U);
|
||||
if (auto const *type = selected_practice_type(state); type != nullptr) {
|
||||
state.selected_practice_lesson_index = std::min(state.selected_practice_lesson_index, menu_row_count(type->lesson_groups.size()) - 1U);
|
||||
state.selected_practice_lesson_index =
|
||||
std::min(state.selected_practice_lesson_index, menu_row_count(type->lesson_groups.size()) - 1U);
|
||||
} else {
|
||||
state.selected_practice_lesson_index = 0U;
|
||||
}
|
||||
if (auto const *lesson = selected_practice_lesson(state); lesson != nullptr) {
|
||||
state.selected_practice_exercise_index = std::min(state.selected_practice_exercise_index, menu_row_count(lesson->exercises.size()) - 1U);
|
||||
state.selected_practice_exercise_index =
|
||||
std::min(state.selected_practice_exercise_index, menu_row_count(lesson->exercises.size()) - 1U);
|
||||
} else {
|
||||
state.selected_practice_exercise_index = 0U;
|
||||
}
|
||||
state.selected_practice_key_index = std::min(state.selected_practice_key_index, practice_key_row_count(state) - 1U);
|
||||
|
||||
if (state.mode == vamp_zero::app_mode::PERFORMANCE_PART_MENU && selected_group(state) == nullptr) {
|
||||
state.mode = vamp_zero::app_mode::PERFORMANCE_SONG_MENU;
|
||||
}
|
||||
@@ -93,7 +96,8 @@ auto clamp_state(vamp_zero::app_state state) -> vamp_zero::app_state {
|
||||
state.mode = vamp_zero::app_mode::PRACTICE_TYPE_MENU;
|
||||
}
|
||||
if (state.mode == vamp_zero::app_mode::PRACTICE_EXERCISE_MENU && selected_practice_lesson(state) == nullptr) {
|
||||
state.mode = selected_practice_type(state) == nullptr ? vamp_zero::app_mode::PRACTICE_TYPE_MENU : vamp_zero::app_mode::PRACTICE_LESSON_MENU;
|
||||
state.mode = selected_practice_type(state) == nullptr ? vamp_zero::app_mode::PRACTICE_TYPE_MENU
|
||||
: vamp_zero::app_mode::PRACTICE_LESSON_MENU;
|
||||
}
|
||||
if (state.mode == vamp_zero::app_mode::PRACTICE_KEY_MENU) {
|
||||
auto const *exercise = selected_practice_exercise(state);
|
||||
@@ -104,9 +108,9 @@ auto clamp_state(vamp_zero::app_state state) -> vamp_zero::app_state {
|
||||
state.selected_practice_key_index = 0U;
|
||||
}
|
||||
}
|
||||
|
||||
if (state.mode == vamp_zero::app_mode::PERFORMANCE_SCORE && active_part(state) == nullptr) {
|
||||
state.mode = selected_group(state) == nullptr ? vamp_zero::app_mode::PERFORMANCE_SONG_MENU : vamp_zero::app_mode::PERFORMANCE_PART_MENU;
|
||||
state.mode = selected_group(state) == nullptr ? vamp_zero::app_mode::PERFORMANCE_SONG_MENU
|
||||
: vamp_zero::app_mode::PERFORMANCE_PART_MENU;
|
||||
state.active_part_id.clear();
|
||||
state.page_index = 0;
|
||||
} else if (state.mode == vamp_zero::app_mode::PRACTICE_SCORE) {
|
||||
@@ -121,14 +125,13 @@ auto clamp_state(vamp_zero::app_state state) -> vamp_zero::app_state {
|
||||
state.page_index = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (state.mode != vamp_zero::app_mode::MAIN_MENU) {
|
||||
state.audio.tempo_editing = false;
|
||||
}
|
||||
|
||||
if (auto pages = active_pages(state); pages != 0U) {
|
||||
state.page_index = std::min(state.page_index, pages - 1U);
|
||||
} else if (state.mode != vamp_zero::app_mode::PERFORMANCE_SCORE && state.mode != vamp_zero::app_mode::PRACTICE_SCORE) {
|
||||
} else if (state.mode != vamp_zero::app_mode::PERFORMANCE_SCORE &&
|
||||
state.mode != vamp_zero::app_mode::PRACTICE_SCORE) {
|
||||
state.page_index = 0;
|
||||
}
|
||||
return state;
|
||||
@@ -246,7 +249,6 @@ auto performance_json(vamp_zero::app_state const& state) -> boost::json::object
|
||||
item["parts"] = std::move(parts);
|
||||
groups.push_back(std::move(item));
|
||||
}
|
||||
|
||||
auto library = boost::json::object{};
|
||||
library["song_count"] = static_cast<std::uint64_t>(state.performance.groups.size());
|
||||
library["part_count"] = static_cast<std::uint64_t>(state.performance.parts.size());
|
||||
@@ -267,7 +269,6 @@ auto practice_json(vamp_zero::app_state const& state) -> boost::json::object {
|
||||
for (auto key : vamp_zero::practice_key_names()) {
|
||||
keys.push_back(boost::json::value{std::string{key}});
|
||||
}
|
||||
|
||||
auto selection = boost::json::object{};
|
||||
selection["type_row_index"] = static_cast<std::uint64_t>(state.selected_practice_type_index);
|
||||
selection["lesson_row_index"] = static_cast<std::uint64_t>(state.selected_practice_lesson_index);
|
||||
@@ -297,7 +298,6 @@ auto practice_json(vamp_zero::app_state const& state) -> boost::json::object {
|
||||
selection["key"] = std::string{*key};
|
||||
}
|
||||
}
|
||||
|
||||
auto practice = boost::json::object{};
|
||||
practice["type_count"] = static_cast<std::uint64_t>(state.practice.types.size());
|
||||
practice["exercise_count"] = static_cast<std::uint64_t>(state.practice.exercises.size());
|
||||
@@ -330,7 +330,6 @@ auto vamp_zero::apply_app_command(vamp_zero::app_state state, vamp_protocol::pag
|
||||
++state.select_count;
|
||||
}
|
||||
state.last_command = vamp_protocol::page_command_name(command);
|
||||
|
||||
switch (state.mode) {
|
||||
case vamp_zero::app_mode::MAIN_MENU:
|
||||
if (state.audio.tempo_editing) {
|
||||
@@ -367,13 +366,13 @@ auto vamp_zero::apply_app_command(vamp_zero::app_state state, vamp_protocol::pag
|
||||
state.audio.tempo_editing = true;
|
||||
}
|
||||
return clamp_state(std::move(state));
|
||||
|
||||
case vamp_zero::app_mode::PERFORMANCE_SONG_MENU:
|
||||
if (command == vamp_protocol::page_command::NEXT) {
|
||||
state.selected_song_index = wrap_next(state.selected_song_index, menu_row_count(state.performance.groups.size()));
|
||||
state.selected_part_index = 0;
|
||||
} else if (command == vamp_protocol::page_command::PREVIOUS) {
|
||||
state.selected_song_index = wrap_previous(state.selected_song_index, menu_row_count(state.performance.groups.size()));
|
||||
state.selected_song_index =
|
||||
wrap_previous(state.selected_song_index, menu_row_count(state.performance.groups.size()));
|
||||
state.selected_part_index = 0;
|
||||
} else if (state.selected_song_index == 0U) {
|
||||
state.mode = vamp_zero::app_mode::MAIN_MENU;
|
||||
@@ -382,7 +381,6 @@ auto vamp_zero::apply_app_command(vamp_zero::app_state state, vamp_protocol::pag
|
||||
state.selected_part_index = 0;
|
||||
}
|
||||
return clamp_state(std::move(state));
|
||||
|
||||
case vamp_zero::app_mode::PERFORMANCE_PART_MENU:
|
||||
if (auto const *group = selected_group(state); group != nullptr) {
|
||||
if (command == vamp_protocol::page_command::NEXT) {
|
||||
@@ -401,7 +399,6 @@ auto vamp_zero::apply_app_command(vamp_zero::app_state state, vamp_protocol::pag
|
||||
}
|
||||
}
|
||||
return clamp_state(std::move(state));
|
||||
|
||||
case vamp_zero::app_mode::PERFORMANCE_SCORE:
|
||||
if (auto const *part = active_part(state); part != nullptr) {
|
||||
if (command == vamp_protocol::page_command::NEXT) {
|
||||
@@ -415,15 +412,16 @@ auto vamp_zero::apply_app_command(vamp_zero::app_state state, vamp_protocol::pag
|
||||
}
|
||||
}
|
||||
return clamp_state(std::move(state));
|
||||
|
||||
case vamp_zero::app_mode::PRACTICE_TYPE_MENU:
|
||||
if (command == vamp_protocol::page_command::NEXT) {
|
||||
state.selected_practice_type_index = wrap_next(state.selected_practice_type_index, menu_row_count(state.practice.types.size()));
|
||||
state.selected_practice_type_index =
|
||||
wrap_next(state.selected_practice_type_index, menu_row_count(state.practice.types.size()));
|
||||
state.selected_practice_lesson_index = 0;
|
||||
state.selected_practice_exercise_index = 0;
|
||||
state.selected_practice_key_index = 0;
|
||||
} else if (command == vamp_protocol::page_command::PREVIOUS) {
|
||||
state.selected_practice_type_index = wrap_previous(state.selected_practice_type_index, menu_row_count(state.practice.types.size()));
|
||||
state.selected_practice_type_index =
|
||||
wrap_previous(state.selected_practice_type_index, menu_row_count(state.practice.types.size()));
|
||||
state.selected_practice_lesson_index = 0;
|
||||
state.selected_practice_exercise_index = 0;
|
||||
state.selected_practice_key_index = 0;
|
||||
@@ -436,15 +434,16 @@ auto vamp_zero::apply_app_command(vamp_zero::app_state state, vamp_protocol::pag
|
||||
state.selected_practice_key_index = 0;
|
||||
}
|
||||
return clamp_state(std::move(state));
|
||||
|
||||
case vamp_zero::app_mode::PRACTICE_LESSON_MENU:
|
||||
if (auto const *type = selected_practice_type(state); type != nullptr) {
|
||||
if (command == vamp_protocol::page_command::NEXT) {
|
||||
state.selected_practice_lesson_index = wrap_next(state.selected_practice_lesson_index, menu_row_count(type->lesson_groups.size()));
|
||||
state.selected_practice_lesson_index =
|
||||
wrap_next(state.selected_practice_lesson_index, menu_row_count(type->lesson_groups.size()));
|
||||
state.selected_practice_exercise_index = 0;
|
||||
state.selected_practice_key_index = 0;
|
||||
} else if (command == vamp_protocol::page_command::PREVIOUS) {
|
||||
state.selected_practice_lesson_index = wrap_previous(state.selected_practice_lesson_index, menu_row_count(type->lesson_groups.size()));
|
||||
state.selected_practice_lesson_index =
|
||||
wrap_previous(state.selected_practice_lesson_index, menu_row_count(type->lesson_groups.size()));
|
||||
state.selected_practice_exercise_index = 0;
|
||||
state.selected_practice_key_index = 0;
|
||||
} else if (state.selected_practice_lesson_index == 0U) {
|
||||
@@ -458,14 +457,15 @@ auto vamp_zero::apply_app_command(vamp_zero::app_state state, vamp_protocol::pag
|
||||
}
|
||||
}
|
||||
return clamp_state(std::move(state));
|
||||
|
||||
case vamp_zero::app_mode::PRACTICE_EXERCISE_MENU:
|
||||
if (auto const *lesson = selected_practice_lesson(state); lesson != nullptr) {
|
||||
if (command == vamp_protocol::page_command::NEXT) {
|
||||
state.selected_practice_exercise_index = wrap_next(state.selected_practice_exercise_index, menu_row_count(lesson->exercises.size()));
|
||||
state.selected_practice_exercise_index =
|
||||
wrap_next(state.selected_practice_exercise_index, menu_row_count(lesson->exercises.size()));
|
||||
state.selected_practice_key_index = 0;
|
||||
} else if (command == vamp_protocol::page_command::PREVIOUS) {
|
||||
state.selected_practice_exercise_index = wrap_previous(state.selected_practice_exercise_index, menu_row_count(lesson->exercises.size()));
|
||||
state.selected_practice_exercise_index =
|
||||
wrap_previous(state.selected_practice_exercise_index, menu_row_count(lesson->exercises.size()));
|
||||
state.selected_practice_key_index = 0;
|
||||
} else if (state.selected_practice_exercise_index == 0U) {
|
||||
state.mode = vamp_zero::app_mode::PRACTICE_LESSON_MENU;
|
||||
@@ -486,13 +486,15 @@ auto vamp_zero::apply_app_command(vamp_zero::app_state state, vamp_protocol::pag
|
||||
}
|
||||
}
|
||||
return clamp_state(std::move(state));
|
||||
|
||||
case vamp_zero::app_mode::PRACTICE_KEY_MENU:
|
||||
if (auto const* exercise = selected_practice_exercise(state); exercise != nullptr && supports_key_selection(*exercise)) {
|
||||
if (auto const *exercise = selected_practice_exercise(state);
|
||||
exercise != nullptr && supports_key_selection(*exercise)) {
|
||||
if (command == vamp_protocol::page_command::NEXT) {
|
||||
state.selected_practice_key_index = wrap_next(state.selected_practice_key_index, menu_row_count(exercise->variants.size()));
|
||||
state.selected_practice_key_index =
|
||||
wrap_next(state.selected_practice_key_index, menu_row_count(exercise->variants.size()));
|
||||
} else if (command == vamp_protocol::page_command::PREVIOUS) {
|
||||
state.selected_practice_key_index = wrap_previous(state.selected_practice_key_index, menu_row_count(exercise->variants.size()));
|
||||
state.selected_practice_key_index =
|
||||
wrap_previous(state.selected_practice_key_index, menu_row_count(exercise->variants.size()));
|
||||
} else if (state.selected_practice_key_index == 0U) {
|
||||
state.mode = vamp_zero::app_mode::PRACTICE_EXERCISE_MENU;
|
||||
state.active_practice_id.clear();
|
||||
@@ -505,7 +507,6 @@ auto vamp_zero::apply_app_command(vamp_zero::app_state state, vamp_protocol::pag
|
||||
}
|
||||
}
|
||||
return clamp_state(std::move(state));
|
||||
|
||||
case vamp_zero::app_mode::PRACTICE_SCORE:
|
||||
if (auto const *exercise = active_practice(state); exercise != nullptr) {
|
||||
auto const *variant = selected_practice_variant(state, *exercise);
|
||||
@@ -515,7 +516,8 @@ auto vamp_zero::apply_app_command(vamp_zero::app_state state, vamp_protocol::pag
|
||||
} else if (command == vamp_protocol::page_command::PREVIOUS) {
|
||||
state.page_index = state.page_index == 0U ? 0U : state.page_index - 1U;
|
||||
} else {
|
||||
state.mode = supports_key_selection(*exercise) ? vamp_zero::app_mode::PRACTICE_KEY_MENU : vamp_zero::app_mode::PRACTICE_EXERCISE_MENU;
|
||||
state.mode = supports_key_selection(*exercise) ? vamp_zero::app_mode::PRACTICE_KEY_MENU
|
||||
: vamp_zero::app_mode::PRACTICE_EXERCISE_MENU;
|
||||
state.active_practice_id.clear();
|
||||
state.page_index = 0;
|
||||
}
|
||||
@@ -537,7 +539,6 @@ auto vamp_zero::app_status_json(vamp_zero::app_state const& state) -> boost::jso
|
||||
if (auto const *part = selected_part(state); part != nullptr) {
|
||||
selection["part"] = part_json(*part);
|
||||
}
|
||||
|
||||
auto active = boost::json::object{};
|
||||
if (state.mode == vamp_zero::app_mode::PERFORMANCE_SCORE) {
|
||||
if (auto const *part = active_part(state); part != nullptr) {
|
||||
@@ -566,11 +567,9 @@ auto vamp_zero::app_status_json(vamp_zero::app_state const& state) -> boost::jso
|
||||
active["page_number"] = 0;
|
||||
active["page_total"] = 0;
|
||||
}
|
||||
|
||||
auto remote = boost::json::object{};
|
||||
remote["pico_connected"] = state.pico_connected;
|
||||
remote["heartbeat_count"] = state.heartbeat_count;
|
||||
|
||||
auto performance = performance_json(state);
|
||||
auto root = boost::json::object{};
|
||||
root["mode"] = vamp_zero::to_string(state.mode);
|
||||
@@ -590,7 +589,7 @@ auto vamp_zero::remote_display_text(vamp_zero::app_state const& state) -> std::s
|
||||
auto part = std::string{};
|
||||
auto page = std::size_t{};
|
||||
auto pages = std::size_t{};
|
||||
auto active = false;
|
||||
auto active = std::bool_t{false};
|
||||
if (state.mode == vamp_zero::app_mode::PERFORMANCE_SCORE) {
|
||||
if (auto const *selected = active_part(state); selected != nullptr) {
|
||||
active = true;
|
||||
@@ -609,7 +608,6 @@ auto vamp_zero::remote_display_text(vamp_zero::app_state const& state) -> std::s
|
||||
pages = variant == nullptr ? 0U : variant->total_pages;
|
||||
}
|
||||
}
|
||||
|
||||
auto output = std::ostringstream{};
|
||||
output << "mode=" << vamp_zero::to_string(state.mode) << "\n";
|
||||
output << "active=" << (active ? "1" : "0") << "\n";
|
||||
@@ -633,7 +631,8 @@ auto vamp_zero::validate_app_state(vamp_zero::app_state const& state) -> std::ex
|
||||
if (state.audio.metronome_step == 0U || state.audio.metronome_step > 100U) {
|
||||
return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::INVALID_ARGUMENT, "metronome BPM step is out of range"));
|
||||
}
|
||||
if (state.audio.metronome_bpm < state.audio.metronome_min_bpm || state.audio.metronome_bpm > state.audio.metronome_max_bpm) {
|
||||
if (state.audio.metronome_bpm < state.audio.metronome_min_bpm ||
|
||||
state.audio.metronome_bpm > state.audio.metronome_max_bpm) {
|
||||
return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::INVALID_ARGUMENT, "metronome BPM is out of range"));
|
||||
}
|
||||
if (state.audio.tempo_editing && state.mode != vamp_zero::app_mode::MAIN_MENU) {
|
||||
@@ -642,16 +641,19 @@ auto vamp_zero::validate_app_state(vamp_zero::app_state const& state) -> std::ex
|
||||
if (state.selected_song_index >= menu_row_count(state.performance.groups.size())) {
|
||||
return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::INVALID_ARGUMENT, "selected performance song row is out of range"));
|
||||
}
|
||||
if (auto const* group = selected_group(state); group != nullptr && state.selected_part_index >= menu_row_count(group->parts.size())) {
|
||||
if (auto const *group = selected_group(state);
|
||||
group != nullptr && state.selected_part_index >= menu_row_count(group->parts.size())) {
|
||||
return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::INVALID_ARGUMENT, "selected performance part row is out of range"));
|
||||
}
|
||||
if (state.selected_practice_type_index >= menu_row_count(state.practice.types.size())) {
|
||||
return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::INVALID_ARGUMENT, "selected practice type row is out of range"));
|
||||
}
|
||||
if (auto const* type = selected_practice_type(state); type != nullptr && state.selected_practice_lesson_index >= menu_row_count(type->lesson_groups.size())) {
|
||||
if (auto const *type = selected_practice_type(state);
|
||||
type != nullptr && state.selected_practice_lesson_index >= menu_row_count(type->lesson_groups.size())) {
|
||||
return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::INVALID_ARGUMENT, "selected practice lesson row is out of range"));
|
||||
}
|
||||
if (auto const* lesson = selected_practice_lesson(state); lesson != nullptr && state.selected_practice_exercise_index >= menu_row_count(lesson->exercises.size())) {
|
||||
if (auto const *lesson = selected_practice_lesson(state);
|
||||
lesson != nullptr && state.selected_practice_exercise_index >= menu_row_count(lesson->exercises.size())) {
|
||||
return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::INVALID_ARGUMENT, "selected practice exercise row is out of range"));
|
||||
}
|
||||
if (state.selected_practice_key_index >= practice_key_row_count(state)) {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
#include <vamp_zero/portal.hpp>
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <expected>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
@@ -15,6 +14,7 @@
|
||||
#include <vamp_protocol/page_control.hpp>
|
||||
#include <vamp_zero/page_control.hpp>
|
||||
|
||||
|
||||
namespace {
|
||||
|
||||
auto portal_html() -> std::string {
|
||||
@@ -41,7 +41,8 @@ auto remote_response(vamp_zero::app_state const& state) -> ligo::service_respons
|
||||
|
||||
}
|
||||
|
||||
vamp_zero::control_state::control_state(vamp_zero::config config, vamp_zero::app_state state) : config_{std::move(config)}, state_{std::move(state)} {}
|
||||
vamp_zero::control_state::control_state(vamp_zero::config config, vamp_zero::app_state state)
|
||||
: config_{std::move(config)}, state_{std::move(state)} {}
|
||||
|
||||
auto vamp_zero::control_state::config() const -> vamp_zero::config {
|
||||
auto lock = std::scoped_lock{mutex_};
|
||||
@@ -64,7 +65,7 @@ auto vamp_zero::control_state::snapshot() const -> vamp_zero::app_state {
|
||||
}
|
||||
|
||||
auto vamp_zero::control_state::apply(vamp_protocol::page_command command) -> vamp_zero::app_state {
|
||||
auto observer = vamp_zero::control_state::observer{};
|
||||
auto observer = std::function<void(vamp_zero::app_state)>{};
|
||||
auto updated = vamp_zero::app_state{};
|
||||
{
|
||||
auto lock = std::scoped_lock{mutex_};
|
||||
@@ -80,7 +81,7 @@ auto vamp_zero::control_state::apply(vamp_protocol::page_command command) -> vam
|
||||
}
|
||||
|
||||
auto vamp_zero::control_state::heartbeat() -> vamp_zero::app_state {
|
||||
auto observer = vamp_zero::control_state::observer{};
|
||||
auto observer = std::function<void(vamp_zero::app_state)>{};
|
||||
auto updated = vamp_zero::app_state{};
|
||||
{
|
||||
auto lock = std::scoped_lock{mutex_};
|
||||
@@ -100,7 +101,7 @@ auto vamp_zero::control_state::heartbeat() -> vamp_zero::app_state {
|
||||
}
|
||||
|
||||
auto vamp_zero::control_state::refresh_connection() -> std::optional<vamp_zero::app_state> {
|
||||
auto observer = vamp_zero::control_state::observer{};
|
||||
auto observer = std::function<void(vamp_zero::app_state)>{};
|
||||
auto updated = vamp_zero::app_state{};
|
||||
{
|
||||
auto lock = std::scoped_lock{mutex_};
|
||||
@@ -118,7 +119,7 @@ auto vamp_zero::control_state::refresh_connection() -> std::optional<vamp_zero::
|
||||
return updated;
|
||||
}
|
||||
|
||||
auto vamp_zero::control_state::set_observer(vamp_zero::control_state::observer observer) -> void {
|
||||
auto vamp_zero::control_state::set_observer(std::function<void(vamp_zero::app_state)> observer) -> void {
|
||||
auto lock = std::scoped_lock{mutex_};
|
||||
observer_ = std::move(observer);
|
||||
}
|
||||
@@ -128,7 +129,6 @@ auto vamp_zero::portal_status_json(vamp_zero::config const& config, vamp_zero::a
|
||||
control["ap_ssid"] = config.control.ap_ssid;
|
||||
control["zero_ip"] = config.control.zero_ip;
|
||||
control["endpoint"] = "http://" + config.control.zero_ip + ":" + endpoint_port(config.control.endpoint.bind);
|
||||
|
||||
auto root = boost::json::object{};
|
||||
root["device"] = "zero";
|
||||
root["version"] = VAMP_VERSION;
|
||||
|
||||
+652
-558
File diff suppressed because it is too large
Load Diff
@@ -3,6 +3,7 @@
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
|
||||
auto vamp_zero::to_string(vamp_zero::hardware_mode value) -> std::string {
|
||||
switch (value) {
|
||||
case vamp_zero::hardware_mode::FAKE:
|
||||
|
||||
+179
-68
@@ -22,14 +22,15 @@
|
||||
#include <vamp_zero/color.hpp>
|
||||
#include <vamp_zero/library.hpp>
|
||||
#include <vamp_zero/navigation.hpp>
|
||||
#include <vamp_zero/parse.hpp>
|
||||
#include <vamp_zero/score.hpp>
|
||||
|
||||
|
||||
struct vamp_zero::score_draw_cache {
|
||||
struct entry {
|
||||
std::string source_json{};
|
||||
std::shared_ptr<vamp_zero::score_document const> document{};
|
||||
};
|
||||
|
||||
auto get(std::string_view source_json, std::string source_path, std::string part_override) -> std::expected<std::shared_ptr<vamp_zero::score_document const>, modulus::error> {
|
||||
auto key = source_path;
|
||||
key.push_back('\n');
|
||||
@@ -38,12 +39,10 @@ struct vamp_zero::score_draw_cache {
|
||||
key += std::to_string(source_json.size());
|
||||
key.push_back('\n');
|
||||
key += std::to_string(std::hash<std::string_view>{}(source_json));
|
||||
|
||||
auto found = entries.find(key);
|
||||
if (found != entries.end() && found->second.source_json == source_json && found->second.document) {
|
||||
return found->second.document;
|
||||
}
|
||||
|
||||
auto parsed = vamp_zero::parse_score_document(source_json, source_path);
|
||||
if (!parsed) {
|
||||
return std::unexpected(parsed.error());
|
||||
@@ -58,15 +57,13 @@ struct vamp_zero::score_draw_cache {
|
||||
};
|
||||
return document;
|
||||
}
|
||||
|
||||
std::unordered_map<std::string, entry> entries{};
|
||||
std::shared_ptr<vamp_zero::score_document const> page_document{};
|
||||
std::size_t page_index{};
|
||||
std::optional<vamp_zero::score_page_layout> page{};
|
||||
|
||||
auto draw(luma::draw_context &context, luma::rect bounds, std::shared_ptr<vamp_zero::score_document const> document, std::size_t index) -> void {
|
||||
if (!page || page_document != document || index != page_index
|
||||
|| page->bounds.x != bounds.x || page->bounds.y != bounds.y || page->bounds.width != bounds.width || page->bounds.height != bounds.height) {
|
||||
if (!page || page_document != document || index != page_index || page->bounds.x != bounds.x ||
|
||||
page->bounds.y != bounds.y || page->bounds.width != bounds.width || page->bounds.height != bounds.height) {
|
||||
page = vamp_zero::layout_score_page(bounds, *document, index);
|
||||
page_document = std::move(document);
|
||||
page_index = index;
|
||||
@@ -80,19 +77,13 @@ namespace {
|
||||
constexpr auto touch_hold_duration = std::chrono::milliseconds{700};
|
||||
constexpr auto touch_double_tap_duration = std::chrono::milliseconds{220};
|
||||
|
||||
using touch_handler = std::function<std::bool_t(luma::touch_event const&)>;
|
||||
|
||||
auto bind_port(vamp_zero::config const &config) -> std::uint16_t {
|
||||
auto const &bind = config.control.endpoint.bind;
|
||||
auto colon = bind.rfind(':');
|
||||
if (colon == std::string::npos) {
|
||||
return 8080;
|
||||
}
|
||||
try {
|
||||
return static_cast<std::uint16_t>(std::stoul(bind.substr(colon + 1U)));
|
||||
} catch (...) {
|
||||
return 8080;
|
||||
}
|
||||
return vamp_zero::parse_integer<std::uint16_t>(std::string_view{bind}.substr(colon + 1U), "port").value_or(8080);
|
||||
}
|
||||
|
||||
auto status_color(vamp_zero::app_state const &state) -> vamp_style::color {
|
||||
@@ -126,11 +117,12 @@ auto draw_text(luma::draw_context& context, std::float32_t x, std::float32_t y,
|
||||
}
|
||||
|
||||
auto draw_header(luma::draw_context &context, luma::rect bounds, vamp_zero::ui_state const &state) -> void {
|
||||
context.fill_rect({.x = bounds.x, .y = bounds.y, .width = bounds.width, .height = 80.0F}, vamp_zero::to_luma(vamp_style::panel));
|
||||
context.fill_rect({.x = bounds.x, .y = bounds.y + 78.0F, .width = bounds.width, .height = 2.0F}, vamp_zero::to_luma(vamp_style::accent));
|
||||
context.fill_rect(
|
||||
{.x = bounds.x, .y = bounds.y, .width = bounds.width, .height = 80.0F}, vamp_zero::to_luma(vamp_style::panel));
|
||||
context.fill_rect({.x = bounds.x, .y = bounds.y + 78.0F, .width = bounds.width, .height = 2.0F},
|
||||
vamp_zero::to_luma(vamp_style::accent));
|
||||
draw_text(context, bounds.x + 28.0F, bounds.y + 18.0F, "PRODUCT VAMP", 4, vamp_style::ink, 360.0F);
|
||||
draw_text(context, bounds.x + 30.0F, bounds.y + 55.0F, "AP " + state.ap_ssid + " " + state.zero_ip + ":" + std::to_string(state.port), 1, vamp_style::muted, 520.0F);
|
||||
|
||||
auto indicator = status_color(state.app);
|
||||
auto x = bounds.x + bounds.width - 148.0F;
|
||||
context.fill_rect({.x = x, .y = bounds.y + 22.0F, .width = 24.0F, .height = 24.0F}, vamp_zero::to_luma(indicator));
|
||||
@@ -141,7 +133,10 @@ auto draw_row(luma::draw_context& context, luma::rect row, std::string const& pr
|
||||
if (selected) {
|
||||
context.fill_rect(row, vamp_zero::to_luma(vamp_style::selection));
|
||||
}
|
||||
context.stroke_line({.x = row.x, .y = row.y + row.height}, {.x = row.x + row.width, .y = row.y + row.height}, 1.0F, vamp_zero::to_luma(vamp_style::rule));
|
||||
context.stroke_line({.x = row.x, .y = row.y + row.height},
|
||||
{.x = row.x + row.width, .y = row.y + row.height},
|
||||
1.0F,
|
||||
vamp_zero::to_luma(vamp_style::rule));
|
||||
draw_text(context, row.x + 16.0F, row.y + 10.0F, primary, 2, vamp_style::ink, row.width * 0.62F);
|
||||
if (!secondary.empty()) {
|
||||
draw_text(context, row.x + row.width - 250.0F, row.y + 17.0F, secondary, 1, selected ? vamp_style::ink : vamp_style::muted, 230.0F);
|
||||
@@ -161,21 +156,40 @@ auto draw_main_menu(luma::draw_context& context, luma::rect bounds, vamp_zero::a
|
||||
auto list_y = bounds.y + 112.0F;
|
||||
auto row_height = std::float32_t{54.0F};
|
||||
auto row_width = bounds.width - 72.0F;
|
||||
draw_row(context, {.x = bounds.x + 36.0F, .y = list_y, .width = row_width, .height = row_height}, "Performance Mode", std::to_string(state.performance.groups.size()) + " SONGS", state.selected_main_menu_index == 0U);
|
||||
draw_row(context, {.x = bounds.x + 36.0F, .y = list_y + row_height, .width = row_width, .height = row_height}, "Practice Mode", std::to_string(state.practice.exercises.size()) + " EXERCISES", state.selected_main_menu_index == 1U);
|
||||
draw_row(context, {.x = bounds.x + 36.0F, .y = list_y + row_height * 2.0F, .width = row_width, .height = row_height}, "Track", state.audio.track_enabled ? "ON" : "OFF", state.selected_main_menu_index == 2U);
|
||||
draw_row(context, {.x = bounds.x + 36.0F, .y = list_y + row_height * 3.0F, .width = row_width, .height = row_height}, "Metronome", state.audio.metronome_enabled ? "ON" : "OFF", state.selected_main_menu_index == 3U);
|
||||
draw_row(context,
|
||||
{.x = bounds.x + 36.0F, .y = list_y, .width = row_width, .height = row_height},
|
||||
"Performance Mode",
|
||||
std::to_string(state.performance.groups.size()) + " SONGS",
|
||||
state.selected_main_menu_index == 0U);
|
||||
draw_row(context,
|
||||
{.x = bounds.x + 36.0F, .y = list_y + row_height, .width = row_width, .height = row_height},
|
||||
"Practice Mode",
|
||||
std::to_string(state.practice.exercises.size()) + " EXERCISES",
|
||||
state.selected_main_menu_index == 1U);
|
||||
draw_row(context,
|
||||
{.x = bounds.x + 36.0F, .y = list_y + row_height * 2.0F, .width = row_width, .height = row_height},
|
||||
"Track",
|
||||
state.audio.track_enabled ? "ON" : "OFF",
|
||||
state.selected_main_menu_index == 2U);
|
||||
draw_row(context,
|
||||
{.x = bounds.x + 36.0F, .y = list_y + row_height * 3.0F, .width = row_width, .height = row_height},
|
||||
"Metronome",
|
||||
state.audio.metronome_enabled ? "ON" : "OFF",
|
||||
state.selected_main_menu_index == 3U);
|
||||
auto tempo = std::to_string(state.audio.metronome_bpm) + " BPM";
|
||||
if (state.audio.tempo_editing) {
|
||||
tempo = "EDIT " + tempo;
|
||||
}
|
||||
draw_row(context, {.x = bounds.x + 36.0F, .y = list_y + row_height * 4.0F, .width = row_width, .height = row_height}, "Tempo", tempo, state.selected_main_menu_index == 4U);
|
||||
draw_row(context,
|
||||
{.x = bounds.x + 36.0F, .y = list_y + row_height * 4.0F, .width = row_width, .height = row_height},
|
||||
"Tempo",
|
||||
tempo,
|
||||
state.selected_main_menu_index == 4U);
|
||||
}
|
||||
|
||||
auto draw_performance_song_menu(luma::draw_context &context, luma::rect bounds, vamp_zero::app_state const &state) -> void {
|
||||
draw_text(context, bounds.x + 36.0F, bounds.y + 28.0F, "PERFORMANCE", 4, vamp_style::ink, 360.0F);
|
||||
draw_text(context, bounds.x + 38.0F, bounds.y + 66.0F, std::to_string(state.performance.groups.size()) + " SONGS " + std::to_string(state.performance.parts.size()) + " PARTS", 1, vamp_style::muted, 320.0F);
|
||||
|
||||
auto list_y = bounds.y + 112.0F;
|
||||
auto row_height = std::float32_t{54.0F};
|
||||
auto row_width = bounds.width - 72.0F;
|
||||
@@ -185,11 +199,25 @@ auto draw_performance_song_menu(luma::draw_context& context, luma::rect bounds,
|
||||
for (auto row = std::size_t{}; row < max_rows; ++row) {
|
||||
auto index = first + row;
|
||||
if (index == 0U) {
|
||||
draw_row(context, {.x = bounds.x + 36.0F, .y = list_y + static_cast<std::float32_t>(row) * row_height, .width = row_width, .height = row_height}, "BACK", "", state.selected_song_index == 0U);
|
||||
draw_row(context,
|
||||
{.x = bounds.x + 36.0F,
|
||||
.y = list_y + static_cast<std::float32_t>(row) * row_height,
|
||||
.width = row_width,
|
||||
.height = row_height},
|
||||
"BACK",
|
||||
"",
|
||||
state.selected_song_index == 0U);
|
||||
continue;
|
||||
}
|
||||
auto const &group = state.performance.groups[index - 1U];
|
||||
draw_row(context, {.x = bounds.x + 36.0F, .y = list_y + static_cast<std::float32_t>(row) * row_height, .width = row_width, .height = row_height}, group.title, std::to_string(group.parts.size()) + " PARTS", index == state.selected_song_index);
|
||||
draw_row(context,
|
||||
{.x = bounds.x + 36.0F,
|
||||
.y = list_y + static_cast<std::float32_t>(row) * row_height,
|
||||
.width = row_width,
|
||||
.height = row_height},
|
||||
group.title,
|
||||
std::to_string(group.parts.size()) + " PARTS",
|
||||
index == state.selected_song_index);
|
||||
}
|
||||
if (state.performance.groups.empty()) {
|
||||
draw_text(context, bounds.x + 38.0F, list_y + row_height + 28.0F, "NO SONGS FOUND", 3, vamp_style::retrying, 420.0F);
|
||||
@@ -198,13 +226,18 @@ auto draw_performance_song_menu(luma::draw_context& context, luma::rect bounds,
|
||||
|
||||
auto draw_performance_part_menu(luma::draw_context &context, luma::rect bounds, vamp_zero::app_state const &state) -> void {
|
||||
auto const *group = selected_group(state);
|
||||
draw_text(context, bounds.x + 36.0F, bounds.y + 28.0F, group == nullptr ? std::string{"PARTS"} : group->title, 4, vamp_style::ink, bounds.width - 72.0F);
|
||||
draw_text(context,
|
||||
bounds.x + 36.0F,
|
||||
bounds.y + 28.0F,
|
||||
group == nullptr ? std::string{"PARTS"} : group->title,
|
||||
4,
|
||||
vamp_style::ink,
|
||||
bounds.width - 72.0F);
|
||||
if (group == nullptr) {
|
||||
draw_text(context, bounds.x + 38.0F, bounds.y + 118.0F, "NO PARTS FOUND", 3, vamp_style::retrying, 420.0F);
|
||||
return;
|
||||
}
|
||||
draw_text(context, bounds.x + 38.0F, bounds.y + 66.0F, group->composer, 1, vamp_style::muted, bounds.width - 72.0F);
|
||||
|
||||
auto list_y = bounds.y + 112.0F;
|
||||
auto row_height = std::float32_t{54.0F};
|
||||
auto row_width = bounds.width - 72.0F;
|
||||
@@ -214,18 +247,31 @@ auto draw_performance_part_menu(luma::draw_context& context, luma::rect bounds,
|
||||
for (auto row = std::size_t{}; row < max_rows; ++row) {
|
||||
auto index = first + row;
|
||||
if (index == 0U) {
|
||||
draw_row(context, {.x = bounds.x + 36.0F, .y = list_y + static_cast<std::float32_t>(row) * row_height, .width = row_width, .height = row_height}, "BACK", "", state.selected_part_index == 0U);
|
||||
draw_row(context,
|
||||
{.x = bounds.x + 36.0F,
|
||||
.y = list_y + static_cast<std::float32_t>(row) * row_height,
|
||||
.width = row_width,
|
||||
.height = row_height},
|
||||
"BACK",
|
||||
"",
|
||||
state.selected_part_index == 0U);
|
||||
continue;
|
||||
}
|
||||
auto const &part = group->parts[index - 1U];
|
||||
draw_row(context, {.x = bounds.x + 36.0F, .y = list_y + static_cast<std::float32_t>(row) * row_height, .width = row_width, .height = row_height}, part.part, std::to_string(part.total_pages) + " PAGES", index == state.selected_part_index);
|
||||
draw_row(context,
|
||||
{.x = bounds.x + 36.0F,
|
||||
.y = list_y + static_cast<std::float32_t>(row) * row_height,
|
||||
.width = row_width,
|
||||
.height = row_height},
|
||||
part.part,
|
||||
std::to_string(part.total_pages) + " PAGES",
|
||||
index == state.selected_part_index);
|
||||
}
|
||||
}
|
||||
|
||||
auto draw_practice_type_menu(luma::draw_context &context, luma::rect bounds, vamp_zero::app_state const &state) -> void {
|
||||
draw_text(context, bounds.x + 36.0F, bounds.y + 28.0F, "PRACTICE", 4, vamp_style::ink, 300.0F);
|
||||
draw_text(context, bounds.x + 38.0F, bounds.y + 66.0F, std::to_string(state.practice.types.size()) + " TYPES " + std::to_string(state.practice.exercises.size()) + " EXERCISES", 1, vamp_style::muted, 420.0F);
|
||||
|
||||
auto list_y = bounds.y + 112.0F;
|
||||
auto row_height = std::float32_t{54.0F};
|
||||
auto row_width = bounds.width - 72.0F;
|
||||
@@ -235,7 +281,14 @@ auto draw_practice_type_menu(luma::draw_context& context, luma::rect bounds, vam
|
||||
for (auto row = std::size_t{}; row < max_rows; ++row) {
|
||||
auto index = first + row;
|
||||
if (index == 0U) {
|
||||
draw_row(context, {.x = bounds.x + 36.0F, .y = list_y + static_cast<std::float32_t>(row) * row_height, .width = row_width, .height = row_height}, "BACK", "", state.selected_practice_type_index == 0U);
|
||||
draw_row(context,
|
||||
{.x = bounds.x + 36.0F,
|
||||
.y = list_y + static_cast<std::float32_t>(row) * row_height,
|
||||
.width = row_width,
|
||||
.height = row_height},
|
||||
"BACK",
|
||||
"",
|
||||
state.selected_practice_type_index == 0U);
|
||||
continue;
|
||||
}
|
||||
auto const &type = state.practice.types[index - 1U];
|
||||
@@ -243,7 +296,14 @@ auto draw_practice_type_menu(luma::draw_context& context, luma::rect bounds, vam
|
||||
for (auto const &lesson : type.lesson_groups) {
|
||||
exercise_count += lesson.exercises.size();
|
||||
}
|
||||
draw_row(context, {.x = bounds.x + 36.0F, .y = list_y + static_cast<std::float32_t>(row) * row_height, .width = row_width, .height = row_height}, type.title, std::to_string(exercise_count) + " EXERCISES", index == state.selected_practice_type_index);
|
||||
draw_row(context,
|
||||
{.x = bounds.x + 36.0F,
|
||||
.y = list_y + static_cast<std::float32_t>(row) * row_height,
|
||||
.width = row_width,
|
||||
.height = row_height},
|
||||
type.title,
|
||||
std::to_string(exercise_count) + " EXERCISES",
|
||||
index == state.selected_practice_type_index);
|
||||
}
|
||||
if (state.practice.types.empty()) {
|
||||
draw_text(context, bounds.x + 38.0F, list_y + row_height + 28.0F, "NO PRACTICE EXERCISES FOUND", 3, vamp_style::retrying, 560.0F);
|
||||
@@ -252,13 +312,18 @@ auto draw_practice_type_menu(luma::draw_context& context, luma::rect bounds, vam
|
||||
|
||||
auto draw_practice_lesson_menu(luma::draw_context &context, luma::rect bounds, vamp_zero::app_state const &state) -> void {
|
||||
auto const *type = selected_practice_type(state);
|
||||
draw_text(context, bounds.x + 36.0F, bounds.y + 28.0F, type == nullptr ? std::string{"PRACTICE"} : type->title, 4, vamp_style::ink, bounds.width - 72.0F);
|
||||
draw_text(context,
|
||||
bounds.x + 36.0F,
|
||||
bounds.y + 28.0F,
|
||||
type == nullptr ? std::string{"PRACTICE"} : type->title,
|
||||
4,
|
||||
vamp_style::ink,
|
||||
bounds.width - 72.0F);
|
||||
draw_text(context, bounds.x + 38.0F, bounds.y + 66.0F, "SELECT LESSON GROUP", 1, vamp_style::muted, 360.0F);
|
||||
if (type == nullptr) {
|
||||
draw_text(context, bounds.x + 38.0F, bounds.y + 118.0F, "NO LESSON GROUPS FOUND", 3, vamp_style::retrying, 520.0F);
|
||||
return;
|
||||
}
|
||||
|
||||
auto list_y = bounds.y + 112.0F;
|
||||
auto row_height = std::float32_t{54.0F};
|
||||
auto row_width = bounds.width - 72.0F;
|
||||
@@ -268,23 +333,42 @@ auto draw_practice_lesson_menu(luma::draw_context& context, luma::rect bounds, v
|
||||
for (auto row = std::size_t{}; row < max_rows; ++row) {
|
||||
auto index = first + row;
|
||||
if (index == 0U) {
|
||||
draw_row(context, {.x = bounds.x + 36.0F, .y = list_y + static_cast<std::float32_t>(row) * row_height, .width = row_width, .height = row_height}, "BACK", "", state.selected_practice_lesson_index == 0U);
|
||||
draw_row(context,
|
||||
{.x = bounds.x + 36.0F,
|
||||
.y = list_y + static_cast<std::float32_t>(row) * row_height,
|
||||
.width = row_width,
|
||||
.height = row_height},
|
||||
"BACK",
|
||||
"",
|
||||
state.selected_practice_lesson_index == 0U);
|
||||
continue;
|
||||
}
|
||||
auto const &lesson = type->lesson_groups[index - 1U];
|
||||
draw_row(context, {.x = bounds.x + 36.0F, .y = list_y + static_cast<std::float32_t>(row) * row_height, .width = row_width, .height = row_height}, lesson.title, std::to_string(lesson.exercises.size()) + " EXERCISES", index == state.selected_practice_lesson_index);
|
||||
draw_row(context,
|
||||
{.x = bounds.x + 36.0F,
|
||||
.y = list_y + static_cast<std::float32_t>(row) * row_height,
|
||||
.width = row_width,
|
||||
.height = row_height},
|
||||
lesson.title,
|
||||
std::to_string(lesson.exercises.size()) + " EXERCISES",
|
||||
index == state.selected_practice_lesson_index);
|
||||
}
|
||||
}
|
||||
|
||||
auto draw_practice_exercise_menu(luma::draw_context &context, luma::rect bounds, vamp_zero::app_state const &state) -> void {
|
||||
auto const *lesson = selected_practice_lesson(state);
|
||||
draw_text(context, bounds.x + 36.0F, bounds.y + 28.0F, lesson == nullptr ? std::string{"EXERCISES"} : lesson->title, 4, vamp_style::ink, bounds.width - 72.0F);
|
||||
draw_text(context,
|
||||
bounds.x + 36.0F,
|
||||
bounds.y + 28.0F,
|
||||
lesson == nullptr ? std::string{"EXERCISES"} : lesson->title,
|
||||
4,
|
||||
vamp_style::ink,
|
||||
bounds.width - 72.0F);
|
||||
draw_text(context, bounds.x + 38.0F, bounds.y + 66.0F, "SELECT EXERCISE", 1, vamp_style::muted, 320.0F);
|
||||
if (lesson == nullptr) {
|
||||
draw_text(context, bounds.x + 38.0F, bounds.y + 118.0F, "NO EXERCISES FOUND", 3, vamp_style::retrying, 440.0F);
|
||||
return;
|
||||
}
|
||||
|
||||
auto list_y = bounds.y + 112.0F;
|
||||
auto row_height = std::float32_t{54.0F};
|
||||
auto row_width = bounds.width - 72.0F;
|
||||
@@ -294,20 +378,40 @@ auto draw_practice_exercise_menu(luma::draw_context& context, luma::rect bounds,
|
||||
for (auto row = std::size_t{}; row < max_rows; ++row) {
|
||||
auto index = first + row;
|
||||
if (index == 0U) {
|
||||
draw_row(context, {.x = bounds.x + 36.0F, .y = list_y + static_cast<std::float32_t>(row) * row_height, .width = row_width, .height = row_height}, "BACK", "", state.selected_practice_exercise_index == 0U);
|
||||
draw_row(context,
|
||||
{.x = bounds.x + 36.0F,
|
||||
.y = list_y + static_cast<std::float32_t>(row) * row_height,
|
||||
.width = row_width,
|
||||
.height = row_height},
|
||||
"BACK",
|
||||
"",
|
||||
state.selected_practice_exercise_index == 0U);
|
||||
continue;
|
||||
}
|
||||
auto const &exercise = lesson->exercises[index - 1U];
|
||||
auto secondary = supports_key_selection(exercise) ? std::string{"KEY SELECT"} : "KEY " + effective_practice_key(state, exercise);
|
||||
draw_row(context, {.x = bounds.x + 36.0F, .y = list_y + static_cast<std::float32_t>(row) * row_height, .width = row_width, .height = row_height}, exercise.title, secondary, index == state.selected_practice_exercise_index);
|
||||
auto secondary =
|
||||
supports_key_selection(exercise) ? std::string{"KEY SELECT"} : "KEY " + effective_practice_key(state, exercise);
|
||||
draw_row(context,
|
||||
{.x = bounds.x + 36.0F,
|
||||
.y = list_y + static_cast<std::float32_t>(row) * row_height,
|
||||
.width = row_width,
|
||||
.height = row_height},
|
||||
exercise.title,
|
||||
secondary,
|
||||
index == state.selected_practice_exercise_index);
|
||||
}
|
||||
}
|
||||
|
||||
auto draw_practice_key_menu(luma::draw_context &context, luma::rect bounds, vamp_zero::app_state const &state) -> void {
|
||||
auto const *exercise = selected_practice_exercise(state);
|
||||
draw_text(context, bounds.x + 36.0F, bounds.y + 28.0F, exercise == nullptr ? std::string{"KEY"} : exercise->title, 4, vamp_style::ink, bounds.width - 72.0F);
|
||||
draw_text(context,
|
||||
bounds.x + 36.0F,
|
||||
bounds.y + 28.0F,
|
||||
exercise == nullptr ? std::string{"KEY"} : exercise->title,
|
||||
4,
|
||||
vamp_style::ink,
|
||||
bounds.width - 72.0F);
|
||||
draw_text(context, bounds.x + 38.0F, bounds.y + 66.0F, "SELECT PRACTICE KEY", 1, vamp_style::muted, 360.0F);
|
||||
|
||||
auto list_y = bounds.y + 112.0F;
|
||||
auto row_height = std::float32_t{54.0F};
|
||||
auto row_width = bounds.width - 72.0F;
|
||||
@@ -317,12 +421,27 @@ auto draw_practice_key_menu(luma::draw_context& context, luma::rect bounds, vamp
|
||||
for (auto row = std::size_t{}; row < max_rows; ++row) {
|
||||
auto index = first + row;
|
||||
if (index == 0U) {
|
||||
draw_row(context, {.x = bounds.x + 36.0F, .y = list_y + static_cast<std::float32_t>(row) * row_height, .width = row_width, .height = row_height}, "BACK", "", state.selected_practice_key_index == 0U);
|
||||
draw_row(context,
|
||||
{.x = bounds.x + 36.0F,
|
||||
.y = list_y + static_cast<std::float32_t>(row) * row_height,
|
||||
.width = row_width,
|
||||
.height = row_height},
|
||||
"BACK",
|
||||
"",
|
||||
state.selected_practice_key_index == 0U);
|
||||
continue;
|
||||
}
|
||||
auto key = exercise == nullptr ? std::string{"C"} : exercise->variants[index - 1U].key_signature;
|
||||
auto source = exercise == nullptr || exercise->key_signature.empty() ? std::string{"SOURCE C"} : "SOURCE " + exercise->key_signature;
|
||||
draw_row(context, {.x = bounds.x + 36.0F, .y = list_y + static_cast<std::float32_t>(row) * row_height, .width = row_width, .height = row_height}, key, source, index == state.selected_practice_key_index);
|
||||
auto source = exercise == nullptr || exercise->key_signature.empty() ? std::string{"SOURCE C"}
|
||||
: "SOURCE " + exercise->key_signature;
|
||||
draw_row(context,
|
||||
{.x = bounds.x + 36.0F,
|
||||
.y = list_y + static_cast<std::float32_t>(row) * row_height,
|
||||
.width = row_width,
|
||||
.height = row_height},
|
||||
key,
|
||||
source,
|
||||
index == state.selected_practice_key_index);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -353,7 +472,6 @@ auto draw_score(luma::draw_context& context, luma::rect bounds, vamp_zero::app_s
|
||||
source_path = variant->source_path;
|
||||
part_override = "Key: " + effective_practice_key(state, *exercise);
|
||||
}
|
||||
|
||||
auto parsed = score_cache.get(source_json, source_path, std::move(part_override));
|
||||
if (!parsed) {
|
||||
draw_text(context, bounds.x + 36.0F, bounds.y + 38.0F, "SCORE ERROR", 4, vamp_style::danger, 360.0F);
|
||||
@@ -371,7 +489,7 @@ auto draw_score(luma::draw_context& context, luma::rect bounds, vamp_zero::app_s
|
||||
|
||||
class vamp_root_control final : public luma::control {
|
||||
public:
|
||||
explicit vamp_root_control(vamp_zero::ui_state state, std::shared_ptr<vamp_zero::score_draw_cache> score_cache, touch_handler on_touch)
|
||||
explicit vamp_root_control(vamp_zero::ui_state state, std::shared_ptr<vamp_zero::score_draw_cache> score_cache, std::function<std::bool_t(luma::touch_event const &)> on_touch)
|
||||
: state_{std::move(state)}, score_cache_{std::move(score_cache)}, on_touch_{std::move(on_touch)} {}
|
||||
auto measure(luma::draw_context &, luma::theme const &, luma::constraints limits) -> luma::size override {
|
||||
return luma::clamp({.width = 1024.0F, .height = 600.0F}, limits);
|
||||
@@ -400,7 +518,8 @@ public:
|
||||
draw_practice_exercise_menu(context, content, state_.app);
|
||||
} else if (state_.app.mode == vamp_zero::app_mode::PRACTICE_KEY_MENU) {
|
||||
draw_practice_key_menu(context, content, state_.app);
|
||||
} else if (state_.app.mode == vamp_zero::app_mode::PERFORMANCE_SCORE || state_.app.mode == vamp_zero::app_mode::PRACTICE_SCORE) {
|
||||
} else if (state_.app.mode == vamp_zero::app_mode::PERFORMANCE_SCORE ||
|
||||
state_.app.mode == vamp_zero::app_mode::PRACTICE_SCORE) {
|
||||
if (score_cache_) {
|
||||
draw_score(context, content, state_.app, *score_cache_);
|
||||
}
|
||||
@@ -414,25 +533,20 @@ public:
|
||||
private:
|
||||
vamp_zero::ui_state state_{};
|
||||
std::shared_ptr<vamp_zero::score_draw_cache> score_cache_{};
|
||||
touch_handler on_touch_{};
|
||||
std::function<std::bool_t(luma::touch_event const &)> on_touch_{};
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
vamp_zero::zero_ui::zero_ui(boost::asio::any_io_executor executor, luma::application_config config, vamp_zero::zero_ui::command_handler command_handler)
|
||||
: executor_{std::move(executor)},
|
||||
tap_timer_{executor_},
|
||||
application_{std::move(config)},
|
||||
command_handler_{std::move(command_handler)},
|
||||
score_cache_{std::make_shared<vamp_zero::score_draw_cache>()} {}
|
||||
vamp_zero::zero_ui::zero_ui(boost::asio::any_io_executor executor, luma::application_config config, std::function<void(vamp_protocol::page_command)> command_handler)
|
||||
: executor_{std::move(executor)}, tap_timer_{executor_}, application_{std::move(config)},
|
||||
command_handler_{std::move(command_handler)}, score_cache_{std::make_shared<vamp_zero::score_draw_cache>()} {}
|
||||
|
||||
auto vamp_zero::zero_ui::send_touch_command(vamp_protocol::page_command command) -> void {
|
||||
if (!command_handler_) {
|
||||
return;
|
||||
}
|
||||
boost::asio::post(executor_, [handler = command_handler_, command] {
|
||||
handler(command);
|
||||
});
|
||||
boost::asio::post(executor_, [handler = command_handler_, command] { handler(command); });
|
||||
}
|
||||
|
||||
auto vamp_zero::zero_ui::cancel_pending_tap_locked() -> void {
|
||||
@@ -445,7 +559,7 @@ auto vamp_zero::zero_ui::arm_pending_tap() -> void {
|
||||
if (error) {
|
||||
return;
|
||||
}
|
||||
auto send_next = false;
|
||||
auto send_next = std::bool_t{false};
|
||||
{
|
||||
auto lock = std::scoped_lock{touch_mutex_};
|
||||
send_next = pending_tap_;
|
||||
@@ -461,10 +575,9 @@ auto vamp_zero::zero_ui::handle_touch(luma::touch_event const& event) -> std::bo
|
||||
if (!command_handler_ || event.points.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto consumed = false;
|
||||
auto consumed = std::bool_t{false};
|
||||
auto commands = std::vector<vamp_protocol::page_command>{};
|
||||
auto arm_tap = false;
|
||||
auto arm_tap = std::bool_t{false};
|
||||
{
|
||||
auto lock = std::scoped_lock{touch_mutex_};
|
||||
for (auto const &point : event.points) {
|
||||
@@ -515,7 +628,6 @@ auto vamp_zero::zero_ui::handle_touch(luma::touch_event const& event) -> std::bo
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (auto command : commands) {
|
||||
send_touch_command(command);
|
||||
}
|
||||
@@ -562,9 +674,8 @@ auto vamp_zero::zero_ui::snapshot() const -> vamp_zero::ui_state {
|
||||
}
|
||||
|
||||
auto vamp_zero::zero_ui::make_root(vamp_zero::ui_state const &state) -> std::expected<std::unique_ptr<luma::control>, modulus::error> {
|
||||
return std::make_unique<vamp_root_control>(state, score_cache_, [this](luma::touch_event const& event) {
|
||||
return handle_touch(event);
|
||||
});
|
||||
return std::make_unique<vamp_root_control>(
|
||||
state, score_cache_, [this](luma::touch_event const &event) { return handle_touch(event); });
|
||||
}
|
||||
|
||||
auto vamp_zero::make_ui_state(vamp_zero::config const &config, vamp_zero::app_state app) -> vamp_zero::ui_state {
|
||||
|
||||
+35
-11
@@ -7,14 +7,19 @@
|
||||
#include <rlgl.h>
|
||||
#include <luma.hpp>
|
||||
#include <vamp_zero/library.hpp>
|
||||
#include <vamp_zero/parse.hpp>
|
||||
#include <vamp_zero/score.hpp>
|
||||
|
||||
|
||||
namespace {
|
||||
|
||||
class score_view final : public luma::control {
|
||||
public:
|
||||
score_view(luma::application& app, vamp_zero::score_document score, std::string output, std::size_t page, bool& saved)
|
||||
score_view(luma::application &app, vamp_zero::score_document score, std::string output, std::size_t page, std::bool_t &saved)
|
||||
: app_{app}, score_{std::move(score)}, output_{std::move(output)}, page_{page}, saved_{saved} {}
|
||||
auto measure(luma::draw_context&, luma::theme const&, luma::constraints) -> luma::size override { return {976,484}; }
|
||||
auto measure(luma::draw_context &, luma::theme const &, luma::constraints) -> luma::size override {
|
||||
return {976, 484};
|
||||
}
|
||||
auto draw(luma::draw_context &context, luma::theme const &) const -> void override {
|
||||
vamp_zero::draw_score_page(context, bounds(), score_, page_);
|
||||
rlDrawRenderBatchActive();
|
||||
@@ -28,20 +33,39 @@ private:
|
||||
vamp_zero::score_document score_;
|
||||
std::string output_;
|
||||
std::size_t page_;
|
||||
bool& saved_;
|
||||
std::bool_t &saved_;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
if (argc < 3) { std::cerr << "Usage: vamp-render-score input.json output.png [page]\n"; return 2; }
|
||||
int main(int argc, std::char_t **argv) {
|
||||
if (argc < 3) {
|
||||
std::cerr << "Usage: vamp-render-score input.json output.png [page]\n";
|
||||
return 2;
|
||||
}
|
||||
auto page = std::size_t{};
|
||||
if (argc > 3) {
|
||||
auto parsed = vamp_zero::parse_integer<std::size_t>(argv[3], "page");
|
||||
if (!parsed) {
|
||||
std::cerr << parsed.error().describe() << '\n';
|
||||
return 2;
|
||||
}
|
||||
page = *parsed;
|
||||
}
|
||||
auto input = std::ifstream{argv[1]};
|
||||
auto json = std::string{std::istreambuf_iterator<char>{input},std::istreambuf_iterator<char>{}};
|
||||
auto json = std::string{std::istreambuf_iterator<std::char_t>{input}, std::istreambuf_iterator<std::char_t>{}};
|
||||
auto score = vamp_zero::parse_score_document(json, argv[1]);
|
||||
if (!score) { std::cerr << score.error().describe() << '\n'; return 1; }
|
||||
auto app = luma::application{{.title="Notation Regression",.display={.width=976,.height=484,.target_fps=30,.fullscreen=false}}};
|
||||
auto saved = false;
|
||||
auto root = app.set_root(std::make_unique<score_view>(app,std::move(*score),argv[2],argc>3 ? std::stoul(argv[3]) : 0,saved));
|
||||
if (!root) return 1;
|
||||
if (!score) {
|
||||
std::cerr << score.error().describe() << '\n';
|
||||
return 1;
|
||||
}
|
||||
auto app = luma::application{
|
||||
{.title = "Notation Regression", .display = {.width = 976, .height = 484, .target_fps = 30, .fullscreen = false}}};
|
||||
auto saved = std::bool_t{false};
|
||||
auto root = app.set_root(std::make_unique<score_view>(app, std::move(*score), argv[2], page, saved));
|
||||
if (!root) {
|
||||
return 1;
|
||||
}
|
||||
auto ran = app.run();
|
||||
return ran && saved ? 0 : 1;
|
||||
}
|
||||
|
||||
+398
-134
File diff suppressed because it is too large
Load Diff
@@ -6,7 +6,6 @@ import types
|
||||
import unittest
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
|
||||
class BuildBootstrapTests(unittest.TestCase):
|
||||
def test_requirements_fingerprint_invalidates_cached_build_image(self):
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
@@ -16,19 +15,20 @@ class BuildBootstrapTests(unittest.TestCase):
|
||||
digest = hashlib.sha256((root / 'tools/requirements.txt').read_bytes()).hexdigest()
|
||||
self.assertEqual(bootstrap.build_requirements_digest(), digest)
|
||||
self.assertIn(f'build-requirements="{digest}"', bootstrap.product_build_dockerfile())
|
||||
|
||||
package = types.ModuleType('modulus')
|
||||
cli = types.ModuleType('modulus.cli')
|
||||
package.cli = cli
|
||||
docker_run = Mock()
|
||||
cli.docker_run = docker_run
|
||||
cli.main = Mock(return_value=0)
|
||||
for command in ('clangd', 'format'):
|
||||
with patch.dict(sys.modules, {'modulus': package, 'modulus.cli': cli}), \
|
||||
patch.object(bootstrap, 'ensure_runtime') as prepare, \
|
||||
patch.object(bootstrap, 'runtime_ready', return_value=True), \
|
||||
patch.object(sys, 'path', list(sys.path)):
|
||||
self.assertEqual(bootstrap.delegate('clangd', []), 0)
|
||||
self.assertEqual(bootstrap.delegate(command, []), 0)
|
||||
prepare.assert_not_called()
|
||||
docker_run.assert_not_called()
|
||||
cli.main.assert_called_once_with('clangd')
|
||||
cli.main.assert_called_once_with(command)
|
||||
self.assertIs(cli.docker_run, docker_run)
|
||||
cli.main.reset_mock()
|
||||
|
||||
@@ -15,7 +15,6 @@ from generator.styles import BUILT_IN_STYLES, canonical_style
|
||||
from generator.transposition import concert_document, shift_pitch, transpose_document
|
||||
from tests.test_musicxml import parse, notes, xml_note, xml_score
|
||||
|
||||
|
||||
class MusicModelTests(unittest.TestCase):
|
||||
def test_chord_semantics_are_not_voicing_heuristics(self):
|
||||
cases = {'C': {0, 4, 7}, 'Cdim': {0, 3, 6}, 'Cdim7': {0, 3, 6, 9}, 'Csus2': {0, 2, 7},
|
||||
@@ -27,7 +26,6 @@ class MusicModelTests(unittest.TestCase):
|
||||
self.assertTrue(essentials.issubset({pitch_number(p) % 12 for p in pitches}), label)
|
||||
with self.assertRaises(ValueError):
|
||||
harmony.parse_symbol('Cunrecognised')
|
||||
|
||||
def test_transposition_preserves_spelling_octaves_modes(self):
|
||||
for p in ({'letter': 'b', 'alter': 1, 'octave': 4}, {'letter': 'c', 'alter': -1, 'octave': 4}, {'letter': 'f', 'alter': 2, 'octave': 4}):
|
||||
self.assertEqual(shift_pitch(p, 0, 0), p)
|
||||
@@ -36,12 +34,10 @@ class MusicModelTests(unittest.TestCase):
|
||||
result = transpose_document(source, 'D')
|
||||
self.assertEqual(result['metadata']['key_signature'], 'D minor')
|
||||
self.assertEqual(result['metadata']['key']['alterations'], key_from_fifths(-1, 'minor')['alterations'])
|
||||
|
||||
def test_additive_and_compound_grouping(self):
|
||||
self.assertEqual(beat_lengths({'groups': [{'beats': [3, 2], 'unit': 8}]}), [Fraction(3, 2), Fraction(1)])
|
||||
self.assertEqual(beat_lengths({'groups': [{'beats': [6], 'unit': 8}]}), [Fraction(3, 2)] * 2)
|
||||
self.assertEqual(beat_lengths({'groups': [{'beats': [3], 'unit': 4}]}), [Fraction(1)] * 3)
|
||||
|
||||
def test_generated_rhythm_follows_mid_measure_meter(self):
|
||||
change = '<attributes><time><beats>6</beats><beat-type>8</beat-type></time></attributes>'
|
||||
source = parse(xml_score(xml_note(duration=1, kind='quarter') + change + xml_note(duration=3, kind='half', extra='<dot/>')))
|
||||
@@ -49,7 +45,6 @@ class MusicModelTests(unittest.TestCase):
|
||||
events = generated_voice(measure, 'slash', None)
|
||||
self.assertEqual([fraction(n['onset']) for n in events], [0, 1, Fraction(5, 2)])
|
||||
self.assertEqual([fraction(n['duration']) for n in events], [1, Fraction(3, 2), Fraction(3, 2)])
|
||||
|
||||
def test_harmony_carry_uses_last_event_and_never_future_event(self):
|
||||
c = harmony.parse_symbol('C7') | {'onset': rational(0)}
|
||||
f = harmony.parse_symbol('F7') | {'onset': rational(2)}
|
||||
@@ -57,7 +52,6 @@ class MusicModelTests(unittest.TestCase):
|
||||
self.assertEqual(propagated_chords(measures), [None, f])
|
||||
self.assertIsNone(active_chord({'harmonies': [f]}, Fraction(0), None))
|
||||
self.assertEqual(active_chord(measures[0], Fraction(1), None), c)
|
||||
|
||||
def test_tuplet_ratios_and_tie_links_validated(self):
|
||||
document = parse(xml_score(xml_note()))
|
||||
notes(document)[0]['pitches'][0]['tie_to'] = 'missing'
|
||||
@@ -67,9 +61,8 @@ class MusicModelTests(unittest.TestCase):
|
||||
notes(document)[0]['duration'] = {'numerator': 8, 'denominator': 2}
|
||||
with self.assertRaisesRegex(ValueError, 'reduced'):
|
||||
validate_document(document)
|
||||
|
||||
def test_optional_turnaround_and_form(self):
|
||||
source = load_musicxml(Path('data/songs/it-could-happen-to-you.musicxml'))
|
||||
source = load_musicxml(Path('data/songs/it-could-happen-to-you.xml'))
|
||||
for part in ('acoustic', 'electric', 'vocal'):
|
||||
result = arrange_document(source, part)
|
||||
validate_document(result)
|
||||
@@ -78,20 +71,18 @@ class MusicModelTests(unittest.TestCase):
|
||||
self.assertFalse(measures[-1]['harmonies'])
|
||||
self.assertTrue(any(h for m in measures[:-1] for h in m['harmonies']))
|
||||
self.assertFalse(any('chord_optional' in h for m in measures for h in m['harmonies']))
|
||||
|
||||
def test_player_styles_preserved(self):
|
||||
self.assertEqual(len(BUILT_IN_STYLES), 50)
|
||||
for text in ('Med.', 'Med. Swing', 'Medium', 'Medium Swing'):
|
||||
self.assertEqual(canonical_style(text), 'Medium Swing')
|
||||
with self.assertRaises(ValueError):
|
||||
canonical_style('Unrecognised style')
|
||||
|
||||
def test_failure_preserves_generated_assets(self):
|
||||
with tempfile.TemporaryDirectory() as temp:
|
||||
root = Path(temp)
|
||||
data = root / 'data'
|
||||
(data / 'songs').mkdir(parents=True)
|
||||
(data/'songs'/'bad.musicxml').write_text(xml_score(xml_note(duration=5)),encoding='utf-8')
|
||||
(data / 'songs' / 'bad.xml').write_text(xml_score(xml_note(duration=5)), encoding='utf-8')
|
||||
output = root / 'output'
|
||||
output.mkdir()
|
||||
(output / 'last-good').write_text('preserved')
|
||||
@@ -99,12 +90,11 @@ class MusicModelTests(unittest.TestCase):
|
||||
with self.assertRaises(ValueError):
|
||||
generate_assets(args)
|
||||
self.assertEqual((output / 'last-good').read_text(), 'preserved')
|
||||
(data/'songs'/'bad.musicxml').write_text(xml_score(xml_note()),encoding='utf-8')
|
||||
(data / 'songs' / 'bad.xml').write_text(xml_score(xml_note()), encoding='utf-8')
|
||||
args.check = True
|
||||
with self.assertRaisesRegex(ValueError, 'summary'):
|
||||
generate_assets(args)
|
||||
self.assertEqual((output / 'last-good').read_text(), 'preserved')
|
||||
|
||||
def test_instrument_transposition_is_converted_once(self):
|
||||
source = parse(xml_score(xml_note(), attributes='<transpose><diatonic>-1</diatonic><chromatic>-2</chromatic></transpose>'))
|
||||
concert = concert_document(source)
|
||||
@@ -112,12 +102,11 @@ class MusicModelTests(unittest.TestCase):
|
||||
self.assertEqual((p['letter'], p['alter'], p['octave']), ('b', -1, 3))
|
||||
self.assertEqual(concert_document(concert), concert)
|
||||
self.assertEqual(concert['metadata']['key_signature'], 'Bb')
|
||||
|
||||
def test_deterministic_generation_and_removal(self):
|
||||
with tempfile.TemporaryDirectory() as temp:
|
||||
data, output = Path(temp) / 'data', Path(temp) / 'output'
|
||||
(data / 'songs').mkdir(parents=True)
|
||||
source = data/'songs'/'fixture.musicxml'
|
||||
source = data / 'songs' / 'fixture.xml'
|
||||
source.write_text(xml_score(xml_note()), encoding='utf-8')
|
||||
args = argparse.Namespace(data_dir=data, output_dir=output, static_assets_dir=Path('package/assets'), clean=True, check=False)
|
||||
generate_assets(args)
|
||||
@@ -127,7 +116,6 @@ class MusicModelTests(unittest.TestCase):
|
||||
source.unlink()
|
||||
generate_assets(args)
|
||||
self.assertFalse(list(output.rglob('*.json')))
|
||||
|
||||
def test_pickup_tie_survives_vocal_restoration(self):
|
||||
source = xml_score(xml_note(duration=1, kind='quarter', extra='<tie type="start"/>'), implicit=True)
|
||||
source = source.replace('</measure></part>', '</measure><measure number="2">' + xml_note(extra='<tie type="stop"/>') + '</measure></part>')
|
||||
|
||||
+13
-26
@@ -6,7 +6,6 @@ import unittest
|
||||
from generator.musicxml import MusicXMLError, load_musicxml
|
||||
from generator.model import NOTE_TYPES, TYPE_LENGTHS, fraction, pitch_number
|
||||
|
||||
|
||||
def xml_score(content, attributes='', *, divisions=1, implicit=False):
|
||||
return ('<?xml version="1.0"?>'
|
||||
'<score-partwise version="4.0"><work><work-title>Fixture</work-title></work>'
|
||||
@@ -17,36 +16,39 @@ def xml_score(content, attributes='', *, divisions=1, implicit=False):
|
||||
f'<clef><sign>G</sign><line>2</line></clef>{attributes}</attributes>'
|
||||
f'{content}</measure></part></score-partwise>')
|
||||
|
||||
|
||||
def xml_note(step='C', duration=4, kind='whole', extra='', prefix='', alter=0, octave=4):
|
||||
return (f'<note>{prefix}<pitch><step>{step}</step><alter>{alter}</alter><octave>{octave}</octave></pitch>'
|
||||
f'<duration>{duration}</duration><type>{kind}</type>{extra}</note>')
|
||||
|
||||
|
||||
def parse(text):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
path = Path(directory) / 'fixture.musicxml'
|
||||
path = Path(directory) / 'fixture.xml'
|
||||
path.write_text(text, encoding='utf-8')
|
||||
return load_musicxml(path)
|
||||
|
||||
|
||||
def notes(document, measure=0):
|
||||
return document['staves'][0]['measures'][measure]['voices'][0]['notes']
|
||||
|
||||
|
||||
class MusicXMLTests(unittest.TestCase):
|
||||
def test_measure_sound_tempo_matches_direction_sound(self):
|
||||
sound = '<sound tempo="120"/>'
|
||||
direct = parse(xml_score(sound + xml_note()))
|
||||
direction = parse(xml_score('<direction>' + sound + '</direction>' + xml_note()))
|
||||
self.assertEqual(direct, direction)
|
||||
with self.assertRaises(MusicXMLError):
|
||||
parse(xml_score('<sound tempo="0"/>' + xml_note()))
|
||||
with self.assertRaises(MusicXMLError):
|
||||
parse(xml_score('<sound dacapo="yes"/>' + xml_note()))
|
||||
def test_regression_fixture(self):
|
||||
score = load_musicxml(Path('tests/fixtures/notation.musicxml'))
|
||||
score = load_musicxml(Path('tests/fixtures/notation.xml'))
|
||||
self.assertEqual(len(score['staves'][0]['measures']), 4)
|
||||
self.assertEqual(len(score['staves'][0]['octave_shifts']), 1)
|
||||
|
||||
def test_catalogue_imports(self):
|
||||
sources = sorted(Path('data').rglob('*.musicxml'))
|
||||
sources = sorted(Path('data').rglob('*.xml'))
|
||||
self.assertEqual(len(sources), 6)
|
||||
for path in sources:
|
||||
with self.subTest(path=path):
|
||||
self.assertEqual(load_musicxml(path)['schema_version'], 2)
|
||||
|
||||
def test_all_written_values_and_dots(self):
|
||||
for kind in NOTE_TYPES:
|
||||
for dots in (0, 1, 2, 3):
|
||||
@@ -54,12 +56,10 @@ class MusicXMLTests(unittest.TestCase):
|
||||
score = parse(xml_score(xml_note(duration=value.numerator, kind=kind, extra='<dot/>' * dots), divisions=value.denominator, implicit=True))
|
||||
self.assertEqual(fraction(notes(score)[0]['duration']), value)
|
||||
self.assertEqual(notes(score)[0]['written'], {'type': kind, 'dots': dots})
|
||||
|
||||
def test_divisions_do_not_change_music(self):
|
||||
a = parse(xml_score(xml_note(duration=4)))
|
||||
b = parse(xml_score(xml_note(duration=1920), divisions=480))
|
||||
self.assertEqual(a, b)
|
||||
|
||||
def test_doctype_allowed_but_entities_rejected(self):
|
||||
source = xml_score(xml_note())
|
||||
source = source.replace('<score-partwise', '<!DOCTYPE score-partwise SYSTEM "https://invalid.test/partwise.dtd"><score-partwise', 1)
|
||||
@@ -69,13 +69,11 @@ class MusicXMLTests(unittest.TestCase):
|
||||
parse(malicious)
|
||||
with self.assertRaises(MusicXMLError):
|
||||
parse('<score-partwise')
|
||||
|
||||
def test_harmony_inside_sustained_note_remains_independent(self):
|
||||
h = '<harmony><root><root-step>F</root-step></root><kind>major</kind><offset>-2</offset></harmony>'
|
||||
score = parse(xml_score(xml_note() + h))
|
||||
self.assertEqual(len(notes(score)), 1)
|
||||
self.assertEqual(fraction(score['staves'][0]['measures'][0]['harmonies'][0]['onset']), 2)
|
||||
|
||||
def test_partial_chord_ties_and_unequal_tones(self):
|
||||
source = xml_note(duration=2, kind='half', extra='<tie type="start"/>')
|
||||
source += xml_note('E', 1, 'quarter', prefix='<chord/>')
|
||||
@@ -85,7 +83,6 @@ class MusicXMLTests(unittest.TestCase):
|
||||
self.assertEqual(fraction(result[0]['pitches'][1]['duration']), 1)
|
||||
self.assertEqual(result[0]['pitches'][0]['tie_to'], result[1]['pitches'][0]['id'])
|
||||
self.assertNotIn('tie_to', result[0]['pitches'][1])
|
||||
|
||||
def test_grace_cue_and_measured_tremolo(self):
|
||||
grace = '<note><grace slash="yes"/><pitch><step>D</step><octave>4</octave></pitch><type>16th</type></note>'
|
||||
regular = xml_note(extra='<notations><ornaments><tremolo type="single">3</tremolo></ornaments></notations>', prefix='<cue/>')
|
||||
@@ -94,7 +91,6 @@ class MusicXMLTests(unittest.TestCase):
|
||||
self.assertTrue(result[0]['grace']['slash'])
|
||||
self.assertTrue(result[1]['cue'])
|
||||
self.assertEqual(result[1]['tremolo']['marks'], 3)
|
||||
|
||||
def test_exact_quintuplets(self):
|
||||
content = ''
|
||||
for i in range(5):
|
||||
@@ -106,14 +102,12 @@ class MusicXMLTests(unittest.TestCase):
|
||||
self.assertEqual(sum(fraction(n['duration']) for n in notes(result)), 4)
|
||||
self.assertTrue(all(fraction(n['duration']) == Fraction(4, 5) for n in notes(result)))
|
||||
self.assertEqual(result['staves'][0]['tuplets'][0]['note_count'], 5)
|
||||
|
||||
def test_inconsistent_written_timing_is_rejected(self):
|
||||
modulation = '<time-modification><actual-notes>3</actual-notes><normal-notes>2</normal-notes></time-modification>'
|
||||
with self.assertRaisesRegex(MusicXMLError, 'disagrees with written duration'):
|
||||
parse(xml_score(xml_note(extra=modulation)))
|
||||
with self.assertRaisesRegex(MusicXMLError, 'disagrees with written duration'):
|
||||
parse(xml_score(xml_note(kind='half')))
|
||||
|
||||
def test_paired_tremolo_timing(self):
|
||||
modulation = '<time-modification><actual-notes>2</actual-notes><normal-notes>1</normal-notes></time-modification>'
|
||||
content = ''.join(xml_note(step, 2, 'whole', extra=modulation + f'<notations><ornaments><tremolo type="{action}">3</tremolo></ornaments></notations>')
|
||||
@@ -121,7 +115,6 @@ class MusicXMLTests(unittest.TestCase):
|
||||
result = notes(parse(xml_score(content)))
|
||||
self.assertEqual(result[0]['tremolo']['partner'], result[1]['id'])
|
||||
self.assertEqual(fraction(result[0]['duration']), 2)
|
||||
|
||||
def test_mid_measure_clef_and_key_changes(self):
|
||||
change = '<attributes><key><fifths>-3</fifths><mode>minor</mode></key><clef><sign>C</sign><line>3</line></clef></attributes>'
|
||||
result = parse(xml_score(xml_note(duration=2, kind='half') + change + xml_note(duration=2, kind='half')))
|
||||
@@ -129,7 +122,6 @@ class MusicXMLTests(unittest.TestCase):
|
||||
self.assertEqual(fraction(attr['onset']), 2)
|
||||
self.assertEqual(attr['key']['mode'], 'minor')
|
||||
self.assertEqual(attr['clef']['sign'], 'C')
|
||||
|
||||
def test_optional_marker_contract(self):
|
||||
marker = '<direction><direction-type><words>vamp:optional-chord</words></direction-type></direction>'
|
||||
h = '<harmony><root><root-step>C</root-step></root><kind>major</kind></harmony>'
|
||||
@@ -138,7 +130,6 @@ class MusicXMLTests(unittest.TestCase):
|
||||
for content in (marker + xml_note(), marker + marker + h + xml_note(), marker.replace('optional-chord', 'unknown') + xml_note()):
|
||||
with self.assertRaisesRegex(MusicXMLError, 'measure 1'):
|
||||
parse(xml_score(content))
|
||||
|
||||
def test_rejects_musical_content_outside_contract(self):
|
||||
extras = ['<notations><slur type="start"/></notations>', '<notations><articulations><accent/></articulations></notations>',
|
||||
'<voice>2</voice><staff>2</staff>', '<pitch><unpitched/></pitch>',
|
||||
@@ -146,8 +137,7 @@ class MusicXMLTests(unittest.TestCase):
|
||||
for extra in extras:
|
||||
with self.subTest(extra=extra), self.assertRaises(MusicXMLError):
|
||||
parse(xml_score(xml_note(extra=extra)))
|
||||
for extra in ('<barline><repeat direction="backward"/></barline>', '<backup><duration>4</duration></backup>',
|
||||
'<direction><direction-type><dynamics><ff/></dynamics></direction-type></direction>'):
|
||||
for extra in ('<barline><repeat direction="backward"/></barline>', '<backup><duration>4</duration></backup>', '<direction><direction-type><dynamics><ff/></dynamics></direction-type></direction>'):
|
||||
with self.subTest(extra=extra), self.assertRaises(MusicXMLError):
|
||||
parse(xml_score(xml_note() + extra))
|
||||
with self.assertRaises(MusicXMLError):
|
||||
@@ -156,13 +146,11 @@ class MusicXMLTests(unittest.TestCase):
|
||||
parse(xml_score(xml_note(extra='<tie type="stop"/>')))
|
||||
with self.assertRaises(MusicXMLError):
|
||||
parse(xml_score(xml_note(duration=5)))
|
||||
|
||||
def test_b_sharp_c_flat_preserve_octave(self):
|
||||
b = notes(parse(xml_score(xml_note('B', alter=1))))[0]['pitches'][0]
|
||||
c = notes(parse(xml_score(xml_note('C', alter=-1))))[0]['pitches'][0]
|
||||
self.assertEqual(pitch_number(b), 72)
|
||||
self.assertEqual(pitch_number(c), 59)
|
||||
|
||||
def test_musicxml_degree_alterations_are_relative(self):
|
||||
h = '<harmony><root><root-step>C</root-step></root><kind>minor</kind><degree><degree-value>3</degree-value><degree-alter>1</degree-alter><degree-type>alter</degree-type></degree></harmony>'
|
||||
from generator.harmony import degrees
|
||||
@@ -171,7 +159,6 @@ class MusicXMLTests(unittest.TestCase):
|
||||
h = h.replace('<kind>minor</kind>', '<kind text="maj7">minor</kind>')
|
||||
with self.assertRaisesRegex(MusicXMLError, 'contradictory'):
|
||||
parse(xml_score(h + xml_note()))
|
||||
|
||||
def test_nested_and_cross_bar_tuplets(self):
|
||||
modulation = '<time-modification><actual-notes>9</actual-notes><normal-notes>4</normal-notes></time-modification>'
|
||||
def tuplet(number, action):
|
||||
|
||||
@@ -9,14 +9,21 @@ from pathlib import Path
|
||||
|
||||
from generator.__main__ import copy_runtime_fonts
|
||||
from tools import generate_pico_font
|
||||
|
||||
from tools import generate_score_metrics
|
||||
|
||||
class FontAssetsTests(unittest.TestCase):
|
||||
def test_score_metrics_use_fixed_width_codepoints(self):
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
output = Path(directory) / "generated/score_metrics.hpp"
|
||||
generate_score_metrics.generate(root / "package/assets/fonts/petaluma/Petaluma.otf", output)
|
||||
content = output.read_text(encoding="ascii")
|
||||
self.assertIn("#include <cstdint>", content)
|
||||
self.assertIn("std::int32_t codepoint", content)
|
||||
def test_font_generator_requires_kerning_backend(self):
|
||||
with unittest.mock.patch.object(generate_pico_font.features, "check_feature", return_value=False):
|
||||
with self.assertRaisesRegex(RuntimeError, "Raqm"):
|
||||
generate_pico_font.generate()
|
||||
|
||||
def test_runtime_fonts_exclude_unused_assets(self):
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
@@ -24,7 +31,6 @@ class FontAssetsTests(unittest.TestCase):
|
||||
copy_runtime_fonts(root / "package/assets", output)
|
||||
files = {path.name for path in (output / "fonts/petaluma").iterdir()}
|
||||
self.assertEqual(files, {"Petaluma.otf", "PetalumaScript.otf", "OFL.txt"})
|
||||
|
||||
def test_font_generator_uses_explicit_build_output(self):
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
|
||||
@@ -6,7 +6,6 @@ from pathlib import Path
|
||||
|
||||
from PIL import Image, ImageDraw, ImageFont, features
|
||||
|
||||
|
||||
REPO = Path(__file__).resolve().parents[1]
|
||||
FONT_PATH = REPO / "package/assets/fonts/petaluma/PetalumaScript.otf"
|
||||
FONT_SIZE = 32
|
||||
@@ -15,7 +14,6 @@ FONT_MAX_WIDTH = 28
|
||||
FIRST_CODEPOINT = 32
|
||||
LAST_CODEPOINT = 126
|
||||
|
||||
|
||||
def cpp_char_literal(value: str) -> str:
|
||||
if value == "'":
|
||||
return "'\\''"
|
||||
@@ -23,21 +21,18 @@ def cpp_char_literal(value: str) -> str:
|
||||
return "'\\\\'"
|
||||
return f"'{value}'"
|
||||
|
||||
|
||||
def render_glyph(font: ImageFont.FreeTypeFont, value: str, y_offset: int) -> tuple[list[int], int, int]:
|
||||
image = Image.new("L", (FONT_MAX_WIDTH, FONT_HEIGHT), 0)
|
||||
draw = ImageDraw.Draw(image)
|
||||
bbox = font.getbbox(value)
|
||||
x = max(0, -bbox[0])
|
||||
draw.text((x, y_offset), value, font=font, fill=255)
|
||||
|
||||
pixels = [min(15, max(0, round(pixel / 17))) for pixel in image.get_flattened_data()]
|
||||
visible = image.getbbox()
|
||||
width = 0 if visible is None else min(FONT_MAX_WIDTH, visible[2])
|
||||
advance = min(255, max(0, round(font.getlength(value))))
|
||||
return pixels, width, advance
|
||||
|
||||
|
||||
def render_alpha_rows(values: list[int]) -> list[str]:
|
||||
lines = []
|
||||
for row in range(FONT_HEIGHT):
|
||||
@@ -46,7 +41,6 @@ def render_alpha_rows(values: list[int]) -> list[str]:
|
||||
lines.append(" " + ", ".join(f"0x{value:02x}" for value in row_values) + ",")
|
||||
return lines
|
||||
|
||||
|
||||
def pair_adjustments(font: ImageFont.FreeTypeFont) -> list[tuple[int, int]]:
|
||||
chars = [chr(value) for value in range(FIRST_CODEPOINT, LAST_CODEPOINT + 1)]
|
||||
adjustments = []
|
||||
@@ -60,7 +54,6 @@ def pair_adjustments(font: ImageFont.FreeTypeFont) -> list[tuple[int, int]]:
|
||||
adjustments.append((key, int(adjustment)))
|
||||
return sorted(adjustments)
|
||||
|
||||
|
||||
def generate() -> str:
|
||||
if not features.check_feature("raqm"):
|
||||
raise RuntimeError("Font generation requires Pillow with Raqm support; install libfribidi0 on Debian/Ubuntu.")
|
||||
@@ -68,7 +61,6 @@ def generate() -> str:
|
||||
min_top = min(font.getbbox(chr(value))[1] for value in range(FIRST_CODEPOINT, LAST_CODEPOINT + 1))
|
||||
y_offset = max(0, -min_top)
|
||||
adjustments = pair_adjustments(font)
|
||||
|
||||
lines = [
|
||||
"#include <vamp_style/font.hpp>",
|
||||
"",
|
||||
@@ -112,7 +104,6 @@ def generate() -> str:
|
||||
" switch (value) {",
|
||||
]
|
||||
)
|
||||
|
||||
for codepoint in range(FIRST_CODEPOINT, LAST_CODEPOINT + 1):
|
||||
value = chr(codepoint)
|
||||
alpha, width, advance = render_glyph(font, value, y_offset)
|
||||
@@ -132,7 +123,6 @@ def generate() -> str:
|
||||
" };",
|
||||
]
|
||||
)
|
||||
|
||||
lines.extend(
|
||||
[
|
||||
" default:",
|
||||
@@ -171,13 +161,11 @@ def generate() -> str:
|
||||
)
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--output", type=Path, required=True)
|
||||
parser.add_argument("--check", action="store_true")
|
||||
args = parser.parse_args()
|
||||
|
||||
generated = generate()
|
||||
if args.check:
|
||||
if not args.output.is_file():
|
||||
@@ -195,12 +183,10 @@ def main() -> int:
|
||||
)
|
||||
print("\n".join(diff))
|
||||
return 1
|
||||
|
||||
args.output.parent.mkdir(parents=True, exist_ok=True)
|
||||
args.output.write_text(generated, encoding="utf-8")
|
||||
print(f"generated {args.output}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
||||
@@ -5,7 +5,6 @@ from pathlib import Path
|
||||
from fontTools.pens.boundsPen import BoundsPen
|
||||
from fontTools.ttLib import TTFont
|
||||
|
||||
|
||||
def generate(font_path, output):
|
||||
with TTFont(font_path) as font:
|
||||
glyphs, cmap = font.getGlyphSet(), font.getBestCmap()
|
||||
@@ -24,12 +23,11 @@ def generate(font_path, output):
|
||||
box = ', '.join(f'{v / units:.8f}F' for v in pen.bounds)
|
||||
records.append(f' {{{codepoint}, {{{box}}}}},')
|
||||
output.parent.mkdir(parents=True, exist_ok=True)
|
||||
output.write_text('#pragma once\n#include <array>\nnamespace vamp_zero::font_data {\n'
|
||||
'struct glyph { int codepoint; std::array<float, 4> bounds; };\n'
|
||||
output.write_text('#pragma once\n#include <array>\n#include <cstdint>\nnamespace vamp_zero::font_data {\n'
|
||||
'struct glyph { std::int32_t codepoint; std::array<float, 4> bounds; };\n'
|
||||
f'inline constexpr std::array<glyph, {len(records)}> glyphs = {{{{\n'
|
||||
+ '\n'.join(records) + '\n}};\n}\n', encoding='ascii')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--font', type=Path, required=True)
|
||||
|
||||
Reference in New Issue
Block a user