+6
-1
@@ -1,5 +1,10 @@
|
|||||||
.git
|
.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
|
dist
|
||||||
node_modules
|
node_modules
|
||||||
*.log
|
*.log
|
||||||
|
|||||||
@@ -98,6 +98,10 @@ jobs:
|
|||||||
restore-keys: product-vamp-conan-${{ runner.os }}-
|
restore-keys: product-vamp-conan-${{ runner.os }}-
|
||||||
- name: Configure native release
|
- name: Configure native release
|
||||||
run: python3 modulus.py configure --target x86_64_gcc12_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
|
- name: Build native release
|
||||||
run: python3 modulus.py build --target x86_64_gcc12_release
|
run: python3 modulus.py build --target x86_64_gcc12_release
|
||||||
- name: Test native 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 `--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 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.
|
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 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
|
### 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. |
|
| 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. |
|
| 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. |
|
| 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.
|
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
|
### 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`.
|
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)
|
configure_file(package/manifest.json.in ${VAMP_MANIFEST} @ONLY)
|
||||||
|
|
||||||
file(GLOB_RECURSE VAMP_GENERATOR_INPUTS CONFIGURE_DEPENDS
|
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}/data/**/*.json
|
${CMAKE_CURRENT_SOURCE_DIR}/data/**/*.json
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/generator/*.py
|
${CMAKE_CURRENT_SOURCE_DIR}/generator/*.py
|
||||||
|
|||||||
+3
-13
@@ -27,8 +27,8 @@ class product_vamp_conan(conan.ConanFile):
|
|||||||
"cmake/*.cmake",
|
"cmake/*.cmake",
|
||||||
"data/*.json",
|
"data/*.json",
|
||||||
"data/**/*.json",
|
"data/**/*.json",
|
||||||
"data/*.musicxml",
|
"data/*.xml",
|
||||||
"data/**/*.musicxml",
|
"data/**/*.xml",
|
||||||
"tests/fixtures/*",
|
"tests/fixtures/*",
|
||||||
"tests/fixtures/**/*",
|
"tests/fixtures/**/*",
|
||||||
"generator/*.py",
|
"generator/*.py",
|
||||||
@@ -59,7 +59,6 @@ class product_vamp_conan(conan.ConanFile):
|
|||||||
"tools/*.py",
|
"tools/*.py",
|
||||||
"tools/requirements.txt",
|
"tools/requirements.txt",
|
||||||
)
|
)
|
||||||
|
|
||||||
def requirements(self):
|
def requirements(self):
|
||||||
self.requires(
|
self.requires(
|
||||||
str(self.python_requires["modulus"].ref),
|
str(self.python_requires["modulus"].ref),
|
||||||
@@ -77,12 +76,7 @@ class product_vamp_conan(conan.ConanFile):
|
|||||||
"luma/latest",
|
"luma/latest",
|
||||||
"sonus/latest",
|
"sonus/latest",
|
||||||
):
|
):
|
||||||
self.requires(
|
self.requires(reference, transitive_headers=True, transitive_libs=True)
|
||||||
reference,
|
|
||||||
transitive_headers=True,
|
|
||||||
transitive_libs=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
def generate(self):
|
def generate(self):
|
||||||
dependencies = conan.tools.cmake.CMakeDeps(self)
|
dependencies = conan.tools.cmake.CMakeDeps(self)
|
||||||
dependencies.generate()
|
dependencies.generate()
|
||||||
@@ -91,20 +85,16 @@ class product_vamp_conan(conan.ConanFile):
|
|||||||
toolchain.user_presets_path = False
|
toolchain.user_presets_path = False
|
||||||
toolchain.variables["BUILD_TESTING"] = bool(self.options.with_tests)
|
toolchain.variables["BUILD_TESTING"] = bool(self.options.with_tests)
|
||||||
toolchain.generate()
|
toolchain.generate()
|
||||||
|
|
||||||
def build(self):
|
def build(self):
|
||||||
cmake = conan.tools.cmake.CMake(self)
|
cmake = conan.tools.cmake.CMake(self)
|
||||||
cmake.configure()
|
cmake.configure()
|
||||||
cmake.build()
|
cmake.build()
|
||||||
if self.options.with_tests and conan.tools.build.can_run(self):
|
if self.options.with_tests and conan.tools.build.can_run(self):
|
||||||
cmake.test(target="check")
|
cmake.test(target="check")
|
||||||
|
|
||||||
def package_id(self):
|
def package_id(self):
|
||||||
self.info.options.rm_safe("with_tests")
|
self.info.options.rm_safe("with_tests")
|
||||||
|
|
||||||
def package(self):
|
def package(self):
|
||||||
cmake = conan.tools.cmake.CMake(self)
|
cmake = conan.tools.cmake.CMake(self)
|
||||||
cmake.install()
|
cmake.install()
|
||||||
|
|
||||||
def package_info(self):
|
def package_info(self):
|
||||||
self.cpp_info.bindirs = ["zero/bin"]
|
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">
|
<score-partwise version="4.0">
|
||||||
<work>
|
|
||||||
<work-title>02 The Diatonic Scales</work-title>
|
|
||||||
</work>
|
|
||||||
<identification>
|
<identification>
|
||||||
<miscellaneous>
|
<encoding>
|
||||||
<miscellaneous-field name="vamp:style">Medium Swing</miscellaneous-field>
|
<supports element="accidental" type="yes"/>
|
||||||
</miscellaneous>
|
<supports element="transpose" type="yes"/>
|
||||||
|
<software>Soundslice MusicXML exporter</software>
|
||||||
|
<encoding-date>2026-09-14</encoding-date>
|
||||||
|
</encoding>
|
||||||
</identification>
|
</identification>
|
||||||
<part-list>
|
<part-list>
|
||||||
<score-part id="P1">
|
<score-part id="P1">
|
||||||
@@ -16,7 +17,7 @@
|
|||||||
<part id="P1">
|
<part id="P1">
|
||||||
<measure number="1">
|
<measure number="1">
|
||||||
<attributes>
|
<attributes>
|
||||||
<divisions>12</divisions>
|
<divisions>8</divisions>
|
||||||
<key>
|
<key>
|
||||||
<fifths>0</fifths>
|
<fifths>0</fifths>
|
||||||
<mode>major</mode>
|
<mode>major</mode>
|
||||||
@@ -30,77 +31,78 @@
|
|||||||
<line>2</line>
|
<line>2</line>
|
||||||
</clef>
|
</clef>
|
||||||
</attributes>
|
</attributes>
|
||||||
|
<sound tempo="120"/>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>C</step>
|
<step>C</step>
|
||||||
<octave>4</octave>
|
<octave>4</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">begin</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>D</step>
|
<step>D</step>
|
||||||
<octave>4</octave>
|
<octave>4</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">continue</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>E</step>
|
<step>E</step>
|
||||||
<octave>4</octave>
|
<octave>4</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">continue</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>D</step>
|
<step>D</step>
|
||||||
<octave>4</octave>
|
<octave>4</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">end</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>E</step>
|
<step>E</step>
|
||||||
<octave>4</octave>
|
<octave>4</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">begin</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>F</step>
|
<step>F</step>
|
||||||
<octave>4</octave>
|
<octave>4</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">continue</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>E</step>
|
<step>E</step>
|
||||||
<octave>4</octave>
|
<octave>4</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">continue</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>F</step>
|
<step>F</step>
|
||||||
<octave>4</octave>
|
<octave>4</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">end</beam>
|
||||||
</note>
|
</note>
|
||||||
</measure>
|
</measure>
|
||||||
<measure number="2">
|
<measure number="2">
|
||||||
@@ -109,72 +111,72 @@
|
|||||||
<step>G</step>
|
<step>G</step>
|
||||||
<octave>4</octave>
|
<octave>4</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">begin</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>F</step>
|
<step>F</step>
|
||||||
<octave>4</octave>
|
<octave>4</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">continue</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>G</step>
|
<step>G</step>
|
||||||
<octave>4</octave>
|
<octave>4</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">continue</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>A</step>
|
<step>A</step>
|
||||||
<octave>4</octave>
|
<octave>4</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">end</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>G</step>
|
<step>G</step>
|
||||||
<octave>4</octave>
|
<octave>4</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">begin</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>A</step>
|
<step>A</step>
|
||||||
<octave>4</octave>
|
<octave>4</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">continue</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>B</step>
|
<step>B</step>
|
||||||
<octave>4</octave>
|
<octave>4</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">continue</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>A</step>
|
<step>A</step>
|
||||||
<octave>4</octave>
|
<octave>4</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">end</beam>
|
||||||
</note>
|
</note>
|
||||||
</measure>
|
</measure>
|
||||||
<measure number="3">
|
<measure number="3">
|
||||||
@@ -183,72 +185,72 @@
|
|||||||
<step>B</step>
|
<step>B</step>
|
||||||
<octave>4</octave>
|
<octave>4</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">begin</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>C</step>
|
<step>C</step>
|
||||||
<octave>5</octave>
|
<octave>5</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">continue</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>B</step>
|
<step>B</step>
|
||||||
<octave>4</octave>
|
<octave>4</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">continue</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>C</step>
|
<step>C</step>
|
||||||
<octave>5</octave>
|
<octave>5</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">end</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>D</step>
|
<step>D</step>
|
||||||
<octave>6</octave>
|
<octave>5</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">begin</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>C</step>
|
<step>C</step>
|
||||||
<octave>5</octave>
|
<octave>5</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">continue</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>D</step>
|
<step>D</step>
|
||||||
<octave>5</octave>
|
<octave>5</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">continue</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>E</step>
|
<step>E</step>
|
||||||
<octave>5</octave>
|
<octave>5</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">end</beam>
|
||||||
</note>
|
</note>
|
||||||
</measure>
|
</measure>
|
||||||
<measure number="4">
|
<measure number="4">
|
||||||
@@ -257,72 +259,72 @@
|
|||||||
<step>D</step>
|
<step>D</step>
|
||||||
<octave>5</octave>
|
<octave>5</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">begin</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>E</step>
|
<step>E</step>
|
||||||
<octave>5</octave>
|
<octave>5</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">continue</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>F</step>
|
<step>F</step>
|
||||||
<octave>5</octave>
|
<octave>5</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">continue</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>E</step>
|
<step>E</step>
|
||||||
<octave>5</octave>
|
<octave>5</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">end</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>F</step>
|
<step>F</step>
|
||||||
<octave>5</octave>
|
<octave>5</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">begin</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>G</step>
|
<step>G</step>
|
||||||
<octave>5</octave>
|
<octave>5</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">continue</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>F</step>
|
<step>F</step>
|
||||||
<octave>5</octave>
|
<octave>5</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">continue</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>G</step>
|
<step>G</step>
|
||||||
<octave>5</octave>
|
<octave>5</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">end</beam>
|
||||||
</note>
|
</note>
|
||||||
</measure>
|
</measure>
|
||||||
<measure number="5">
|
<measure number="5">
|
||||||
@@ -331,72 +333,72 @@
|
|||||||
<step>A</step>
|
<step>A</step>
|
||||||
<octave>5</octave>
|
<octave>5</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">begin</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>G</step>
|
<step>G</step>
|
||||||
<octave>5</octave>
|
<octave>5</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">continue</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>A</step>
|
<step>A</step>
|
||||||
<octave>5</octave>
|
<octave>5</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">continue</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>B</step>
|
<step>B</step>
|
||||||
<octave>5</octave>
|
<octave>5</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">end</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>A</step>
|
<step>A</step>
|
||||||
<octave>5</octave>
|
<octave>5</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">begin</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>B</step>
|
<step>B</step>
|
||||||
<octave>5</octave>
|
<octave>5</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">continue</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>C</step>
|
<step>C</step>
|
||||||
<octave>6</octave>
|
<octave>6</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">continue</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>B</step>
|
<step>B</step>
|
||||||
<octave>5</octave>
|
<octave>5</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">end</beam>
|
||||||
</note>
|
</note>
|
||||||
</measure>
|
</measure>
|
||||||
<measure number="6">
|
<measure number="6">
|
||||||
@@ -405,72 +407,72 @@
|
|||||||
<step>C</step>
|
<step>C</step>
|
||||||
<octave>6</octave>
|
<octave>6</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">begin</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>D</step>
|
<step>D</step>
|
||||||
<octave>6</octave>
|
<octave>6</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">continue</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>C</step>
|
<step>C</step>
|
||||||
<octave>6</octave>
|
<octave>6</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">continue</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>B</step>
|
<step>B</step>
|
||||||
<octave>5</octave>
|
<octave>5</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">end</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>C</step>
|
<step>C</step>
|
||||||
<octave>6</octave>
|
<octave>6</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">begin</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>B</step>
|
<step>B</step>
|
||||||
<octave>5</octave>
|
<octave>5</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">continue</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>A</step>
|
<step>A</step>
|
||||||
<octave>5</octave>
|
<octave>5</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">continue</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>B</step>
|
<step>B</step>
|
||||||
<octave>5</octave>
|
<octave>5</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">end</beam>
|
||||||
</note>
|
</note>
|
||||||
</measure>
|
</measure>
|
||||||
<measure number="7">
|
<measure number="7">
|
||||||
@@ -479,72 +481,72 @@
|
|||||||
<step>A</step>
|
<step>A</step>
|
||||||
<octave>5</octave>
|
<octave>5</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">begin</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>G</step>
|
<step>G</step>
|
||||||
<octave>5</octave>
|
<octave>5</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">continue</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>A</step>
|
<step>A</step>
|
||||||
<octave>5</octave>
|
<octave>5</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">continue</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>G</step>
|
<step>G</step>
|
||||||
<octave>5</octave>
|
<octave>5</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">end</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>F</step>
|
<step>F</step>
|
||||||
<octave>5</octave>
|
<octave>5</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">begin</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>G</step>
|
<step>G</step>
|
||||||
<octave>5</octave>
|
<octave>5</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">continue</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>F</step>
|
<step>F</step>
|
||||||
<octave>5</octave>
|
<octave>5</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">continue</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>E</step>
|
<step>E</step>
|
||||||
<octave>5</octave>
|
<octave>5</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">end</beam>
|
||||||
</note>
|
</note>
|
||||||
</measure>
|
</measure>
|
||||||
<measure number="8">
|
<measure number="8">
|
||||||
@@ -553,72 +555,72 @@
|
|||||||
<step>F</step>
|
<step>F</step>
|
||||||
<octave>5</octave>
|
<octave>5</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">begin</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>E</step>
|
<step>E</step>
|
||||||
<octave>5</octave>
|
<octave>5</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">continue</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>D</step>
|
<step>D</step>
|
||||||
<octave>5</octave>
|
<octave>5</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">continue</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>E</step>
|
<step>E</step>
|
||||||
<octave>5</octave>
|
<octave>5</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">end</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>D</step>
|
<step>D</step>
|
||||||
<octave>5</octave>
|
<octave>5</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">begin</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>C</step>
|
<step>C</step>
|
||||||
<octave>5</octave>
|
<octave>5</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">continue</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>D</step>
|
<step>D</step>
|
||||||
<octave>5</octave>
|
<octave>5</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">continue</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>C</step>
|
<step>C</step>
|
||||||
<octave>5</octave>
|
<octave>5</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">end</beam>
|
||||||
</note>
|
</note>
|
||||||
</measure>
|
</measure>
|
||||||
<measure number="9">
|
<measure number="9">
|
||||||
@@ -627,72 +629,72 @@
|
|||||||
<step>B</step>
|
<step>B</step>
|
||||||
<octave>4</octave>
|
<octave>4</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">begin</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>C</step>
|
<step>C</step>
|
||||||
<octave>5</octave>
|
<octave>5</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">continue</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>B</step>
|
<step>B</step>
|
||||||
<octave>4</octave>
|
<octave>4</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">continue</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>A</step>
|
<step>A</step>
|
||||||
<octave>4</octave>
|
<octave>4</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">end</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>G</step>
|
<step>G</step>
|
||||||
<octave>4</octave>
|
<octave>4</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">begin</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>F</step>
|
<step>F</step>
|
||||||
<octave>4</octave>
|
<octave>4</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">continue</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>E</step>
|
<step>E</step>
|
||||||
<octave>4</octave>
|
<octave>4</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">continue</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>D</step>
|
<step>D</step>
|
||||||
<octave>4</octave>
|
<octave>4</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">end</beam>
|
||||||
</note>
|
</note>
|
||||||
</measure>
|
</measure>
|
||||||
<measure number="10">
|
<measure number="10">
|
||||||
@@ -701,8 +703,7 @@
|
|||||||
<step>C</step>
|
<step>C</step>
|
||||||
<octave>4</octave>
|
<octave>4</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>48</duration>
|
<duration>32</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>whole</type>
|
<type>whole</type>
|
||||||
</note>
|
</note>
|
||||||
</measure>
|
</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">
|
<score-partwise version="4.0">
|
||||||
<work>
|
|
||||||
<work-title>03 The Diatonic Scales</work-title>
|
|
||||||
</work>
|
|
||||||
<identification>
|
<identification>
|
||||||
<miscellaneous>
|
<encoding>
|
||||||
<miscellaneous-field name="vamp:style">Medium Swing</miscellaneous-field>
|
<supports element="accidental" type="yes"/>
|
||||||
</miscellaneous>
|
<supports element="transpose" type="yes"/>
|
||||||
|
<software>Soundslice MusicXML exporter</software>
|
||||||
|
<encoding-date>2026-09-14</encoding-date>
|
||||||
|
</encoding>
|
||||||
</identification>
|
</identification>
|
||||||
<part-list>
|
<part-list>
|
||||||
<score-part id="P1">
|
<score-part id="P1">
|
||||||
@@ -16,7 +17,7 @@
|
|||||||
<part id="P1">
|
<part id="P1">
|
||||||
<measure number="1">
|
<measure number="1">
|
||||||
<attributes>
|
<attributes>
|
||||||
<divisions>12</divisions>
|
<divisions>8</divisions>
|
||||||
<key>
|
<key>
|
||||||
<fifths>0</fifths>
|
<fifths>0</fifths>
|
||||||
<mode>major</mode>
|
<mode>major</mode>
|
||||||
@@ -30,77 +31,78 @@
|
|||||||
<line>2</line>
|
<line>2</line>
|
||||||
</clef>
|
</clef>
|
||||||
</attributes>
|
</attributes>
|
||||||
|
<sound tempo="120"/>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>B</step>
|
<step>B</step>
|
||||||
<octave>3</octave>
|
<octave>3</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">begin</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>C</step>
|
<step>C</step>
|
||||||
<octave>4</octave>
|
<octave>4</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">continue</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>D</step>
|
<step>D</step>
|
||||||
<octave>4</octave>
|
<octave>4</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">continue</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>E</step>
|
<step>E</step>
|
||||||
<octave>4</octave>
|
<octave>4</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">end</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>F</step>
|
<step>F</step>
|
||||||
<octave>4</octave>
|
<octave>4</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">begin</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>E</step>
|
<step>E</step>
|
||||||
<octave>4</octave>
|
<octave>4</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">continue</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>F</step>
|
<step>F</step>
|
||||||
<octave>4</octave>
|
<octave>4</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">continue</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>G</step>
|
<step>G</step>
|
||||||
<octave>4</octave>
|
<octave>4</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">end</beam>
|
||||||
</note>
|
</note>
|
||||||
</measure>
|
</measure>
|
||||||
<measure number="2">
|
<measure number="2">
|
||||||
@@ -109,72 +111,72 @@
|
|||||||
<step>F</step>
|
<step>F</step>
|
||||||
<octave>4</octave>
|
<octave>4</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">begin</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>E</step>
|
<step>E</step>
|
||||||
<octave>4</octave>
|
<octave>4</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">continue</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>F</step>
|
<step>F</step>
|
||||||
<octave>4</octave>
|
<octave>4</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">continue</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>G</step>
|
<step>G</step>
|
||||||
<octave>4</octave>
|
<octave>4</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">end</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>A</step>
|
<step>A</step>
|
||||||
<octave>4</octave>
|
<octave>4</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">begin</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>B</step>
|
<step>B</step>
|
||||||
<octave>4</octave>
|
<octave>4</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">continue</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>C</step>
|
<step>C</step>
|
||||||
<octave>5</octave>
|
<octave>5</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">continue</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>B</step>
|
<step>B</step>
|
||||||
<octave>4</octave>
|
<octave>4</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">end</beam>
|
||||||
</note>
|
</note>
|
||||||
</measure>
|
</measure>
|
||||||
<measure number="3">
|
<measure number="3">
|
||||||
@@ -183,72 +185,72 @@
|
|||||||
<step>A</step>
|
<step>A</step>
|
||||||
<octave>4</octave>
|
<octave>4</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">begin</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>B</step>
|
<step>B</step>
|
||||||
<octave>4</octave>
|
<octave>4</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">continue</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>C</step>
|
<step>C</step>
|
||||||
<octave>5</octave>
|
<octave>5</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">continue</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>D</step>
|
<step>D</step>
|
||||||
<octave>5</octave>
|
<octave>5</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">end</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>E</step>
|
<step>E</step>
|
||||||
<octave>5</octave>
|
<octave>5</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">begin</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>F</step>
|
<step>F</step>
|
||||||
<octave>5</octave>
|
<octave>5</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">continue</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>E</step>
|
<step>E</step>
|
||||||
<octave>5</octave>
|
<octave>5</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">continue</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>D</step>
|
<step>D</step>
|
||||||
<octave>5</octave>
|
<octave>5</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">end</beam>
|
||||||
</note>
|
</note>
|
||||||
</measure>
|
</measure>
|
||||||
<measure number="4">
|
<measure number="4">
|
||||||
@@ -257,72 +259,72 @@
|
|||||||
<step>C</step>
|
<step>C</step>
|
||||||
<octave>5</octave>
|
<octave>5</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">begin</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>B</step>
|
<step>B</step>
|
||||||
<octave>4</octave>
|
<octave>4</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">continue</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>C</step>
|
<step>C</step>
|
||||||
<octave>5</octave>
|
<octave>5</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">continue</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>D</step>
|
<step>D</step>
|
||||||
<octave>5</octave>
|
<octave>5</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">end</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>E</step>
|
<step>E</step>
|
||||||
<octave>5</octave>
|
<octave>5</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">begin</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>F</step>
|
<step>F</step>
|
||||||
<octave>5</octave>
|
<octave>5</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">continue</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>G</step>
|
<step>G</step>
|
||||||
<octave>5</octave>
|
<octave>5</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">continue</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>A</step>
|
<step>A</step>
|
||||||
<octave>5</octave>
|
<octave>5</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">end</beam>
|
||||||
</note>
|
</note>
|
||||||
</measure>
|
</measure>
|
||||||
<measure number="5">
|
<measure number="5">
|
||||||
@@ -331,72 +333,72 @@
|
|||||||
<step>B</step>
|
<step>B</step>
|
||||||
<octave>5</octave>
|
<octave>5</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">begin</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>C</step>
|
<step>C</step>
|
||||||
<octave>6</octave>
|
<octave>6</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">continue</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>B</step>
|
<step>B</step>
|
||||||
<octave>5</octave>
|
<octave>5</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">continue</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>A</step>
|
<step>A</step>
|
||||||
<octave>5</octave>
|
<octave>5</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">end</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>G</step>
|
<step>G</step>
|
||||||
<octave>5</octave>
|
<octave>5</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">begin</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>A</step>
|
<step>A</step>
|
||||||
<octave>5</octave>
|
<octave>5</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">continue</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>B</step>
|
<step>B</step>
|
||||||
<octave>5</octave>
|
<octave>5</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">continue</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>C</step>
|
<step>C</step>
|
||||||
<octave>6</octave>
|
<octave>6</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">end</beam>
|
||||||
</note>
|
</note>
|
||||||
</measure>
|
</measure>
|
||||||
<measure number="6">
|
<measure number="6">
|
||||||
@@ -405,72 +407,72 @@
|
|||||||
<step>D</step>
|
<step>D</step>
|
||||||
<octave>6</octave>
|
<octave>6</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">begin</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>E</step>
|
<step>E</step>
|
||||||
<octave>6</octave>
|
<octave>6</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">continue</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>D</step>
|
<step>D</step>
|
||||||
<octave>6</octave>
|
<octave>6</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">continue</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>C</step>
|
<step>C</step>
|
||||||
<octave>6</octave>
|
<octave>6</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">end</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>B</step>
|
<step>B</step>
|
||||||
<octave>5</octave>
|
<octave>5</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">begin</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>C</step>
|
<step>C</step>
|
||||||
<octave>6</octave>
|
<octave>6</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">continue</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>B</step>
|
<step>B</step>
|
||||||
<octave>5</octave>
|
<octave>5</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">continue</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>A</step>
|
<step>A</step>
|
||||||
<octave>5</octave>
|
<octave>5</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">end</beam>
|
||||||
</note>
|
</note>
|
||||||
</measure>
|
</measure>
|
||||||
<measure number="7">
|
<measure number="7">
|
||||||
@@ -479,72 +481,72 @@
|
|||||||
<step>G</step>
|
<step>G</step>
|
||||||
<octave>5</octave>
|
<octave>5</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">begin</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>A</step>
|
<step>A</step>
|
||||||
<octave>5</octave>
|
<octave>5</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">continue</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>B</step>
|
<step>B</step>
|
||||||
<octave>5</octave>
|
<octave>5</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">continue</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>A</step>
|
<step>A</step>
|
||||||
<octave>5</octave>
|
<octave>5</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">end</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>G</step>
|
<step>G</step>
|
||||||
<octave>5</octave>
|
<octave>5</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">begin</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>F</step>
|
<step>F</step>
|
||||||
<octave>5</octave>
|
<octave>5</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">continue</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>E</step>
|
<step>E</step>
|
||||||
<octave>5</octave>
|
<octave>5</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">continue</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>F</step>
|
<step>F</step>
|
||||||
<octave>5</octave>
|
<octave>5</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">end</beam>
|
||||||
</note>
|
</note>
|
||||||
</measure>
|
</measure>
|
||||||
<measure number="8">
|
<measure number="8">
|
||||||
@@ -553,72 +555,72 @@
|
|||||||
<step>G</step>
|
<step>G</step>
|
||||||
<octave>5</octave>
|
<octave>5</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">begin</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>F</step>
|
<step>F</step>
|
||||||
<octave>5</octave>
|
<octave>5</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">continue</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>E</step>
|
<step>E</step>
|
||||||
<octave>5</octave>
|
<octave>5</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">continue</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>D</step>
|
<step>D</step>
|
||||||
<octave>5</octave>
|
<octave>5</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">end</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>E</step>
|
<step>E</step>
|
||||||
<octave>5</octave>
|
<octave>5</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">begin</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>F</step>
|
<step>F</step>
|
||||||
<octave>5</octave>
|
<octave>5</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">continue</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>E</step>
|
<step>E</step>
|
||||||
<octave>5</octave>
|
<octave>5</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">continue</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>D</step>
|
<step>D</step>
|
||||||
<octave>5</octave>
|
<octave>5</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">end</beam>
|
||||||
</note>
|
</note>
|
||||||
</measure>
|
</measure>
|
||||||
<measure number="9">
|
<measure number="9">
|
||||||
@@ -627,72 +629,72 @@
|
|||||||
<step>C</step>
|
<step>C</step>
|
||||||
<octave>5</octave>
|
<octave>5</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">begin</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>B</step>
|
<step>B</step>
|
||||||
<octave>4</octave>
|
<octave>4</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">continue</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>A</step>
|
<step>A</step>
|
||||||
<octave>4</octave>
|
<octave>4</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">continue</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>G</step>
|
<step>G</step>
|
||||||
<octave>4</octave>
|
<octave>4</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">end</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>A</step>
|
<step>A</step>
|
||||||
<octave>4</octave>
|
<octave>4</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">begin</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>B</step>
|
<step>B</step>
|
||||||
<octave>4</octave>
|
<octave>4</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">continue</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>A</step>
|
<step>A</step>
|
||||||
<octave>4</octave>
|
<octave>4</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">continue</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>G</step>
|
<step>G</step>
|
||||||
<octave>4</octave>
|
<octave>4</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">end</beam>
|
||||||
</note>
|
</note>
|
||||||
</measure>
|
</measure>
|
||||||
<measure number="10">
|
<measure number="10">
|
||||||
@@ -701,72 +703,72 @@
|
|||||||
<step>F</step>
|
<step>F</step>
|
||||||
<octave>4</octave>
|
<octave>4</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">begin</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>E</step>
|
<step>E</step>
|
||||||
<octave>4</octave>
|
<octave>4</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">continue</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>D</step>
|
<step>D</step>
|
||||||
<octave>4</octave>
|
<octave>4</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">continue</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>C</step>
|
<step>C</step>
|
||||||
<octave>4</octave>
|
<octave>4</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">end</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>B</step>
|
<step>B</step>
|
||||||
<octave>3</octave>
|
<octave>3</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">begin</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>C</step>
|
<step>C</step>
|
||||||
<octave>4</octave>
|
<octave>4</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">continue</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>D</step>
|
<step>D</step>
|
||||||
<octave>4</octave>
|
<octave>4</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">continue</beam>
|
||||||
</note>
|
</note>
|
||||||
<note>
|
<note>
|
||||||
<pitch>
|
<pitch>
|
||||||
<step>E</step>
|
<step>E</step>
|
||||||
<octave>4</octave>
|
<octave>4</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>6</duration>
|
<duration>4</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>eighth</type>
|
<type>eighth</type>
|
||||||
|
<beam number="1">end</beam>
|
||||||
</note>
|
</note>
|
||||||
</measure>
|
</measure>
|
||||||
<measure number="11">
|
<measure number="11">
|
||||||
@@ -775,8 +777,7 @@
|
|||||||
<step>F</step>
|
<step>F</step>
|
||||||
<octave>4</octave>
|
<octave>4</octave>
|
||||||
</pitch>
|
</pitch>
|
||||||
<duration>48</duration>
|
<duration>32</duration>
|
||||||
<voice>1</voice>
|
|
||||||
<type>whole</type>
|
<type>whole</type>
|
||||||
</note>
|
</note>
|
||||||
</measure>
|
</measure>
|
||||||
@@ -1,2 +1 @@
|
|||||||
"""Build-time score generation for Product Vamp."""
|
"""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")
|
RUNTIME_FONT_FILES = ("Petaluma.otf", "PetalumaScript.otf", "OFL.txt")
|
||||||
|
|
||||||
|
|
||||||
def copy_runtime_fonts(static_assets_dir: Path, output_dir: Path) -> None:
|
def copy_runtime_fonts(static_assets_dir: Path, output_dir: Path) -> None:
|
||||||
source = static_assets_dir / "fonts" / "petaluma"
|
source = static_assets_dir / "fonts" / "petaluma"
|
||||||
destination = output_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:
|
for name in RUNTIME_FONT_FILES:
|
||||||
shutil.copy2(source / name, destination / name)
|
shutil.copy2(source / name, destination / name)
|
||||||
|
|
||||||
|
|
||||||
def parse_args() -> argparse.Namespace:
|
def parse_args() -> argparse.Namespace:
|
||||||
parser = argparse.ArgumentParser(description="Generate Product Vamp package score assets.")
|
parser = argparse.ArgumentParser(description="Generate Product Vamp package score assets.")
|
||||||
parser.add_argument("--data-dir", type=Path, required=True)
|
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")
|
parser.add_argument("--check", action="store_true")
|
||||||
return parser.parse_args()
|
return parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
def generate_assets(args: argparse.Namespace) -> None:
|
def generate_assets(args: argparse.Namespace) -> None:
|
||||||
data_dir = args.data_dir.resolve()
|
data_dir = args.data_dir.resolve()
|
||||||
output_dir = args.output_dir.resolve()
|
output_dir = args.output_dir.resolve()
|
||||||
static_assets_dir = args.static_assets_dir.resolve()
|
static_assets_dir = args.static_assets_dir.resolve()
|
||||||
|
for source in sorted(data_dir.rglob('*.xml')):
|
||||||
for source in sorted(data_dir.rglob('*.musicxml')):
|
|
||||||
load_musicxml(source)
|
load_musicxml(source)
|
||||||
|
|
||||||
output_dir.parent.mkdir(parents=True, exist_ok=True)
|
output_dir.parent.mkdir(parents=True, exist_ok=True)
|
||||||
with tempfile.TemporaryDirectory(prefix='.vamp-assets-', dir=output_dir.parent) as temporary:
|
with tempfile.TemporaryDirectory(prefix='.vamp-assets-', dir=output_dir.parent) as temporary:
|
||||||
staged = Path(temporary) / 'assets'
|
staged = Path(temporary) / 'assets'
|
||||||
@@ -63,7 +58,7 @@ def generate_assets(args: argparse.Namespace) -> None:
|
|||||||
backup.rename(output_dir)
|
backup.rename(output_dir)
|
||||||
raise
|
raise
|
||||||
print(f'Generated Product Vamp assets: {performance_count} performance scores, '
|
print(f'Generated Product Vamp assets: {performance_count} performance scores, '
|
||||||
f'{practice_count} practice score variants, {count_json_files(output_dir)} total JSON files.')
|
f'{practice_count} practice score variants, {count_json_files(output_dir)} total JSON files.')
|
||||||
if args.check:
|
if args.check:
|
||||||
print('Generator check passed.')
|
print('Generator check passed.')
|
||||||
|
|
||||||
@@ -71,6 +66,5 @@ def main() -> int:
|
|||||||
generate_assets(parse_args())
|
generate_assets(parse_args())
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
raise SystemExit(main())
|
raise SystemExit(main())
|
||||||
|
|||||||
@@ -1,2 +1 @@
|
|||||||
"""Performance arrangement generation."""
|
"""Performance arrangement generation."""
|
||||||
|
|
||||||
|
|||||||
@@ -5,17 +5,13 @@ from typing import Literal
|
|||||||
|
|
||||||
from generator.score import body_start_measure_index
|
from generator.score import body_start_measure_index
|
||||||
|
|
||||||
SectionRole = Literal["rhythm", "accompaniment", "melody", "vocal", "rest", "slash"]
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class ArrangementSection:
|
class ArrangementSection:
|
||||||
label: str
|
label: str
|
||||||
source_measure_index: int
|
source_measure_index: int
|
||||||
measure_count: int
|
measure_count: int
|
||||||
section_index: 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:
|
def phrase_aware_edge_measure_count(body_measure_count: int) -> int:
|
||||||
if body_measure_count >= 8:
|
if body_measure_count >= 8:
|
||||||
@@ -24,8 +20,7 @@ def phrase_aware_edge_measure_count(body_measure_count: int) -> int:
|
|||||||
return 4
|
return 4
|
||||||
return body_measure_count
|
return body_measure_count
|
||||||
|
|
||||||
|
def section_role(part_type: str, section_index: int) -> Literal["rhythm", "accompaniment", "melody", "vocal", "rest", "slash"]:
|
||||||
def section_role(part_type: str, section_index: int) -> SectionRole:
|
|
||||||
if part_type == "acoustic":
|
if part_type == "acoustic":
|
||||||
return "slash" if section_index == 3 else "rhythm"
|
return "slash" if section_index == 3 else "rhythm"
|
||||||
if part_type == "electric":
|
if part_type == "electric":
|
||||||
@@ -38,7 +33,6 @@ def section_role(part_type: str, section_index: int) -> SectionRole:
|
|||||||
return "rest"
|
return "rest"
|
||||||
return "vocal" if section_index in {1, 5, 6} else "rest"
|
return "vocal" if section_index in {1, 5, 6} else "rest"
|
||||||
|
|
||||||
|
|
||||||
def full_arrangement_sections(measures: list[dict], part_type: str) -> list[ArrangementSection]:
|
def full_arrangement_sections(measures: list[dict], part_type: str) -> list[ArrangementSection]:
|
||||||
if not measures:
|
if not measures:
|
||||||
return []
|
return []
|
||||||
@@ -62,4 +56,3 @@ def full_arrangement_sections(measures: list[dict], part_type: str) -> list[Arra
|
|||||||
for label, start, count, index in specs
|
for label, start, count, index in specs
|
||||||
if count > 0
|
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))
|
STANDARD_TUNING = ((6, 40), (5, 45), (4, 50), (3, 55), (2, 59), (1, 64))
|
||||||
WRITTEN_GUITAR_OCTAVE_OFFSET = 12
|
WRITTEN_GUITAR_OCTAVE_OFFSET = 12
|
||||||
|
|
||||||
|
|
||||||
class TabPosition(NamedTuple):
|
class TabPosition(NamedTuple):
|
||||||
string_number: int
|
string_number: int
|
||||||
fret: int
|
fret: int
|
||||||
midi: int
|
midi: int
|
||||||
|
|
||||||
|
|
||||||
def chord_pitch_classes(symbol: str | dict) -> tuple[int, ...]:
|
def chord_pitch_classes(symbol: str | dict) -> tuple[int, ...]:
|
||||||
return harmony.pitch_classes(harmony.parse_symbol(symbol) if isinstance(symbol, str) else symbol)
|
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]:
|
def positions_for_string(string_number: int, open_midi: int, pitch_classes: tuple[int, ...]) -> list[TabPosition]:
|
||||||
result: list[TabPosition] = []
|
result: list[TabPosition] = []
|
||||||
for fret in range(0, 13):
|
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))
|
result.append(TabPosition(string_number, fret, midi))
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
def candidate_score(candidate: tuple[TabPosition, ...], pitch_classes: tuple[int, ...]) -> int:
|
def candidate_score(candidate: tuple[TabPosition, ...], pitch_classes: tuple[int, ...]) -> int:
|
||||||
fretted = [position.fret for position in candidate if position.fret > 0]
|
fretted = [position.fret for position in candidate if position.fret > 0]
|
||||||
if not fretted:
|
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)
|
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)
|
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]:
|
def find_playable_voicing(symbol: str | dict) -> list[TabPosition]:
|
||||||
definition = harmony.parse_symbol(symbol) if isinstance(symbol, str) else symbol
|
definition = harmony.parse_symbol(symbol) if isinstance(symbol, str) else symbol
|
||||||
semantic_degrees = harmony.degrees(definition)
|
semantic_degrees = harmony.degrees(definition)
|
||||||
@@ -54,16 +49,15 @@ def find_playable_voicing(symbol: str | dict) -> list[TabPosition]:
|
|||||||
for degree in (11, 9, 5):
|
for degree in (11, 9, 5):
|
||||||
if len(voicing) > 4 and voicing.get(degree) == 0:
|
if len(voicing) > 4 and voicing.get(degree) == 0:
|
||||||
del voicing[degree]
|
del voicing[degree]
|
||||||
root = definition.get('root', {'letter':'c','alter':0})
|
root = definition.get('root', {'letter': 'c', 'alter': 0})
|
||||||
root_pc = NATURAL[root['letter']] + root['alter']
|
root_pc = NATURAL[root['letter']] + root['alter']
|
||||||
pitch_classes = tuple(dict.fromkeys((root_pc + harmony.degree_semitones(d,a)) % 12 for d,a in voicing.items()))
|
pitch_classes = tuple(dict.fromkeys((root_pc + harmony.degree_semitones(d, a)) % 12 for d, a in voicing.items()))
|
||||||
bass = definition.get('bass')
|
bass = definition.get('bass')
|
||||||
bass_pc = (NATURAL[bass['letter']] + bass['alter']) % 12 if bass else None
|
bass_pc = (NATURAL[bass['letter']] + bass['alter']) % 12 if bass else None
|
||||||
if bass_pc is not None and bass_pc not in pitch_classes:
|
if bass_pc is not None and bass_pc not in pitch_classes:
|
||||||
pitch_classes += (bass_pc,)
|
pitch_classes += (bass_pc,)
|
||||||
if not pitch_classes:
|
if not pitch_classes:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
string_sets = ((6, 4, 3, 2), (5, 4, 3, 2), (4, 3, 2, 1), (6, 5, 4, 3), (5, 3, 2, 1))
|
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}
|
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
|
best: tuple[TabPosition, ...] | None = None
|
||||||
@@ -73,7 +67,7 @@ def find_playable_voicing(symbol: str | dict) -> list[TabPosition]:
|
|||||||
if any(not item for item in options):
|
if any(not item for item in options):
|
||||||
continue
|
continue
|
||||||
for candidate in product(*options):
|
for candidate in product(*options):
|
||||||
if bass_pc is not None and min(candidate, key=lambda p:p.midi).midi % 12 != bass_pc:
|
if bass_pc is not None and min(candidate, key=lambda p: p.midi).midi % 12 != bass_pc:
|
||||||
continue
|
continue
|
||||||
fretted = [position.fret for position in candidate if position.fret > 0]
|
fretted = [position.fret for position in candidate if position.fret > 0]
|
||||||
if fretted and max(fretted) - min(fretted) > 4:
|
if fretted and max(fretted) - min(fretted) > 4:
|
||||||
@@ -87,17 +81,16 @@ def find_playable_voicing(symbol: str | dict) -> list[TabPosition]:
|
|||||||
best_score = score
|
best_score = score
|
||||||
return sorted(best or [], key=lambda item: item.midi)
|
return sorted(best or [], key=lambda item: item.midi)
|
||||||
|
|
||||||
|
|
||||||
def pitches_for_voicing(symbol: str | dict) -> list[dict[str, int | str]]:
|
def pitches_for_voicing(symbol: str | dict) -> list[dict[str, int | str]]:
|
||||||
definition = harmony.parse_symbol(symbol) if isinstance(symbol, str) else symbol
|
definition = harmony.parse_symbol(symbol) if isinstance(symbol, str) else symbol
|
||||||
from generator.transposition import shift_pitch
|
from generator.transposition import shift_pitch
|
||||||
root = definition.get('root', {'letter':'c', 'alter':0})
|
root = definition.get('root', {'letter': 'c', 'alter': 0})
|
||||||
spellings = {}
|
spellings = {}
|
||||||
for degree, alter in harmony.degrees(definition).items():
|
for degree, alter in harmony.degrees(definition).items():
|
||||||
p = shift_pitch(root, degree-1, harmony.degree_semitones(degree, alter))
|
p = shift_pitch(root, degree - 1, harmony.degree_semitones(degree, alter))
|
||||||
spellings[(NATURAL[p['letter']]+p['alter']) % 12] = p
|
spellings[(NATURAL[p['letter']] + p['alter']) % 12] = p
|
||||||
if bass := definition.get('bass'):
|
if bass := definition.get('bass'):
|
||||||
spellings[(NATURAL[bass['letter']]+bass['alter']) % 12] = bass
|
spellings[(NATURAL[bass['letter']] + bass['alter']) % 12] = bass
|
||||||
result = []
|
result = []
|
||||||
for position in find_playable_voicing(definition):
|
for position in find_playable_voicing(definition):
|
||||||
midi = position.midi + WRITTEN_GUITAR_OCTAVE_OFFSET
|
midi = position.midi + WRITTEN_GUITAR_OCTAVE_OFFSET
|
||||||
|
|||||||
@@ -12,10 +12,9 @@ from generator.musicxml import load_musicxml
|
|||||||
from generator.transposition import concert_document, shift_pitch
|
from generator.transposition import concert_document, shift_pitch
|
||||||
from generator.score import copy_source_voice, make_arranged_document, make_measure, measure_notes, reidentify, slugify, source_staff, write_json
|
from generator.score import copy_source_voice, make_arranged_document, make_measure, measure_notes, reidentify, slugify, source_staff, write_json
|
||||||
|
|
||||||
PART_TYPES = ('acoustic','electric','vocal')
|
PART_TYPES = ('acoustic', 'electric', 'vocal')
|
||||||
FINAL_SECTION_INDEX = 6
|
FINAL_SECTION_INDEX = 6
|
||||||
|
|
||||||
|
|
||||||
def propagated_chords(measures: list[dict], *, include_optional: bool = True) -> list[dict | None]:
|
def propagated_chords(measures: list[dict], *, include_optional: bool = True) -> list[dict | None]:
|
||||||
result = []
|
result = []
|
||||||
current = None
|
current = None
|
||||||
@@ -26,7 +25,6 @@ def propagated_chords(measures: list[dict], *, include_optional: bool = True) ->
|
|||||||
current = h
|
current = h
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
def active_chord(measure: dict, onset: Fraction, previous: dict | None) -> dict | None:
|
def active_chord(measure: dict, onset: Fraction, previous: dict | None) -> dict | None:
|
||||||
active = previous
|
active = previous
|
||||||
for h in measure['harmonies']:
|
for h in measure['harmonies']:
|
||||||
@@ -35,26 +33,25 @@ def active_chord(measure: dict, onset: Fraction, previous: dict | None) -> dict
|
|||||||
active = h
|
active = h
|
||||||
return active if active and active['kind'] != 'none' else None
|
return active if active and active['kind'] != 'none' else None
|
||||||
|
|
||||||
|
|
||||||
def generated_voice(measure: dict, role: str, previous: dict | None) -> list[dict]:
|
def generated_voice(measure: dict, role: str, previous: dict | None) -> list[dict]:
|
||||||
length = fraction(measure['duration'])
|
length = fraction(measure['duration'])
|
||||||
if role == 'rest':
|
if role == 'rest':
|
||||||
return [make_event('rest', Fraction(), length, 'rest', written={'type':'whole','dots':0}) | {'measure_rest':True}]
|
return [make_event('rest', Fraction(), length, 'rest', written={'type': 'whole', 'dots': 0}) | {'measure_rest': True}]
|
||||||
if role == 'rhythm' and measure['time_signature']=='4/4' and not measure.get('attributes'):
|
if role == 'rhythm' and measure['time_signature'] == '4/4' and not measure.get('attributes'):
|
||||||
attacks = [(Fraction(n,2), Fraction(1,2)) for n in (0,3,4,5,7)]
|
attacks = [(Fraction(n, 2), Fraction(1, 2)) for n in (0, 3, 4, 5, 7)]
|
||||||
elif role == 'accompaniment' and measure['time_signature']=='4/4' and not measure.get('attributes'):
|
elif role == 'accompaniment' and measure['time_signature'] == '4/4' and not measure.get('attributes'):
|
||||||
attacks = [(Fraction(1,2),Fraction(1,2))]
|
attacks = [(Fraction(1, 2), Fraction(1, 2))]
|
||||||
else:
|
else:
|
||||||
attacks = []
|
attacks = []
|
||||||
states = [(Fraction(), measure['meter'])]
|
states = [(Fraction(), measure['meter'])]
|
||||||
states.extend((fraction(a['onset']), a['meter']) for a in measure.get('attributes', []) if 'meter' in a)
|
states.extend((fraction(a['onset']), a['meter']) for a in measure.get('attributes', []) if 'meter' in a)
|
||||||
for index, (onset, meter) in enumerate(states):
|
for index, (onset, meter) in enumerate(states):
|
||||||
end = states[index+1][0] if index+1 < len(states) else length
|
end = states[index + 1][0] if index + 1 < len(states) else length
|
||||||
beats = beat_lengths(meter)
|
beats = beat_lengths(meter)
|
||||||
beat_index = 0
|
beat_index = 0
|
||||||
while onset < end:
|
while onset < end:
|
||||||
beat = min(beats[beat_index % len(beats)] if beats else end-onset, end-onset)
|
beat = min(beats[beat_index % len(beats)] if beats else end - onset, end - onset)
|
||||||
duration = beat if role == 'slash' or not beats else min(beat/2, Fraction(1,2))
|
duration = beat if role == 'slash' or not beats else min(beat / 2, Fraction(1, 2))
|
||||||
attacks.append((onset, duration))
|
attacks.append((onset, duration))
|
||||||
onset += beat
|
onset += beat
|
||||||
beat_index += 1
|
beat_index += 1
|
||||||
@@ -64,35 +61,34 @@ def generated_voice(measure: dict, role: str, previous: dict | None) -> list[dic
|
|||||||
if onset >= length:
|
if onset >= length:
|
||||||
break
|
break
|
||||||
if onset > cursor:
|
if onset > cursor:
|
||||||
events.append(make_event(f'rest-{len(events)}',cursor,onset-cursor,'rest'))
|
events.append(make_event(f'rest-{len(events)}', cursor, onset - cursor, 'rest'))
|
||||||
duration = min(duration,length-onset)
|
duration = min(duration, length - onset)
|
||||||
chord = active_chord(measure,onset,previous)
|
chord = active_chord(measure, onset, previous)
|
||||||
source_attack = any(fraction(n['onset'])==onset and n['kind']=='note' and not n.get('grace') and any(not p.get('tie_from') for p in n['pitches']) for n in measure_notes(measure))
|
source_attack = any(fraction(n['onset']) == onset and n['kind'] == 'note' and not n.get('grace') and any(not p.get('tie_from') for p in n['pitches']) for n in measure_notes(measure))
|
||||||
if role=='slash':
|
if role == 'slash':
|
||||||
event = make_event(f'slash-{len(events)}',onset,duration,'note')
|
event = make_event(f'slash-{len(events)}', onset, duration, 'note')
|
||||||
event['notehead'] = 'slash'
|
event['notehead'] = 'slash'
|
||||||
elif not chord or role=='accompaniment' and source_attack:
|
elif not chord or role == 'accompaniment' and source_attack:
|
||||||
event = make_event(f'rest-{len(events)}',onset,duration,'rest')
|
event = make_event(f'rest-{len(events)}', onset, duration, 'rest')
|
||||||
else:
|
else:
|
||||||
pitches = pitches_for_voicing(chord)
|
pitches = pitches_for_voicing(chord)
|
||||||
event = make_event(f'chord-{len(events)}',onset,duration,'note',pitches)
|
event = make_event(f'chord-{len(events)}', onset, duration, 'note', pitches)
|
||||||
if not pitches:
|
if not pitches:
|
||||||
warnings.warn(f'{measure["id"]}: no playable voicing for {chord["symbol"]}; using labelled rhythmic slashes', stacklevel=2)
|
warnings.warn(f'{measure["id"]}: no playable voicing for {chord["symbol"]}; using labelled rhythmic slashes', stacklevel=2)
|
||||||
event['notehead'] = 'slash'
|
event['notehead'] = 'slash'
|
||||||
events.append(event)
|
events.append(event)
|
||||||
cursor = onset + duration
|
cursor = onset + duration
|
||||||
if cursor < length:
|
if cursor < length:
|
||||||
events.append(make_event(f'rest-{len(events)}',cursor,length-cursor,'rest'))
|
events.append(make_event(f'rest-{len(events)}', cursor, length - cursor, 'rest'))
|
||||||
return events
|
return events
|
||||||
|
|
||||||
|
|
||||||
def arrange_document(document: dict, part_type: str) -> dict:
|
def arrange_document(document: dict, part_type: str) -> dict:
|
||||||
if part_type not in PART_TYPES:
|
if part_type not in PART_TYPES:
|
||||||
raise ValueError(f'unknown arrangement part: {part_type}')
|
raise ValueError(f'unknown arrangement part: {part_type}')
|
||||||
document = concert_document(document)
|
document = concert_document(document)
|
||||||
staff = source_staff(document)
|
staff = source_staff(document)
|
||||||
source = staff['measures']
|
source = staff['measures']
|
||||||
sections = full_arrangement_sections(source,part_type)
|
sections = full_arrangement_sections(source, part_type)
|
||||||
output, tuplets, octave_shifts = [], [], []
|
output, tuplets, octave_shifts = [], [], []
|
||||||
absolute = Fraction()
|
absolute = Fraction()
|
||||||
source_starts = []
|
source_starts = []
|
||||||
@@ -101,80 +97,79 @@ def arrange_document(document: dict, part_type: str) -> dict:
|
|||||||
source_starts.append(source_time)
|
source_starts.append(source_time)
|
||||||
source_time += fraction(m['duration'])
|
source_time += fraction(m['duration'])
|
||||||
for section in sections:
|
for section in sections:
|
||||||
previous = propagated_chords(source, include_optional=section.section_index!=FINAL_SECTION_INDEX)
|
previous = propagated_chords(source, include_optional=section.section_index != FINAL_SECTION_INDEX)
|
||||||
segment = []
|
segment = []
|
||||||
for si in range(section.source_measure_index,section.source_measure_index+section.measure_count):
|
for si in range(section.source_measure_index, section.source_measure_index + section.measure_count):
|
||||||
m = copy.deepcopy(source[si])
|
m = copy.deepcopy(source[si])
|
||||||
m['harmonies'] = [h for h in m['harmonies'] if section.section_index!=FINAL_SECTION_INDEX or not h.get('chord_optional')]
|
m['harmonies'] = [h for h in m['harmonies'] if section.section_index != FINAL_SECTION_INDEX or not h.get('chord_optional')]
|
||||||
for h in m['harmonies']:
|
for h in m['harmonies']:
|
||||||
h.pop('chord_optional',None)
|
h.pop('chord_optional', None)
|
||||||
notes = copy_source_voice(m,strip_lyrics=section.role=='melody')['notes'] if section.role in ('vocal','melody') else generated_voice(m,section.role,previous[si])
|
notes = copy_source_voice(m, strip_lyrics=section.role == 'melody')['notes'] if section.role in ('vocal', 'melody') else generated_voice(m, section.role, previous[si])
|
||||||
if section.role == 'melody':
|
if section.role == 'melody':
|
||||||
for note in notes:
|
for note in notes:
|
||||||
note['pitches'] = [shift_pitch(p,7,12) for p in note['pitches']]
|
note['pitches'] = [shift_pitch(p, 7, 12) for p in note['pitches']]
|
||||||
result = make_measure(f'measure-{len(output)+len(segment)+1}',m,notes,section_label=section.label if not segment else '')
|
result = make_measure(f'measure-{len(output) + len(segment) + 1}', m, notes, section_label=section.label if not segment else '')
|
||||||
if part_type!='vocal':
|
if part_type != 'vocal':
|
||||||
result['clef'] = {'sign':'G','line':2,'octave_change':0}
|
result['clef'] = {'sign': 'G', 'line': 2, 'octave_change': 0}
|
||||||
result['transpose'] = {'diatonic':0,'chromatic':0,'octave_change':-1}
|
result['transpose'] = {'diatonic': 0, 'chromatic': 0, 'octave_change': -1}
|
||||||
for change in result.get('attributes',[]):
|
for change in result.get('attributes', []):
|
||||||
change.pop('clef',None)
|
change.pop('clef', None)
|
||||||
change.pop('transpose',None)
|
change.pop('transpose', None)
|
||||||
segment.append(result)
|
segment.append(result)
|
||||||
identity_segment = segment
|
identity_segment = segment
|
||||||
segment_length = sum((fraction(m['duration']) for m in segment),Fraction())
|
segment_length = sum((fraction(m['duration']) for m in segment), Fraction())
|
||||||
if section.role in ('vocal','melody'):
|
if section.role in ('vocal', 'melody'):
|
||||||
start = source_starts[section.source_measure_index]
|
start = source_starts[section.source_measure_index]
|
||||||
for shift in staff.get('octave_shifts',[]):
|
for shift in staff.get('octave_shifts', []):
|
||||||
low, high = max(start,fraction(shift['start'])), min(start+segment_length,fraction(shift['end']))
|
low, high = max(start, fraction(shift['start'])), min(start + segment_length, fraction(shift['end']))
|
||||||
if low < high:
|
if low < high:
|
||||||
octave_shifts.append({**shift,'id':f'section-{section.section_index}-{shift["id"]}','start':rational(absolute+low-start),'end':rational(absolute+high-start)})
|
octave_shifts.append({**shift, 'id': f'section-{section.section_index}-{shift["id"]}', 'start': rational(absolute + low - start), 'end': rational(absolute + high - start)})
|
||||||
if part_type=='vocal' and section.section_index in (1,5) and section.source_measure_index==1 and source[0].get('implicit') and output:
|
if part_type == 'vocal' and section.section_index in (1, 5) and section.source_measure_index == 1 and source[0].get('implicit') and output:
|
||||||
pickup = copy.deepcopy(source[0])
|
pickup = copy.deepcopy(source[0])
|
||||||
destination = output[-1]
|
destination = output[-1]
|
||||||
pad = fraction(destination['duration']) - fraction(pickup['duration'])
|
pad = fraction(destination['duration']) - fraction(pickup['duration'])
|
||||||
if pad < 0:
|
if pad < 0:
|
||||||
raise ValueError('pickup is longer than the preceding arrangement measure')
|
raise ValueError('pickup is longer than the preceding arrangement measure')
|
||||||
notes = copy_source_voice(pickup,strip_lyrics=False)['notes']
|
notes = copy_source_voice(pickup, strip_lyrics=False)['notes']
|
||||||
for n in notes:
|
for n in notes:
|
||||||
n['onset'] = rational(fraction(n['onset'])+pad)
|
n['onset'] = rational(fraction(n['onset']) + pad)
|
||||||
if pad:
|
if pad:
|
||||||
notes.insert(0,make_event('pickup-rest',Fraction(),pad,'rest'))
|
notes.insert(0, make_event('pickup-rest', Fraction(), pad, 'rest'))
|
||||||
destination['voices'][0]['notes'] = notes
|
destination['voices'][0]['notes'] = notes
|
||||||
identity_segment = [destination, *segment]
|
identity_segment = [destination, *segment]
|
||||||
if pickup['harmonies']:
|
if pickup['harmonies']:
|
||||||
destination['harmonies'] = [h for h in destination['harmonies'] if fraction(h['onset']) < pad]
|
destination['harmonies'] = [h for h in destination['harmonies'] if fraction(h['onset']) < pad]
|
||||||
for h in pickup['harmonies']:
|
for h in pickup['harmonies']:
|
||||||
h['onset'] = rational(fraction(h['onset'])+pad)
|
h['onset'] = rational(fraction(h['onset']) + pad)
|
||||||
destination['harmonies'].append(h)
|
destination['harmonies'].append(h)
|
||||||
state = {k:copy.deepcopy(pickup[k]) for k in ('key','clef','meter','transpose')}
|
state = {k: copy.deepcopy(pickup[k]) for k in ('key', 'clef', 'meter', 'transpose')}
|
||||||
if pad:
|
if pad:
|
||||||
destination['attributes'].append({'onset':rational(pad), **state})
|
destination['attributes'].append({'onset': rational(pad), **state})
|
||||||
else:
|
else:
|
||||||
destination.update(state)
|
destination.update(state)
|
||||||
destination['key_signature'] = key_label(state['key'])
|
destination['key_signature'] = key_label(state['key'])
|
||||||
destination['time_signature'] = meter_label(state['meter'])
|
destination['time_signature'] = meter_label(state['meter'])
|
||||||
for change in pickup.get('attributes',[]):
|
for change in pickup.get('attributes', []):
|
||||||
destination['attributes'].append({**change,'onset':rational(fraction(change['onset'])+pad)})
|
destination['attributes'].append({**change, 'onset': rational(fraction(change['onset']) + pad)})
|
||||||
for shift in staff.get('octave_shifts',[]):
|
for shift in staff.get('octave_shifts', []):
|
||||||
low, high = fraction(shift['start']), min(fraction(pickup['duration']),fraction(shift['end']))
|
low, high = fraction(shift['start']), min(fraction(pickup['duration']), fraction(shift['end']))
|
||||||
if low < high:
|
if low < high:
|
||||||
offset = absolute-fraction(pickup['duration'])
|
offset = absolute - fraction(pickup['duration'])
|
||||||
octave_shifts.append({**shift,'id':f'pickup-{section.section_index}-{shift["id"]}','start':rational(offset+low),'end':rational(offset+high)})
|
octave_shifts.append({**shift, 'id': f'pickup-{section.section_index}-{shift["id"]}', 'start': rational(offset + low), 'end': rational(offset + high)})
|
||||||
tuplets.extend(reidentify(identity_segment,staff.get('tuplets',[])))
|
tuplets.extend(reidentify(identity_segment, staff.get('tuplets', [])))
|
||||||
output.extend(segment)
|
output.extend(segment)
|
||||||
absolute += segment_length
|
absolute += segment_length
|
||||||
result = make_arranged_document(document,staff,staff_id='staff-arranged-'+part_type,staff_name=part_type.capitalize(),role='vocal' if part_type=='vocal' else 'guitar',arrangement_part_type=part_type,measures=output,tuplets=tuplets,octave_shifts=octave_shifts)
|
result = make_arranged_document(document, staff, staff_id='staff-arranged-' + part_type, staff_name=part_type.capitalize(), role='vocal' if part_type == 'vocal' else 'guitar', arrangement_part_type=part_type, measures=output, tuplets=tuplets, octave_shifts=octave_shifts)
|
||||||
result['metadata'].setdefault('tempo',124)
|
result['metadata'].setdefault('tempo', 124)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
def generate_performance_scores(source_dir: Path, output_dir: Path) -> int:
|
def generate_performance_scores(source_dir: Path, output_dir: Path) -> int:
|
||||||
if not source_dir.exists():
|
if not source_dir.exists():
|
||||||
raise ValueError(f'performance source directory not found: {source_dir}')
|
raise ValueError(f'performance source directory not found: {source_dir}')
|
||||||
count = 0
|
count = 0
|
||||||
for path in sorted(source_dir.glob('*.musicxml')):
|
for path in sorted(source_dir.glob('*.xml')):
|
||||||
document = load_musicxml(path)
|
document = load_musicxml(path)
|
||||||
for part in PART_TYPES:
|
for part in PART_TYPES:
|
||||||
write_json(output_dir/f'{slugify(path.stem)}-{part}.json',arrange_document(document,part))
|
write_json(output_dir / f'{slugify(path.stem)}-{part}.json', arrange_document(document, part))
|
||||||
count += 1
|
count += 1
|
||||||
return count
|
return count
|
||||||
|
|||||||
+23
-28
@@ -4,28 +4,27 @@ from generator.model import NATURAL, parse_pitch_name, pitch_name
|
|||||||
|
|
||||||
# Semantic degrees are independent of guitar voicing omissions.
|
# Semantic degrees are independent of guitar voicing omissions.
|
||||||
KINDS = {
|
KINDS = {
|
||||||
'major': {1:0, 3:0, 5:0}, 'minor': {1:0, 3:-1, 5:0},
|
'major': {1: 0, 3: 0, 5: 0}, 'minor': {1: 0, 3: -1, 5: 0},
|
||||||
'augmented': {1:0, 3:0, 5:1}, 'diminished': {1:0, 3:-1, 5:-1},
|
'augmented': {1: 0, 3: 0, 5: 1}, 'diminished': {1: 0, 3: -1, 5: -1},
|
||||||
'dominant': {1:0, 3:0, 5:0, 7:-1}, 'major-seventh': {1:0, 3:0, 5:0, 7:0},
|
'dominant': {1: 0, 3: 0, 5: 0, 7: -1}, 'major-seventh': {1: 0, 3: 0, 5: 0, 7: 0},
|
||||||
'minor-seventh': {1:0, 3:-1, 5:0, 7:-1}, 'diminished-seventh': {1:0, 3:-1, 5:-1, 7:-2},
|
'minor-seventh': {1: 0, 3: -1, 5: 0, 7: -1}, 'diminished-seventh': {1: 0, 3: -1, 5: -1, 7: -2},
|
||||||
'augmented-seventh': {1:0, 3:0, 5:1, 7:-1}, 'augmented-major-seventh': {1:0, 3:0, 5:1, 7:0},
|
'augmented-seventh': {1: 0, 3: 0, 5: 1, 7: -1}, 'augmented-major-seventh': {1: 0, 3: 0, 5: 1, 7: 0},
|
||||||
'half-diminished': {1:0, 3:-1, 5:-1, 7:-1}, 'major-minor': {1:0, 3:-1, 5:0, 7:0},
|
'half-diminished': {1: 0, 3: -1, 5: -1, 7: -1}, 'major-minor': {1: 0, 3: -1, 5: 0, 7: 0},
|
||||||
'major-sixth': {1:0, 3:0, 5:0, 6:0}, 'minor-sixth': {1:0, 3:-1, 5:0, 6:0},
|
'major-sixth': {1: 0, 3: 0, 5: 0, 6: 0}, 'minor-sixth': {1: 0, 3: -1, 5: 0, 6: 0},
|
||||||
'suspended-second': {1:0, 2:0, 5:0}, 'suspended-fourth': {1:0, 4:0, 5:0},
|
'suspended-second': {1: 0, 2: 0, 5: 0}, 'suspended-fourth': {1: 0, 4: 0, 5: 0},
|
||||||
'power': {1:0, 5:0}, 'pedal': {1:0}, 'other': {}, 'none': {},
|
'power': {1: 0, 5: 0}, 'pedal': {1: 0}, 'other': {}, 'none': {},
|
||||||
'Neapolitan': {1:0, 3:0, 5:0}, 'Italian': {1:0, 3:0, 6:1},
|
'Neapolitan': {1: 0, 3: 0, 5: 0}, 'Italian': {1: 0, 3: 0, 6: 1},
|
||||||
'French': {1:0, 3:0, 4:1, 6:1}, 'German': {1:0, 3:0, 5:0, 6:1},
|
'French': {1: 0, 3: 0, 4: 1, 6: 1}, 'German': {1: 0, 3: 0, 5: 0, 6: 1},
|
||||||
}
|
}
|
||||||
for family, base in (('dominant', 'dominant'), ('major', 'major-seventh'), ('minor', 'minor-seventh')):
|
for family, base in (('dominant', 'dominant'), ('major', 'major-seventh'), ('minor', 'minor-seventh')):
|
||||||
for suffix, highest in (('ninth',9), ('11th',11), ('13th',13)):
|
for suffix, highest in (('ninth', 9), ('11th', 11), ('13th', 13)):
|
||||||
KINDS[f'{family}-{suffix}'] = {**KINDS[base], **{i:0 for i in range(9, highest+1, 2)}}
|
KINDS[f'{family}-{suffix}'] = {**KINDS[base], **{i: 0 for i in range(9, highest + 1, 2)}}
|
||||||
|
|
||||||
SUFFIXES = {'major':'', 'minor':'-', 'augmented':'+', 'diminished':'dim', 'dominant':'7', 'major-seventh':'maj7', 'minor-seventh':'-7', 'diminished-seventh':'dim7', 'augmented-seventh':'+7', 'augmented-major-seventh':'+maj7', 'half-diminished':'-7b5', 'major-minor':'-maj7', 'major-sixth':'6', 'minor-sixth':'-6', 'suspended-second':'sus2', 'suspended-fourth':'sus4', 'power':'5', 'pedal':'', 'other':''}
|
SUFFIXES = {'major': '', 'minor': '-', 'augmented': '+', 'diminished': 'dim', 'dominant': '7', 'major-seventh': 'maj7', 'minor-seventh': '-7', 'diminished-seventh': 'dim7', 'augmented-seventh': '+7', 'augmented-major-seventh': '+maj7', 'half-diminished': '-7b5', 'major-minor': '-maj7', 'major-sixth': '6', 'minor-sixth': '-6', 'suspended-second': 'sus2', 'suspended-fourth': 'sus4', 'power': '5', 'pedal': '', 'other': ''}
|
||||||
for family, prefix in (('dominant',''), ('major','maj'), ('minor','-')):
|
for family, prefix in (('dominant', ''), ('major', 'maj'), ('minor', '-')):
|
||||||
for suffix, number in (('ninth','9'), ('11th','11'), ('13th','13')):
|
for suffix, number in (('ninth', '9'), ('11th', '11'), ('13th', '13')):
|
||||||
SUFFIXES[f'{family}-{suffix}'] = prefix + number
|
SUFFIXES[f'{family}-{suffix}'] = prefix + number
|
||||||
|
|
||||||
|
|
||||||
def degrees(harmony: dict) -> dict[int, int]:
|
def degrees(harmony: dict) -> dict[int, int]:
|
||||||
kind = harmony['kind']
|
kind = harmony['kind']
|
||||||
if kind not in KINDS:
|
if kind not in KINDS:
|
||||||
@@ -53,10 +52,8 @@ def degrees(harmony: dict) -> dict[int, int]:
|
|||||||
raise ValueError('harmony contains no pitch information')
|
raise ValueError('harmony contains no pitch information')
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
def degree_semitones(degree: int, alter: int) -> int:
|
def degree_semitones(degree: int, alter: int) -> int:
|
||||||
return 12 * ((degree-1)//7) + (0,2,4,5,7,9,11)[(degree-1)%7] + alter
|
return 12 * ((degree - 1) // 7) + (0, 2, 4, 5, 7, 9, 11)[(degree - 1) % 7] + alter
|
||||||
|
|
||||||
|
|
||||||
def pitch_classes(harmony: dict) -> tuple[int, ...]:
|
def pitch_classes(harmony: dict) -> tuple[int, ...]:
|
||||||
if harmony['kind'] == 'none':
|
if harmony['kind'] == 'none':
|
||||||
@@ -68,7 +65,6 @@ def pitch_classes(harmony: dict) -> tuple[int, ...]:
|
|||||||
values.append((NATURAL[bass['letter']] + bass['alter']) % 12)
|
values.append((NATURAL[bass['letter']] + bass['alter']) % 12)
|
||||||
return tuple(dict.fromkeys(values))
|
return tuple(dict.fromkeys(values))
|
||||||
|
|
||||||
|
|
||||||
def symbol(harmony: dict) -> str:
|
def symbol(harmony: dict) -> str:
|
||||||
if harmony['kind'] == 'none':
|
if harmony['kind'] == 'none':
|
||||||
return 'N.C.'
|
return 'N.C.'
|
||||||
@@ -76,21 +72,20 @@ def symbol(harmony: dict) -> str:
|
|||||||
if 'kind_text' not in harmony:
|
if 'kind_text' not in harmony:
|
||||||
for d in harmony.get('degrees', []):
|
for d in harmony.get('degrees', []):
|
||||||
sign = '#' * max(d['alter'], 0) + 'b' * max(-d['alter'], 0)
|
sign = '#' * max(d['alter'], 0) + 'b' * max(-d['alter'], 0)
|
||||||
suffix += ('no' if d['type']=='subtract' else 'add' if d['type']=='add' and not sign else '') + sign + str(d['value'])
|
suffix += ('no' if d['type'] == 'subtract' else 'add' if d['type'] == 'add' and not sign else '') + sign + str(d['value'])
|
||||||
result = pitch_name(harmony['root']) + suffix
|
result = pitch_name(harmony['root']) + suffix
|
||||||
if bass := harmony.get('bass'):
|
if bass := harmony.get('bass'):
|
||||||
result += '/' + pitch_name(bass)
|
result += '/' + pitch_name(bass)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
def parse_symbol(text: str) -> dict:
|
def parse_symbol(text: str) -> dict:
|
||||||
import re
|
import re
|
||||||
match = re.fullmatch(r'([A-Ga-g][#b]*)([^/]*)(?:/([A-Ga-g][#b]*))?', text.strip())
|
match = re.fullmatch(r'([A-Ga-g][#b]*)([^/]*)(?:/([A-Ga-g][#b]*))?', text.strip())
|
||||||
if not match:
|
if not match:
|
||||||
raise ValueError(f'invalid chord symbol: {text!r}')
|
raise ValueError(f'invalid chord symbol: {text!r}')
|
||||||
root, suffix, bass = match.groups()
|
root, suffix, bass = match.groups()
|
||||||
aliases = {'m':'minor', 'min':'minor', 'maj':'major', 'M':'major', 'm7':'minor-seventh', 'm6':'minor-sixth', 'm7b5':'half-diminished', 'o':'diminished', 'o7':'diminished-seventh', 'aug':'augmented', 'sus':'suspended-fourth'}
|
aliases = {'m': 'minor', 'min': 'minor', 'maj': 'major', 'M': 'major', 'm7': 'minor-seventh', 'm6': 'minor-sixth', 'm7b5': 'half-diminished', 'o': 'diminished', 'o7': 'diminished-seventh', 'aug': 'augmented', 'sus': 'suspended-fourth'}
|
||||||
aliases.update({v:k for k,v in SUFFIXES.items() if k not in ('other','pedal')})
|
aliases.update({v: k for k, v in SUFFIXES.items() if k not in ('other', 'pedal')})
|
||||||
core = suffix
|
core = suffix
|
||||||
changes = []
|
changes = []
|
||||||
while core not in aliases:
|
while core not in aliases:
|
||||||
@@ -98,13 +93,13 @@ def parse_symbol(text: str) -> dict:
|
|||||||
if not m:
|
if not m:
|
||||||
raise ValueError(f'unsupported chord spelling: {text!r}')
|
raise ValueError(f'unsupported chord spelling: {text!r}')
|
||||||
op, accidental, value = m.groups()
|
op, accidental, value = m.groups()
|
||||||
changes.insert(0, {'value':int(value), 'alter':1 if accidental=='#' else -1 if accidental=='b' else 0, 'type':'subtract' if op=='no' else 'add'})
|
changes.insert(0, {'value': int(value), 'alter': 1 if accidental == '#' else -1 if accidental == 'b' else 0, 'type': 'subtract' if op == 'no' else 'add'})
|
||||||
core = core[:m.start()]
|
core = core[:m.start()]
|
||||||
kind = aliases[core]
|
kind = aliases[core]
|
||||||
for d in changes:
|
for d in changes:
|
||||||
if d['type']=='add' and d['value'] in KINDS[kind]:
|
if d['type'] == 'add' and d['value'] in KINDS[kind]:
|
||||||
d['type'] = 'alter'
|
d['type'] = 'alter'
|
||||||
result = {'root':parse_pitch_name(root), 'kind':kind, 'degrees':changes, 'kind_text':suffix}
|
result = {'root': parse_pitch_name(root), 'kind': kind, 'degrees': changes, 'kind_text': suffix}
|
||||||
if bass:
|
if bass:
|
||||||
result['bass'] = parse_pitch_name(bass)
|
result['bass'] = parse_pitch_name(bass)
|
||||||
degrees(result)
|
degrees(result)
|
||||||
|
|||||||
+7
-23
@@ -12,14 +12,12 @@ NATURAL = {'c': 0, 'd': 2, 'e': 4, 'f': 5, 'g': 7, 'a': 9, 'b': 11}
|
|||||||
LETTERS = 'cdefgab'
|
LETTERS = 'cdefgab'
|
||||||
MODE_STEPS = {'major': 0, 'ionian': 0, 'dorian': 1, 'phrygian': 2, 'lydian': 3, 'mixolydian': 4, 'minor': 5, 'aeolian': 5, 'locrian': 6}
|
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]:
|
def rational(value: Fraction | int) -> dict[str, int]:
|
||||||
value = Fraction(value)
|
value = Fraction(value)
|
||||||
if abs(value.numerator) > MAX_INTEGER or value.denominator > MAX_INTEGER:
|
if abs(value.numerator) > MAX_INTEGER or value.denominator > MAX_INTEGER:
|
||||||
raise ValueError('musical time exceeds the supported 64-bit rational range')
|
raise ValueError('musical time exceeds the supported 64-bit rational range')
|
||||||
return {'numerator': value.numerator, 'denominator': value.denominator}
|
return {'numerator': value.numerator, 'denominator': value.denominator}
|
||||||
|
|
||||||
|
|
||||||
def fraction(value: Any) -> Fraction:
|
def fraction(value: Any) -> Fraction:
|
||||||
if not isinstance(value, dict) or set(value) != {'numerator', 'denominator'}:
|
if not isinstance(value, dict) or set(value) != {'numerator', 'denominator'}:
|
||||||
raise ValueError('musical time must be a numerator/denominator object')
|
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')
|
raise ValueError('musical time must be reduced')
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
def written_length(written: dict) -> Fraction:
|
def written_length(written: dict) -> Fraction:
|
||||||
kind, dots = written['type'], written.get('dots', 0)
|
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:
|
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)')
|
raise ValueError('invalid written duration or augmentation-dot count (maximum 16)')
|
||||||
return TYPE_LENGTHS[kind] * (2 - Fraction(1, 2 ** dots))
|
return TYPE_LENGTHS[kind] * (2 - Fraction(1, 2 ** dots))
|
||||||
|
|
||||||
|
|
||||||
def written_for(length: Fraction) -> dict:
|
def written_for(length: Fraction) -> dict:
|
||||||
for name, base in TYPE_LENGTHS.items():
|
for name, base in TYPE_LENGTHS.items():
|
||||||
for dots in range(17):
|
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')
|
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])}
|
return {'type': base, 'dots': 0, 'ratio': rational(length / TYPE_LENGTHS[base])}
|
||||||
|
|
||||||
|
|
||||||
def metric_length(written: dict) -> Fraction:
|
def metric_length(written: dict) -> Fraction:
|
||||||
ratio = fraction(written.get('ratio', rational(1)))
|
ratio = fraction(written.get('ratio', rational(1)))
|
||||||
if ratio <= 0:
|
if ratio <= 0:
|
||||||
@@ -57,7 +52,6 @@ def metric_length(written: dict) -> Fraction:
|
|||||||
written_length(written['normal'])
|
written_length(written['normal'])
|
||||||
return written_length(written) * ratio
|
return written_length(written) * ratio
|
||||||
|
|
||||||
|
|
||||||
def pitch_number(pitch: dict) -> int:
|
def pitch_number(pitch: dict) -> int:
|
||||||
letter = pitch['letter'].lower()
|
letter = pitch['letter'].lower()
|
||||||
alter, octave = pitch.get('alter', 0), pitch['octave']
|
alter, octave = pitch.get('alter', 0), pitch['octave']
|
||||||
@@ -65,18 +59,15 @@ def pitch_number(pitch: dict) -> int:
|
|||||||
raise ValueError('invalid spelled pitch')
|
raise ValueError('invalid spelled pitch')
|
||||||
return 12 * (octave + 1) + NATURAL[letter] + alter
|
return 12 * (octave + 1) + NATURAL[letter] + alter
|
||||||
|
|
||||||
|
|
||||||
def pitch_name(pitch: dict) -> str:
|
def pitch_name(pitch: dict) -> str:
|
||||||
alter = pitch.get('alter', 0)
|
alter = pitch.get('alter', 0)
|
||||||
return pitch['letter'].upper() + ('#' * alter if alter > 0 else 'b' * -alter)
|
return pitch['letter'].upper() + ('#' * alter if alter > 0 else 'b' * -alter)
|
||||||
|
|
||||||
|
|
||||||
def parse_pitch_name(name: str) -> dict:
|
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:]):
|
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}')
|
raise ValueError(f'invalid pitch name: {name!r}')
|
||||||
return {'letter': name[0].lower(), 'alter': name[1:].count('#') - name[1:].count('b')}
|
return {'letter': name[0].lower(), 'alter': name[1:].count('#') - name[1:].count('b')}
|
||||||
|
|
||||||
|
|
||||||
def key_from_fifths(fifths: int, mode: str = 'major') -> dict:
|
def key_from_fifths(fifths: int, mode: str = 'major') -> dict:
|
||||||
if mode not in MODE_STEPS and mode != 'none':
|
if mode not in MODE_STEPS and mode != 'none':
|
||||||
raise ValueError(f'unsupported mode: {mode}')
|
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)]}
|
tonic = {'letter': letter, 'alter': signature[LETTERS.index(letter)]}
|
||||||
return {'fifths': fifths, 'mode': mode, 'tonic': tonic if mode != 'none' else None, 'alterations': signature}
|
return {'fifths': fifths, 'mode': mode, 'tonic': tonic if mode != 'none' else None, 'alterations': signature}
|
||||||
|
|
||||||
|
|
||||||
def key_label(key: dict) -> str:
|
def key_label(key: dict) -> str:
|
||||||
if not key.get('tonic'):
|
if not key.get('tonic'):
|
||||||
return 'No key'
|
return 'No key'
|
||||||
label = pitch_name(key['tonic'])
|
label = pitch_name(key['tonic'])
|
||||||
return label if key['mode'] in ('major', 'ionian') else label + ' ' + key['mode']
|
return label if key['mode'] in ('major', 'ionian') else label + ' ' + key['mode']
|
||||||
|
|
||||||
|
|
||||||
def meter_length(meter: dict) -> Fraction:
|
def meter_length(meter: dict) -> Fraction:
|
||||||
return sum((Fraction(sum(group['beats']) * 4, group['unit']) for group in meter.get('groups', [])), Fraction())
|
return sum((Fraction(sum(group['beats']) * 4, group['unit']) for group in meter.get('groups', [])), Fraction())
|
||||||
|
|
||||||
|
|
||||||
def meter_label(meter: dict) -> str:
|
def meter_label(meter: dict) -> str:
|
||||||
if meter.get('unmetered'):
|
if meter.get('unmetered'):
|
||||||
return 'senza misura'
|
return 'senza misura'
|
||||||
return '+'.join('+'.join(map(str, group['beats'])) + '/' + str(group['unit']) for group in meter['groups'])
|
return '+'.join('+'.join(map(str, group['beats'])) + '/' + str(group['unit']) for group in meter['groups'])
|
||||||
|
|
||||||
|
|
||||||
def beat_lengths(meter: dict) -> list[Fraction]:
|
def beat_lengths(meter: dict) -> list[Fraction]:
|
||||||
result = []
|
result = []
|
||||||
for group in meter.get('groups', []):
|
for group in meter.get('groups', []):
|
||||||
@@ -121,7 +108,6 @@ def beat_lengths(meter: dict) -> list[Fraction]:
|
|||||||
result.extend([Fraction(4, unit)] * beats[0])
|
result.extend([Fraction(4, unit)] * beats[0])
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
def validate_meter(meter: dict) -> None:
|
def validate_meter(meter: dict) -> None:
|
||||||
groups = meter.get('groups', [])
|
groups = meter.get('groups', [])
|
||||||
if not groups and not meter.get('unmetered'):
|
if not groups and not meter.get('unmetered'):
|
||||||
@@ -132,17 +118,15 @@ def validate_meter(meter: dict) -> None:
|
|||||||
if any(type(b) is not int or not 0 < b <= 1024 for b in group['beats']):
|
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')
|
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:
|
def make_event(identifier: str, onset: Fraction, duration: Fraction, kind: str, pitches: list[dict] | None = None, written: dict | None = None) -> dict:
|
||||||
if duration <= 0:
|
if duration <= 0:
|
||||||
raise ValueError('generated event duration must be positive')
|
raise ValueError('generated event duration must be positive')
|
||||||
notation = written or written_for(duration)
|
notation = written or written_for(duration)
|
||||||
event = {'id': identifier, 'kind': kind, 'onset': rational(onset), 'duration': rational(duration), 'written': notation, 'hidden': False, 'pitches': []}
|
event = {'id': identifier, 'kind': kind, 'onset': rational(onset), 'duration': rational(duration), 'written': notation, 'hidden': False, 'pitches': []}
|
||||||
for i, p in enumerate(pitches or []):
|
for i, p in enumerate(pitches or []):
|
||||||
event['pitches'].append({**copy.deepcopy(p), 'id': f'{identifier}-p{i+1}', 'duration': rational(duration), 'written': copy.deepcopy(notation)})
|
event['pitches'].append({**copy.deepcopy(p), 'id': f'{identifier}-p{i + 1}', 'duration': rational(duration), 'written': copy.deepcopy(notation)})
|
||||||
return event
|
return event
|
||||||
|
|
||||||
|
|
||||||
def validate_document(document: dict) -> None:
|
def validate_document(document: dict) -> None:
|
||||||
if document.get('schema_version') != SCHEMA_VERSION:
|
if document.get('schema_version') != SCHEMA_VERSION:
|
||||||
raise ValueError('unsupported score schema; regenerate score assets with the current generator')
|
raise ValueError('unsupported score schema; regenerate score assets with the current generator')
|
||||||
@@ -203,7 +187,7 @@ def validate_document(document: dict) -> None:
|
|||||||
validate_meter(state['meter'])
|
validate_meter(state['meter'])
|
||||||
if 'key' in state and (len(state['key']['alterations']) != 7 or any(type(a) is not int or not -3 <= a <= 3 for a in state['key']['alterations'])):
|
if 'key' in state and (len(state['key']['alterations']) != 7 or any(type(a) is not int or not -3 <= a <= 3 for a in state['key']['alterations'])):
|
||||||
raise ValueError('key alterations must contain seven integers from -3 through 3')
|
raise ValueError('key alterations must contain seven integers from -3 through 3')
|
||||||
if 'clef' in state and (state['clef']['sign'] not in ('G','F','C') or not 1 <= state['clef']['line'] <= 5 or not -3 <= state['clef']['octave_change'] <= 3):
|
if 'clef' in state and (state['clef']['sign'] not in ('G', 'F', 'C') or not 1 <= state['clef']['line'] <= 5 or not -3 <= state['clef']['octave_change'] <= 3):
|
||||||
raise ValueError('invalid pitched clef')
|
raise ValueError('invalid pitched clef')
|
||||||
absolute += length
|
absolute += length
|
||||||
for identifier, (p, start, duration) in tones.items():
|
for identifier, (p, start, duration) in tones.items():
|
||||||
@@ -217,26 +201,26 @@ def validate_document(document: dict) -> None:
|
|||||||
if source not in tones or tones[source][0].get('tie_to') != identifier:
|
if source not in tones or tones[source][0].get('tie_to') != identifier:
|
||||||
raise ValueError(f'dangling incoming tie: {identifier}')
|
raise ValueError(f'dangling incoming tie: {identifier}')
|
||||||
span_ids = set()
|
span_ids = set()
|
||||||
event_order = {identifier:index for index, identifier in enumerate(events)}
|
event_order = {identifier: index for index, identifier in enumerate(events)}
|
||||||
for span in staves[0].get('tuplets', []):
|
for span in staves[0].get('tuplets', []):
|
||||||
if not span['id'] or span['id'] in span_ids or span['start'] not in ids or span['end'] not in ids or not 0 < span['note_count'] <= 1000000 or not 0 < span['space_count'] <= 1000000 or not 0 <= span['depth'] <= 16:
|
if not span['id'] or span['id'] in span_ids or span['start'] not in ids or span['end'] not in ids or not 0 < span['note_count'] <= 1000000 or not 0 < span['space_count'] <= 1000000 or not 0 <= span['depth'] <= 16:
|
||||||
raise ValueError('invalid tuplet span')
|
raise ValueError('invalid tuplet span')
|
||||||
members = [n['id'] for n in events.values() if span['id'] in n.get('tuplet_ids', [])]
|
members = [n['id'] for n in events.values() if span['id'] in n.get('tuplet_ids', [])]
|
||||||
if not members or members[0] != span['start'] or members[-1] != span['end'] or event_order[span['start']] > event_order[span['end']]:
|
if not members or members[0] != span['start'] or members[-1] != span['end'] or event_order[span['start']] > event_order[span['end']]:
|
||||||
raise ValueError('tuplet endpoints disagree with member events')
|
raise ValueError('tuplet endpoints disagree with member events')
|
||||||
if span.get('bracket', 'auto') not in ('yes','no','auto') or span.get('show_number','actual') not in ('none','actual','both'):
|
if span.get('bracket', 'auto') not in ('yes', 'no', 'auto') or span.get('show_number', 'actual') not in ('none', 'actual', 'both'):
|
||||||
raise ValueError('invalid tuplet appearance')
|
raise ValueError('invalid tuplet appearance')
|
||||||
span_ids.add(span['id'])
|
span_ids.add(span['id'])
|
||||||
for note in events.values():
|
for note in events.values():
|
||||||
if any(t not in span_ids for t in note.get('tuplet_ids', [])):
|
if any(t not in span_ids for t in note.get('tuplet_ids', [])):
|
||||||
raise ValueError('event refers to an unknown tuplet')
|
raise ValueError('event refers to an unknown tuplet')
|
||||||
if tremolo := note.get('tremolo'):
|
if tremolo := note.get('tremolo'):
|
||||||
if tremolo['type'] not in ('single','start','stop') or type(tremolo['marks']) is not int or not 0 <= tremolo['marks'] <= 8:
|
if tremolo['type'] not in ('single', 'start', 'stop') or type(tremolo['marks']) is not int or not 0 <= tremolo['marks'] <= 8:
|
||||||
raise ValueError('invalid measured tremolo')
|
raise ValueError('invalid measured tremolo')
|
||||||
if tremolo['type'] in ('start','stop'):
|
if tremolo['type'] in ('start', 'stop'):
|
||||||
other = events.get(tremolo.get('partner'))
|
other = events.get(tremolo.get('partner'))
|
||||||
expected_type = 'stop' if tremolo['type'] == 'start' else 'start'
|
expected_type = 'stop' if tremolo['type'] == 'start' else 'start'
|
||||||
if not other or other.get('tremolo',{}).get('partner') != note['id'] or other['tremolo']['type'] != expected_type or other['tremolo']['marks'] != tremolo['marks']:
|
if not other or other.get('tremolo', {}).get('partner') != note['id'] or other['tremolo']['type'] != expected_type or other['tremolo']['marks'] != tremolo['marks']:
|
||||||
raise ValueError('invalid paired tremolo')
|
raise ValueError('invalid paired tremolo')
|
||||||
for span in staves[0].get('octave_shifts', []):
|
for span in staves[0].get('octave_shifts', []):
|
||||||
if not 0 <= fraction(span['start']) < fraction(span['end']) <= absolute or not -3 <= span['octaves'] <= 3:
|
if not 0 <= fraction(span['start']) < fraction(span['end']) <= absolute or not -3 <= span['octaves'] <= 3:
|
||||||
|
|||||||
+106
-121
@@ -4,35 +4,28 @@ import copy
|
|||||||
from fractions import Fraction
|
from fractions import Fraction
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from defusedxml import ElementTree as XML
|
from defusedxml import ElementTree
|
||||||
|
|
||||||
from generator import harmony
|
from generator import harmony
|
||||||
from generator.model import (
|
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, )
|
||||||
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
|
from generator.styles import canonical_style
|
||||||
|
|
||||||
|
|
||||||
class MusicXMLError(ValueError):
|
class MusicXMLError(ValueError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
# Ignoring a container never authorizes ignoring its musical descendants.
|
# 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'}
|
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'}
|
IGNORED_TOP = {'defaults', 'credit', 'work', 'movement-number', 'movement-title', 'identification', 'part-list'}
|
||||||
|
|
||||||
|
|
||||||
class Importer:
|
class Importer:
|
||||||
def __init__(self, path: Path) -> None:
|
def __init__(self, path: Path) -> None:
|
||||||
self.path = path
|
self.path = path
|
||||||
self.location = str(path)
|
self.location = str(path)
|
||||||
self.divisions = Fraction(1)
|
self.divisions = Fraction(1)
|
||||||
self.key = key_from_fifths(0)
|
self.key = key_from_fifths(0)
|
||||||
self.meter = {'groups':[{'beats':[4], 'unit':4}], 'symbol':'normal'}
|
self.meter = {'groups': [{'beats': [4], 'unit': 4}], 'symbol': 'normal'}
|
||||||
self.clef = {'sign':'G', 'line':2, 'octave_change':0}
|
self.clef = {'sign': 'G', 'line': 2, 'octave_change': 0}
|
||||||
self.transpose = {'diatonic':0, 'chromatic':0, 'octave_change':0}
|
self.transpose = {'diatonic': 0, 'chromatic': 0, 'octave_change': 0}
|
||||||
self.sequence = 0
|
self.sequence = 0
|
||||||
self.tuplet_sequence = 0
|
self.tuplet_sequence = 0
|
||||||
self.active_tuplets: dict[str, dict] = {}
|
self.active_tuplets: dict[str, dict] = {}
|
||||||
@@ -44,20 +37,16 @@ class Importer:
|
|||||||
self.lyric_number = None
|
self.lyric_number = None
|
||||||
self.absolute = Fraction()
|
self.absolute = Fraction()
|
||||||
self.tremolo_start = None
|
self.tremolo_start = None
|
||||||
|
|
||||||
def fail(self, text: str):
|
def fail(self, text: str):
|
||||||
raise MusicXMLError(f'{self.location}: {text}')
|
raise MusicXMLError(f'{self.location}: {text}')
|
||||||
|
|
||||||
def children(self, node, allowed: set[str]) -> None:
|
def children(self, node, allowed: set[str]) -> None:
|
||||||
for item in node:
|
for item in node:
|
||||||
if item.tag not in allowed:
|
if item.tag not in allowed:
|
||||||
self.fail(f'unsupported musical element <{item.tag}> in <{node.tag}>')
|
self.fail(f'unsupported musical element <{item.tag}> in <{node.tag}>')
|
||||||
|
|
||||||
def attrs(self, node, allowed=()) -> None:
|
def attrs(self, node, allowed=()) -> None:
|
||||||
for name in node.attrib:
|
for name in node.attrib:
|
||||||
if name not in LAYOUT and name not in allowed:
|
if name not in LAYOUT and name not in allowed:
|
||||||
self.fail(f'unsupported {node.tag} attribute {name!r}')
|
self.fail(f'unsupported {node.tag} attribute {name!r}')
|
||||||
|
|
||||||
def number(self, value: str | None, *, integer: bool = False, positive: bool = False) -> Fraction | int:
|
def number(self, value: str | None, *, integer: bool = False, positive: bool = False) -> Fraction | int:
|
||||||
try:
|
try:
|
||||||
result = Fraction(value or '')
|
result = Fraction(value or '')
|
||||||
@@ -67,12 +56,10 @@ class Importer:
|
|||||||
return int(result) if integer else result
|
return int(result) if integer else result
|
||||||
except (ValueError, ZeroDivisionError, OverflowError):
|
except (ValueError, ZeroDivisionError, OverflowError):
|
||||||
self.fail(f'invalid {"integer" if integer else "number"}: {value!r}')
|
self.fail(f'invalid {"integer" if integer else "number"}: {value!r}')
|
||||||
|
|
||||||
def offset(self, node, cursor: Fraction) -> Fraction:
|
def offset(self, node, cursor: Fraction) -> Fraction:
|
||||||
return cursor + self.number(node.findtext('offset', '0')) / self.divisions
|
return cursor + self.number(node.findtext('offset', '0')) / self.divisions
|
||||||
|
|
||||||
def identity(self, node) -> None:
|
def identity(self, node) -> None:
|
||||||
voice = node.findtext('voice', '1' if node.tag in ('note','forward') else self.voice_id)
|
voice = node.findtext('voice', '1' if node.tag in ('note', 'forward') else self.voice_id)
|
||||||
staff = node.findtext('staff', '1')
|
staff = node.findtext('staff', '1')
|
||||||
if staff != '1':
|
if staff != '1':
|
||||||
self.fail('multiple staves are not supported')
|
self.fail('multiple staves are not supported')
|
||||||
@@ -82,10 +69,9 @@ class Importer:
|
|||||||
self.voice_id = voice
|
self.voice_id = voice
|
||||||
if voice != self.voice_id:
|
if voice != self.voice_id:
|
||||||
self.fail('multiple rhythmic voices are not supported')
|
self.fail('multiple rhythmic voices are not supported')
|
||||||
|
|
||||||
def attributes(self, node, onset: Fraction) -> dict:
|
def attributes(self, node, onset: Fraction) -> dict:
|
||||||
self.children(node, {'divisions', 'key', 'time', 'clef', 'staves', 'transpose', 'instruments'})
|
self.children(node, {'divisions', 'key', 'time', 'clef', 'staves', 'transpose', 'instruments'})
|
||||||
result = {'onset':rational(onset)}
|
result = {'onset': rational(onset)}
|
||||||
for item in node:
|
for item in node:
|
||||||
if item.get('number', '1') != '1':
|
if item.get('number', '1') != '1':
|
||||||
self.fail('attributes for multiple staves are not supported')
|
self.fail('attributes for multiple staves are not supported')
|
||||||
@@ -100,7 +86,7 @@ class Importer:
|
|||||||
if item.find('fifths') is not None:
|
if item.find('fifths') is not None:
|
||||||
self.key = key_from_fifths(self.number(item.findtext('fifths'), integer=True), item.findtext('mode', 'major'))
|
self.key = key_from_fifths(self.number(item.findtext('fifths'), integer=True), item.findtext('mode', 'major'))
|
||||||
else:
|
else:
|
||||||
self.key = {'fifths':0, 'mode':'none', 'tonic':None, 'alterations':[0]*7, 'custom':True}
|
self.key = {'fifths': 0, 'mode': 'none', 'tonic': None, 'alterations': [0] * 7, 'custom': True}
|
||||||
pending = None
|
pending = None
|
||||||
for field in item:
|
for field in item:
|
||||||
if field.tag == 'key-step':
|
if field.tag == 'key-step':
|
||||||
@@ -122,7 +108,7 @@ class Importer:
|
|||||||
elif item.tag == 'time':
|
elif item.tag == 'time':
|
||||||
self.attrs(item, {'number', 'symbol', 'separator', 'print-object'})
|
self.attrs(item, {'number', 'symbol', 'separator', 'print-object'})
|
||||||
self.children(item, {'beats', 'beat-type', 'senza-misura'})
|
self.children(item, {'beats', 'beat-type', 'senza-misura'})
|
||||||
self.meter = {'groups':[], 'symbol':item.get('symbol', 'normal')}
|
self.meter = {'groups': [], 'symbol': item.get('symbol', 'normal')}
|
||||||
if self.meter['symbol'] not in ('normal', 'common', 'cut', 'single-number', 'note', 'dotted-note'):
|
if self.meter['symbol'] not in ('normal', 'common', 'cut', 'single-number', 'note', 'dotted-note'):
|
||||||
self.fail('unsupported meter symbol')
|
self.fail('unsupported meter symbol')
|
||||||
if item.find('senza-misura') is not None:
|
if item.find('senza-misura') is not None:
|
||||||
@@ -137,7 +123,7 @@ class Importer:
|
|||||||
elif field.tag == 'beat-type':
|
elif field.tag == 'beat-type':
|
||||||
if not pending:
|
if not pending:
|
||||||
self.fail('beat-type without beats')
|
self.fail('beat-type without beats')
|
||||||
self.meter['groups'].append({'beats':pending, 'unit':self.number(field.text, integer=True, positive=True)})
|
self.meter['groups'].append({'beats': pending, 'unit': self.number(field.text, integer=True, positive=True)})
|
||||||
pending = None
|
pending = None
|
||||||
if pending is not None or not self.meter['groups']:
|
if pending is not None or not self.meter['groups']:
|
||||||
self.fail('incomplete meter')
|
self.fail('incomplete meter')
|
||||||
@@ -146,25 +132,24 @@ class Importer:
|
|||||||
self.attrs(item, {'number', 'additional', 'size', 'after-barline', 'print-object'})
|
self.attrs(item, {'number', 'additional', 'size', 'after-barline', 'print-object'})
|
||||||
self.children(item, {'sign', 'line', 'clef-octave-change'})
|
self.children(item, {'sign', 'line', 'clef-octave-change'})
|
||||||
sign = item.findtext('sign')
|
sign = item.findtext('sign')
|
||||||
if sign not in ('G','F','C'):
|
if sign not in ('G', 'F', 'C'):
|
||||||
self.fail(f'unsupported clef {sign!r}; only pitched G/F/C staves are supported')
|
self.fail(f'unsupported clef {sign!r}; only pitched G/F/C staves are supported')
|
||||||
line = self.number(item.findtext('line', {'G':'2','F':'4','C':'3'}[sign]), integer=True)
|
line = self.number(item.findtext('line', {'G': '2', 'F': '4', 'C': '3'}[sign]), integer=True)
|
||||||
if not 1 <= line <= 5:
|
if not 1 <= line <= 5:
|
||||||
self.fail('clef line must be between 1 and 5')
|
self.fail('clef line must be between 1 and 5')
|
||||||
self.clef = {'sign':sign, 'line':line, 'octave_change':self.number(item.findtext('clef-octave-change','0'), integer=True)}
|
self.clef = {'sign': sign, 'line': line, 'octave_change': self.number(item.findtext('clef-octave-change', '0'), integer=True)}
|
||||||
result['clef'] = copy.deepcopy(self.clef)
|
result['clef'] = copy.deepcopy(self.clef)
|
||||||
elif item.tag == 'transpose':
|
elif item.tag == 'transpose':
|
||||||
self.children(item, {'diatonic', 'chromatic', 'octave-change'})
|
self.children(item, {'diatonic', 'chromatic', 'octave-change'})
|
||||||
self.transpose = {key:self.number(item.findtext(tag, '0'), integer=True) for key,tag in (('diatonic','diatonic'),('chromatic','chromatic'),('octave_change','octave-change'))}
|
self.transpose = {key: self.number(item.findtext(tag, '0'), integer=True) for key, tag in (('diatonic', 'diatonic'), ('chromatic', 'chromatic'), ('octave_change', 'octave-change'))}
|
||||||
if item.find('chromatic') is None:
|
if item.find('chromatic') is None:
|
||||||
self.fail('instrument transposition requires chromatic semitones')
|
self.fail('instrument transposition requires chromatic semitones')
|
||||||
if item.find('diatonic') is None:
|
if item.find('diatonic') is None:
|
||||||
semitones = self.transpose['chromatic']
|
semitones = self.transpose['chromatic']
|
||||||
magnitude = abs(semitones)
|
magnitude = abs(semitones)
|
||||||
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])
|
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)
|
result['transpose'] = copy.deepcopy(self.transpose)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def read_harmony(self, node, cursor: Fraction) -> dict:
|
def read_harmony(self, node, cursor: Fraction) -> dict:
|
||||||
self.attrs(node, {'print-object', 'print-frame', 'type'})
|
self.attrs(node, {'print-object', 'print-frame', 'type'})
|
||||||
self.children(node, {'root', 'kind', 'degree', 'bass', 'inversion', 'offset', 'staff'})
|
self.children(node, {'root', 'kind', 'degree', 'bass', 'inversion', 'offset', 'staff'})
|
||||||
@@ -172,34 +157,34 @@ class Importer:
|
|||||||
self.fail('stacked harmonies require separate harmony events')
|
self.fail('stacked harmonies require separate harmony events')
|
||||||
self.identity(node)
|
self.identity(node)
|
||||||
kind = node.find('kind')
|
kind = node.find('kind')
|
||||||
result = {'id':f'harmony-{self.sequence}-{cursor}', 'onset':rational(self.offset(node, cursor)), 'kind':kind.text, 'degrees':[]}
|
result = {'id': f'harmony-{self.sequence}-{cursor}', 'onset': rational(self.offset(node, cursor)), 'kind': kind.text, 'degrees': []}
|
||||||
self.attrs(kind, {'text', 'use-symbols', 'stack-degrees', 'parentheses-degrees', 'bracket-degrees'})
|
self.attrs(kind, {'text', 'use-symbols', 'stack-degrees', 'parentheses-degrees', 'bracket-degrees'})
|
||||||
if 'text' in kind.attrib:
|
if 'text' in kind.attrib:
|
||||||
result['kind_text'] = kind.get('text')
|
result['kind_text'] = kind.get('text')
|
||||||
for tag in ('root','bass'):
|
for tag in ('root', 'bass'):
|
||||||
item = node.find(tag)
|
item = node.find(tag)
|
||||||
if item is None:
|
if item is None:
|
||||||
if tag == 'root' and kind.text != 'none':
|
if tag == 'root' and kind.text != 'none':
|
||||||
self.fail('pitched harmony requires a root')
|
self.fail('pitched harmony requires a root')
|
||||||
continue
|
continue
|
||||||
self.children(item, {tag+'-step', tag+'-alter'})
|
self.children(item, {tag + '-step', tag + '-alter'})
|
||||||
step = item.findtext(tag+'-step','').lower()
|
step = item.findtext(tag + '-step', '').lower()
|
||||||
if len(step)!=1 or step not in 'cdefgab':
|
if len(step) != 1 or step not in 'cdefgab':
|
||||||
self.fail('invalid harmony root/bass')
|
self.fail('invalid harmony root/bass')
|
||||||
result[tag] = {'letter':step, 'alter':self.number(item.findtext(tag+'-alter','0'), integer=True)}
|
result[tag] = {'letter': step, 'alter': self.number(item.findtext(tag + '-alter', '0'), integer=True)}
|
||||||
for degree in node.findall('degree'):
|
for degree in node.findall('degree'):
|
||||||
self.children(degree, {'degree-value','degree-alter','degree-type'})
|
self.children(degree, {'degree-value', 'degree-alter', 'degree-type'})
|
||||||
value = self.number(degree.findtext('degree-value'), integer=True, positive=True)
|
value = self.number(degree.findtext('degree-value'), integer=True, positive=True)
|
||||||
alteration = self.number(degree.findtext('degree-alter','0'), integer=True)
|
alteration = self.number(degree.findtext('degree-alter', '0'), integer=True)
|
||||||
operation = degree.findtext('degree-type')
|
operation = degree.findtext('degree-type')
|
||||||
# MusicXML alterations are relative to the kind; additions use dominant intervals.
|
# MusicXML alterations are relative to the kind; additions use dominant intervals.
|
||||||
if operation == 'alter':
|
if operation == 'alter':
|
||||||
alteration += harmony.KINDS.get(result['kind'], {}).get(value, 0)
|
alteration += harmony.KINDS.get(result['kind'], {}).get(value, 0)
|
||||||
elif operation == 'add' and (value-1)%7 == 6:
|
elif operation == 'add' and (value - 1) % 7 == 6:
|
||||||
alteration -= 1
|
alteration -= 1
|
||||||
elif operation == 'subtract' and alteration:
|
elif operation == 'subtract' and alteration:
|
||||||
self.fail('cannot subtract a degree different from the one in the chord kind')
|
self.fail('cannot subtract a degree different from the one in the chord kind')
|
||||||
result['degrees'].append({'value':value, 'alter':alteration, 'type':operation})
|
result['degrees'].append({'value': value, 'alter': alteration, 'type': operation})
|
||||||
chord_degrees = harmony.degrees(result)
|
chord_degrees = harmony.degrees(result)
|
||||||
if 'kind_text' in result and result['kind'] != 'none':
|
if 'kind_text' in result and result['kind'] != 'none':
|
||||||
from generator.model import pitch_name
|
from generator.model import pitch_name
|
||||||
@@ -224,16 +209,15 @@ class Importer:
|
|||||||
bass_letter = 'cdefgab'[letter_index % 7]
|
bass_letter = 'cdefgab'[letter_index % 7]
|
||||||
from generator.model import NATURAL
|
from generator.model import NATURAL
|
||||||
target = NATURAL[root['letter']] + root['alter'] + harmony.degree_semitones(degree, chord_degrees[degree])
|
target = NATURAL[root['letter']] + root['alter'] + harmony.degree_semitones(degree, chord_degrees[degree])
|
||||||
result['bass'] = {'letter':bass_letter, 'alter':target - (12*(letter_index//7)+NATURAL[bass_letter])}
|
result['bass'] = {'letter': bass_letter, 'alter': target - (12 * (letter_index // 7) + NATURAL[bass_letter])}
|
||||||
result['symbol'] = harmony.symbol(result)
|
result['symbol'] = harmony.symbol(result)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def direction(self, node, cursor: Fraction, measure: dict, optional: list[Fraction]) -> None:
|
def direction(self, node, cursor: Fraction, measure: dict, optional: list[Fraction]) -> None:
|
||||||
self.attrs(node, {'print-object', 'directive'})
|
self.attrs(node, {'print-object', 'directive'})
|
||||||
self.children(node, {'direction-type','offset','staff','voice','sound'})
|
self.children(node, {'direction-type', 'offset', 'staff', 'voice', 'sound'})
|
||||||
onset = self.offset(node, cursor)
|
onset = self.offset(node, cursor)
|
||||||
for group in node.findall('direction-type'):
|
for group in node.findall('direction-type'):
|
||||||
self.children(group, {'words','octave-shift','rehearsal','metronome'})
|
self.children(group, {'words', 'octave-shift', 'rehearsal', 'metronome'})
|
||||||
for item in group:
|
for item in group:
|
||||||
if item.tag == 'words':
|
if item.tag == 'words':
|
||||||
value = ''.join(item.itertext()).strip()
|
value = ''.join(item.itertext()).strip()
|
||||||
@@ -246,15 +230,15 @@ class Importer:
|
|||||||
self.fail('section labels must be at measure starts')
|
self.fail('section labels must be at measure starts')
|
||||||
measure['section_label'] = ''.join(item.itertext())
|
measure['section_label'] = ''.join(item.itertext())
|
||||||
elif item.tag == 'octave-shift':
|
elif item.tag == 'octave-shift':
|
||||||
number, action = item.get('number','1'), item.get('type')
|
number, action = item.get('number', '1'), item.get('type')
|
||||||
size = self.number(item.get('size','8'), integer=True)
|
size = self.number(item.get('size', '8'), integer=True)
|
||||||
if size not in (8,15,22):
|
if size not in (8, 15, 22):
|
||||||
self.fail('octave shifts support 8, 15, or 22')
|
self.fail('octave shifts support 8, 15, or 22')
|
||||||
position = rational(self.absolute + onset)
|
position = rational(self.absolute + onset)
|
||||||
if action in ('up','down'):
|
if action in ('up', 'down'):
|
||||||
if number in self.open_octaves:
|
if number in self.open_octaves:
|
||||||
self.fail('duplicate octave-shift start')
|
self.fail('duplicate octave-shift start')
|
||||||
span = {'id':f'octave-{len(self.octaves)+1}', 'start':position, 'octaves':(size-1)//7 * (1 if action=='up' else -1)}
|
span = {'id': f'octave-{len(self.octaves) + 1}', 'start': position, 'octaves': (size - 1) // 7 * (1 if action == 'up' else -1)}
|
||||||
self.open_octaves[number] = span
|
self.open_octaves[number] = span
|
||||||
self.octaves.append(span)
|
self.octaves.append(span)
|
||||||
elif action == 'stop':
|
elif action == 'stop':
|
||||||
@@ -264,24 +248,25 @@ class Importer:
|
|||||||
elif action != 'continue' or number not in self.open_octaves:
|
elif action != 'continue' or number not in self.open_octaves:
|
||||||
self.fail('invalid octave-shift continuation')
|
self.fail('invalid octave-shift continuation')
|
||||||
elif item.tag == 'metronome':
|
elif item.tag == 'metronome':
|
||||||
self.children(item, {'beat-unit','beat-unit-dot','per-minute'})
|
self.children(item, {'beat-unit', 'beat-unit-dot', 'per-minute'})
|
||||||
written = {'type':item.findtext('beat-unit'), 'dots':len(item.findall('beat-unit-dot'))}
|
written = {'type': item.findtext('beat-unit'), 'dots': len(item.findall('beat-unit-dot'))}
|
||||||
written_length(written)
|
written_length(written)
|
||||||
bpm = self.number(item.findtext('per-minute'), positive=True)
|
bpm = self.number(item.findtext('per-minute'), positive=True)
|
||||||
measure.setdefault('tempos', []).append({'onset':rational(onset), 'written':written, 'bpm':rational(bpm)})
|
measure.setdefault('tempos', []).append({'onset': rational(onset), 'written': written, 'bpm': rational(bpm)})
|
||||||
sound = node.find('sound')
|
sound = node.find('sound')
|
||||||
if sound is not None:
|
if sound is not None:
|
||||||
self.attrs(sound, {'tempo'})
|
self.sound(sound, onset, measure)
|
||||||
self.children(sound, set())
|
def sound(self, node, onset: Fraction, measure: dict) -> None:
|
||||||
if sound.get('tempo') and not any(fraction(t['onset'])==onset for t in measure.get('tempos', [])):
|
self.attrs(node, {'tempo'})
|
||||||
measure.setdefault('tempos', []).append({'onset':rational(onset), 'written':{'type':'quarter','dots':0}, 'bpm':rational(self.number(sound.get('tempo'), positive=True))})
|
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]:
|
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'})
|
self.attrs(node, {'print-object', 'print-dot', 'print-spacing', 'default-x', 'dynamics', 'end-dynamics'})
|
||||||
if node.get('dynamics') or node.get('end-dynamics'):
|
if node.get('dynamics') or node.get('end-dynamics'):
|
||||||
self.fail('note dynamics are not supported')
|
self.fail('note dynamics are not supported')
|
||||||
self.children(node, {'pitch','rest','chord','duration','voice','type','dot','accidental','time-modification','stem','staff','beam','notations','lyric','tie','grace','cue','notehead'})
|
self.children(node, {'pitch', 'rest', 'chord', 'duration', 'voice', 'type', 'dot', 'accidental', 'time-modification', 'stem', 'staff', 'beam', 'notations', 'lyric', 'tie', 'grace', 'cue', 'notehead'})
|
||||||
for tag in ('pitch','rest','chord','duration','voice','type','accidental','time-modification','stem','staff','grace','cue','notehead'):
|
for tag in ('pitch', 'rest', 'chord', 'duration', 'voice', 'type', 'accidental', 'time-modification', 'stem', 'staff', 'grace', 'cue', 'notehead'):
|
||||||
if len(node.findall(tag)) > 1:
|
if len(node.findall(tag)) > 1:
|
||||||
self.fail(f'duplicate <{tag}> in <note>')
|
self.fail(f'duplicate <{tag}> in <note>')
|
||||||
if node.find('rest') is not None and node.find('pitch') is not None:
|
if node.find('rest') is not None and node.find('pitch') is not None:
|
||||||
@@ -291,22 +276,22 @@ class Importer:
|
|||||||
grace = node.find('grace')
|
grace = node.find('grace')
|
||||||
duration = Fraction() if grace is not None else self.number(node.findtext('duration'), positive=True) / self.divisions
|
duration = Fraction() if grace is not None else self.number(node.findtext('duration'), positive=True) / self.divisions
|
||||||
onset = fraction(previous['onset']) if is_chord and previous else cursor
|
onset = fraction(previous['onset']) if is_chord and previous else cursor
|
||||||
if is_chord and (previous is None or previous['kind']!='note' or duration > fraction(previous['duration'])):
|
if is_chord and (previous is None or previous['kind'] != 'note' or duration > fraction(previous['duration'])):
|
||||||
self.fail('invalid chord tone or chord tone longer than its anchor')
|
self.fail('invalid chord tone or chord tone longer than its anchor')
|
||||||
if is_chord and (grace is not None) != ('grace' in previous):
|
if is_chord and (grace is not None) != ('grace' in previous):
|
||||||
self.fail('chord tones must share grace/regular timing')
|
self.fail('chord tones must share grace/regular timing')
|
||||||
written = {'type':node.findtext('type'), 'dots':len(node.findall('dot'))}
|
written = {'type': node.findtext('type'), 'dots': len(node.findall('dot'))}
|
||||||
if written['type'] is None:
|
if written['type'] is None:
|
||||||
written = {'type':'whole','dots':0} if node.find('rest') is not None and node.find('rest').get('measure')=='yes' else written_for(duration)
|
written = {'type': 'whole', 'dots': 0} if node.find('rest') is not None and node.find('rest').get('measure') == 'yes' else written_for(duration)
|
||||||
written_length(written)
|
written_length(written)
|
||||||
modulation = node.find('time-modification')
|
modulation = node.find('time-modification')
|
||||||
if modulation is not None:
|
if modulation is not None:
|
||||||
self.children(modulation, {'actual-notes','normal-notes','normal-type','normal-dot'})
|
self.children(modulation, {'actual-notes', 'normal-notes', 'normal-type', 'normal-dot'})
|
||||||
actual = self.number(modulation.findtext('actual-notes'), integer=True, positive=True)
|
actual = self.number(modulation.findtext('actual-notes'), integer=True, positive=True)
|
||||||
normal = self.number(modulation.findtext('normal-notes'), integer=True, positive=True)
|
normal = self.number(modulation.findtext('normal-notes'), integer=True, positive=True)
|
||||||
written['ratio'] = rational(Fraction(normal, actual))
|
written['ratio'] = rational(Fraction(normal, actual))
|
||||||
if modulation.find('normal-type') is not None:
|
if modulation.find('normal-type') is not None:
|
||||||
written['normal'] = {'type':modulation.findtext('normal-type'), 'dots':len(modulation.findall('normal-dot'))}
|
written['normal'] = {'type': modulation.findtext('normal-type'), 'dots': len(modulation.findall('normal-dot'))}
|
||||||
written_length(written['normal'])
|
written_length(written['normal'])
|
||||||
elif modulation.findall('normal-dot'):
|
elif modulation.findall('normal-dot'):
|
||||||
self.fail('<normal-dot> requires <normal-type>')
|
self.fail('<normal-dot> requires <normal-type>')
|
||||||
@@ -316,47 +301,47 @@ class Importer:
|
|||||||
if duration != expected:
|
if duration != expected:
|
||||||
self.fail(f'<note> duration {duration} disagrees with written duration/time-modification {expected}')
|
self.fail(f'<note> duration {duration} disagrees with written duration/time-modification {expected}')
|
||||||
self.sequence += 1
|
self.sequence += 1
|
||||||
note = {'id':f'note-{self.sequence}', 'kind':'rest' if node.find('rest') is not None else 'note', 'onset':rational(onset), 'duration':rational(duration), 'written':written, 'hidden':node.get('print-object')=='no', 'pitches':[]}
|
note = {'id': f'note-{self.sequence}', 'kind': 'rest' if node.find('rest') is not None else 'note', 'onset': rational(onset), 'duration': rational(duration), 'written': written, 'hidden': node.get('print-object') == 'no', 'pitches': []}
|
||||||
if is_chord:
|
if is_chord:
|
||||||
note = previous
|
note = previous
|
||||||
if grace is not None:
|
if grace is not None:
|
||||||
self.attrs(grace, {'slash','steal-time-previous','steal-time-following','make-time'})
|
self.attrs(grace, {'slash', 'steal-time-previous', 'steal-time-following', 'make-time'})
|
||||||
note['grace'] = {'slash':grace.get('slash')=='yes'}
|
note['grace'] = {'slash': grace.get('slash') == 'yes'}
|
||||||
for name in ('steal-time-previous','steal-time-following','make-time'):
|
for name in ('steal-time-previous', 'steal-time-following', 'make-time'):
|
||||||
if name in grace.attrib:
|
if name in grace.attrib:
|
||||||
note['grace'][name] = rational(self.number(grace.get(name), positive=True))
|
note['grace'][name] = rational(self.number(grace.get(name), positive=True))
|
||||||
if node.find('cue') is not None:
|
if node.find('cue') is not None:
|
||||||
note['cue'] = True
|
note['cue'] = True
|
||||||
if node.find('rest') is not None:
|
if node.find('rest') is not None:
|
||||||
self.children(node.find('rest'), {'display-step','display-octave'})
|
self.children(node.find('rest'), {'display-step', 'display-octave'})
|
||||||
if node.find('rest').get('measure') == 'yes':
|
if node.find('rest').get('measure') == 'yes':
|
||||||
note['measure_rest'] = True
|
note['measure_rest'] = True
|
||||||
if node.find('rest/display-step') is not None:
|
if node.find('rest/display-step') is not None:
|
||||||
note['rest_position'] = {'letter':node.findtext('rest/display-step').lower(), 'octave':self.number(node.findtext('rest/display-octave'), integer=True)}
|
note['rest_position'] = {'letter': node.findtext('rest/display-step').lower(), 'octave': self.number(node.findtext('rest/display-octave'), integer=True)}
|
||||||
else:
|
else:
|
||||||
pitch_node = node.find('pitch')
|
pitch_node = node.find('pitch')
|
||||||
if pitch_node is None:
|
if pitch_node is None:
|
||||||
self.fail('note has neither pitch nor rest')
|
self.fail('note has neither pitch nor rest')
|
||||||
self.children(pitch_node, {'step','alter','octave'})
|
self.children(pitch_node, {'step', 'alter', 'octave'})
|
||||||
p = {'id':f'{note["id"]}-p{len(note["pitches"])+1}', 'letter':pitch_node.findtext('step','').lower(), 'alter':self.number(pitch_node.findtext('alter','0'), integer=True), 'octave':self.number(pitch_node.findtext('octave'), integer=True), 'duration':rational(duration), 'written':written}
|
p = {'id': f'{note["id"]}-p{len(note["pitches"]) + 1}', 'letter': pitch_node.findtext('step', '').lower(), 'alter': self.number(pitch_node.findtext('alter', '0'), integer=True), 'octave': self.number(pitch_node.findtext('octave'), integer=True), 'duration': rational(duration), 'written': written}
|
||||||
value = pitch_number(p)
|
value = pitch_number(p)
|
||||||
p['_absolute'] = self.absolute + onset
|
p['_absolute'] = self.absolute + onset
|
||||||
accidental = node.find('accidental')
|
accidental = node.find('accidental')
|
||||||
if accidental is not None:
|
if accidental is not None:
|
||||||
self.attrs(accidental, {'cautionary','editorial','parentheses','bracket','size','smufl'})
|
self.attrs(accidental, {'cautionary', 'editorial', 'parentheses', 'bracket', 'size', 'smufl'})
|
||||||
allowed = {'natural':0,'sharp':1,'flat':-1,'double-sharp':2,'sharp-sharp':2,'flat-flat':-2,'triple-sharp':3,'triple-flat':-3,'natural-sharp':1,'natural-flat':-1}
|
allowed = {'natural': 0, 'sharp': 1, 'flat': -1, 'double-sharp': 2, 'sharp-sharp': 2, 'flat-flat': -2, 'triple-sharp': 3, 'triple-flat': -3, 'natural-sharp': 1, 'natural-flat': -1}
|
||||||
if accidental.text not in allowed or allowed[accidental.text] != p['alter']:
|
if accidental.text not in allowed or allowed[accidental.text] != p['alter']:
|
||||||
self.fail('unsupported or contradictory accidental')
|
self.fail('unsupported or contradictory accidental')
|
||||||
p['accidental_display'] = {'name':accidental.text, 'parentheses':accidental.get('parentheses')=='yes' or accidental.get('cautionary')=='yes', 'bracket':accidental.get('bracket')=='yes'}
|
p['accidental_display'] = {'name': accidental.text, 'parentheses': accidental.get('parentheses') == 'yes' or accidental.get('cautionary') == 'yes', 'bracket': accidental.get('bracket') == 'yes'}
|
||||||
ties = {t.get('type') for t in node.findall('tie')}
|
ties = {t.get('type') for t in node.findall('tie')}
|
||||||
notations = node.find('notations')
|
notations = node.find('notations')
|
||||||
if notations is not None:
|
if notations is not None:
|
||||||
visual_ties = {t.get('type') for t in notations.findall('tied')}
|
visual_ties = {t.get('type') for t in notations.findall('tied')}
|
||||||
if 'continue' in visual_ties:
|
if 'continue' in visual_ties:
|
||||||
visual_ties.remove('continue')
|
visual_ties.remove('continue')
|
||||||
visual_ties.update({'start','stop'})
|
visual_ties.update({'start', 'stop'})
|
||||||
ties |= visual_ties
|
ties |= visual_ties
|
||||||
if ties - {'start','stop'}:
|
if ties - {'start', 'stop'}:
|
||||||
self.fail('unsupported tie notation')
|
self.fail('unsupported tie notation')
|
||||||
if 'stop' in ties:
|
if 'stop' in ties:
|
||||||
source = self.open_ties.pop(value, None)
|
source = self.open_ties.pop(value, None)
|
||||||
@@ -371,24 +356,24 @@ class Importer:
|
|||||||
if not is_chord:
|
if not is_chord:
|
||||||
if node.find('stem') is not None:
|
if node.find('stem') is not None:
|
||||||
stem = node.findtext('stem')
|
stem = node.findtext('stem')
|
||||||
if stem not in ('up','down','none','double'):
|
if stem not in ('up', 'down', 'none', 'double'):
|
||||||
self.fail('unsupported stem')
|
self.fail('unsupported stem')
|
||||||
note['stem'] = stem
|
note['stem'] = stem
|
||||||
head = node.findtext('notehead')
|
head = node.findtext('notehead')
|
||||||
if head is not None:
|
if head is not None:
|
||||||
if head not in ('normal','slash'):
|
if head not in ('normal', 'slash'):
|
||||||
self.fail(f'unsupported notehead: {head}')
|
self.fail(f'unsupported notehead: {head}')
|
||||||
note['notehead'] = head
|
note['notehead'] = head
|
||||||
beams = []
|
beams = []
|
||||||
for beam in node.findall('beam'):
|
for beam in node.findall('beam'):
|
||||||
level = self.number(beam.get('number','1'), integer=True)
|
level = self.number(beam.get('number', '1'), integer=True)
|
||||||
if not 1 <= level <= 8 or beam.text not in ('begin','continue','end','forward hook','backward hook'):
|
if not 1 <= level <= 8 or beam.text not in ('begin', 'continue', 'end', 'forward hook', 'backward hook'):
|
||||||
self.fail('invalid beam')
|
self.fail('invalid beam')
|
||||||
beams.append({'level':level,'type':beam.text})
|
beams.append({'level': level, 'type': beam.text})
|
||||||
if beams:
|
if beams:
|
||||||
note['beams'] = beams
|
note['beams'] = beams
|
||||||
for lyric in node.findall('lyric'):
|
for lyric in node.findall('lyric'):
|
||||||
self.children(lyric, {'syllabic','text','elision','extend','end-line','end-paragraph'})
|
self.children(lyric, {'syllabic', 'text', 'elision', 'extend', 'end-line', 'end-paragraph'})
|
||||||
number = lyric.get('number', '1')
|
number = lyric.get('number', '1')
|
||||||
if self.lyric_number is None:
|
if self.lyric_number is None:
|
||||||
self.lyric_number = number
|
self.lyric_number = number
|
||||||
@@ -396,22 +381,22 @@ class Importer:
|
|||||||
self.fail('only one lyric verse is supported')
|
self.fail('only one lyric verse is supported')
|
||||||
pieces = []
|
pieces = []
|
||||||
for item in lyric:
|
for item in lyric:
|
||||||
if item.tag in ('text','elision'):
|
if item.tag in ('text', 'elision'):
|
||||||
pieces.append(item.text or (' ' if item.tag=='elision' else ''))
|
pieces.append(item.text or (' ' if item.tag == 'elision' else ''))
|
||||||
syllabic = lyric.findtext('syllabic','single')
|
syllabic = lyric.findtext('syllabic', 'single')
|
||||||
if syllabic not in ('single','begin','middle','end'):
|
if syllabic not in ('single', 'begin', 'middle', 'end'):
|
||||||
self.fail('invalid lyric syllabification')
|
self.fail('invalid lyric syllabification')
|
||||||
note['lyric'] = ''.join(pieces) + ('-' if syllabic in ('begin','middle') else '')
|
note['lyric'] = ''.join(pieces) + ('-' if syllabic in ('begin', 'middle') else '')
|
||||||
note['lyric_notation'] = {'text':''.join(pieces), 'syllabic':syllabic}
|
note['lyric_notation'] = {'text': ''.join(pieces), 'syllabic': syllabic}
|
||||||
if lyric.find('extend') is not None:
|
if lyric.find('extend') is not None:
|
||||||
note['lyric_notation']['extend'] = lyric.find('extend').get('type','start')
|
note['lyric_notation']['extend'] = lyric.find('extend').get('type', 'start')
|
||||||
stops = []
|
stops = []
|
||||||
for notations in node.findall('notations'):
|
for notations in node.findall('notations'):
|
||||||
self.children(notations, {'tied','tuplet','ornaments'})
|
self.children(notations, {'tied', 'tuplet', 'ornaments'})
|
||||||
for item in notations:
|
for item in notations:
|
||||||
if item.tag == 'tuplet' and not is_chord:
|
if item.tag == 'tuplet' and not is_chord:
|
||||||
number, action = item.get('number','1'), item.get('type')
|
number, action = item.get('number', '1'), item.get('type')
|
||||||
self.children(item, {'tuplet-actual','tuplet-normal'})
|
self.children(item, {'tuplet-actual', 'tuplet-normal'})
|
||||||
if action == 'start':
|
if action == 'start':
|
||||||
if number in self.active_tuplets:
|
if number in self.active_tuplets:
|
||||||
self.fail('duplicate tuplet start')
|
self.fail('duplicate tuplet start')
|
||||||
@@ -420,7 +405,7 @@ class Importer:
|
|||||||
normal = item.findtext('tuplet-normal/tuplet-number', modulation.findtext('normal-notes') if modulation is not None else '')
|
normal = item.findtext('tuplet-normal/tuplet-number', modulation.findtext('normal-notes') if modulation is not None else '')
|
||||||
if self.active_tuplets and (item.find('tuplet-actual') is None or item.find('tuplet-normal') is None):
|
if self.active_tuplets and (item.find('tuplet-actual') is None or item.find('tuplet-normal') is None):
|
||||||
self.fail('nested tuplets require explicit tuplet-actual and tuplet-normal numbers')
|
self.fail('nested tuplets require explicit tuplet-actual and tuplet-normal numbers')
|
||||||
span = {'id':f'tuplet-{self.tuplet_sequence}', 'start':note['id'], 'note_count':self.number(actual, integer=True, positive=True), 'space_count':self.number(normal, integer=True, positive=True), 'depth':len(self.active_tuplets), 'bracket':item.get('bracket','auto'), 'show_number':item.get('show-number','actual')}
|
span = {'id': f'tuplet-{self.tuplet_sequence}', 'start': note['id'], 'note_count': self.number(actual, integer=True, positive=True), 'space_count': self.number(normal, integer=True, positive=True), 'depth': len(self.active_tuplets), 'bracket': item.get('bracket', 'auto'), 'show_number': item.get('show-number', 'actual')}
|
||||||
self.active_tuplets[number] = span
|
self.active_tuplets[number] = span
|
||||||
self.tuplets.append(span)
|
self.tuplets.append(span)
|
||||||
elif action == 'stop':
|
elif action == 'stop':
|
||||||
@@ -431,10 +416,10 @@ class Importer:
|
|||||||
self.children(item, {'tremolo'})
|
self.children(item, {'tremolo'})
|
||||||
for tremolo in item:
|
for tremolo in item:
|
||||||
marks = self.number(tremolo.text, integer=True)
|
marks = self.number(tremolo.text, integer=True)
|
||||||
if not 0 <= marks <= 8 or tremolo.get('type','single') == 'unmeasured':
|
if not 0 <= marks <= 8 or tremolo.get('type', 'single') == 'unmeasured':
|
||||||
self.fail('only measured tremolos are supported')
|
self.fail('only measured tremolos are supported')
|
||||||
action = tremolo.get('type','single')
|
action = tremolo.get('type', 'single')
|
||||||
note['tremolo'] = {'marks':marks, 'type':action}
|
note['tremolo'] = {'marks': marks, 'type': action}
|
||||||
if action == 'start':
|
if action == 'start':
|
||||||
if self.tremolo_start is not None:
|
if self.tremolo_start is not None:
|
||||||
self.fail('nested tremolo')
|
self.fail('nested tremolo')
|
||||||
@@ -454,10 +439,9 @@ class Importer:
|
|||||||
self.fail('tuplet stop without start')
|
self.fail('tuplet stop without start')
|
||||||
self.active_tuplets.pop(number)['end'] = note['id']
|
self.active_tuplets.pop(number)['end'] = note['id']
|
||||||
return note, cursor if is_chord else cursor + duration, is_chord
|
return note, cursor if is_chord else cursor + duration, is_chord
|
||||||
|
|
||||||
def read(self) -> dict:
|
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'):
|
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.fail('expected uncompressed score-partwise MusicXML 3.1 or 4.0')
|
||||||
self.children(root, IGNORED_TOP | {'part'})
|
self.children(root, IGNORED_TOP | {'part'})
|
||||||
parts = root.findall('part')
|
parts = root.findall('part')
|
||||||
@@ -466,32 +450,32 @@ class Importer:
|
|||||||
self.fail('exactly one matching score part is required')
|
self.fail('exactly one matching score part is required')
|
||||||
fields = {}
|
fields = {}
|
||||||
for item in root.findall('identification/miscellaneous/miscellaneous-field'):
|
for item in root.findall('identification/miscellaneous/miscellaneous-field'):
|
||||||
name = item.get('name','')
|
name = item.get('name', '')
|
||||||
if name.startswith('vamp:'):
|
if name.startswith('vamp:'):
|
||||||
if name not in ('vamp:style','vamp:tonic') or name in fields:
|
if name not in ('vamp:style', 'vamp:tonic') or name in fields:
|
||||||
self.fail(f'unknown or duplicate Vamp property {name!r}')
|
self.fail(f'unknown or duplicate Vamp property {name!r}')
|
||||||
fields[name] = item.text or ''
|
fields[name] = item.text or ''
|
||||||
metadata = {'title':root.findtext('work/work-title') or root.findtext('movement-title') or self.path.stem, 'composer':' / '.join(c.text for c in root.findall('identification/creator') if c.get('type')=='composer' and c.text), 'style':canonical_style(fields.get('vamp:style'))}
|
metadata = {'title': root.findtext('work/work-title') or root.findtext('movement-title') or self.path.stem, 'composer': ' / '.join(c.text for c in root.findall('identification/creator') if c.get('type') == 'composer' and c.text), 'style': canonical_style(fields.get('vamp:style'))}
|
||||||
if 'vamp:tonic' in fields:
|
if 'vamp:tonic' in fields:
|
||||||
from generator.model import parse_pitch_name
|
from generator.model import parse_pitch_name
|
||||||
metadata['transpose_origin'] = parse_pitch_name(fields['vamp:tonic'])
|
metadata['transpose_origin'] = parse_pitch_name(fields['vamp:tonic'])
|
||||||
measures = []
|
measures = []
|
||||||
self.children(parts[0], {'measure'})
|
self.children(parts[0], {'measure'})
|
||||||
for index, bar in enumerate(parts[0]):
|
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.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'})
|
self.attrs(bar, {'number', 'implicit', 'non-controlling', 'width', 'text'})
|
||||||
cursor = Fraction()
|
cursor = Fraction()
|
||||||
events = []
|
events = []
|
||||||
previous = None
|
previous = None
|
||||||
optional = []
|
optional = []
|
||||||
measure = {'id':f'measure-{index+1}', 'number':bar.get('number',str(index+1)), 'implicit':bar.get('implicit')=='yes', 'harmonies':[], 'attributes':[], 'key':copy.deepcopy(self.key), 'meter':copy.deepcopy(self.meter), 'clef':copy.deepcopy(self.clef), 'transpose':copy.deepcopy(self.transpose)}
|
measure = {'id': f'measure-{index + 1}', 'number': bar.get('number', str(index + 1)), 'implicit': bar.get('implicit') == 'yes', 'harmonies': [], 'attributes': [], 'key': copy.deepcopy(self.key), 'meter': copy.deepcopy(self.meter), 'clef': copy.deepcopy(self.clef), 'transpose': copy.deepcopy(self.transpose)}
|
||||||
for item in bar:
|
for item in bar:
|
||||||
if item.tag == 'attributes':
|
if item.tag == 'attributes':
|
||||||
state = self.attributes(item, cursor)
|
state = self.attributes(item, cursor)
|
||||||
if cursor == 0:
|
if cursor == 0:
|
||||||
for k in ('key','meter','clef','transpose'):
|
for k in ('key', 'meter', 'clef', 'transpose'):
|
||||||
measure[k] = copy.deepcopy(getattr(self,k))
|
measure[k] = copy.deepcopy(getattr(self, k))
|
||||||
elif len(state) > 1:
|
elif len(state) > 1:
|
||||||
measure['attributes'].append(state)
|
measure['attributes'].append(state)
|
||||||
elif item.tag == 'note':
|
elif item.tag == 'note':
|
||||||
@@ -502,12 +486,14 @@ class Importer:
|
|||||||
measure['harmonies'].append(self.read_harmony(item, cursor))
|
measure['harmonies'].append(self.read_harmony(item, cursor))
|
||||||
elif item.tag == 'direction':
|
elif item.tag == 'direction':
|
||||||
self.direction(item, cursor, measure, optional)
|
self.direction(item, cursor, measure, optional)
|
||||||
|
elif item.tag == 'sound':
|
||||||
|
self.sound(item, cursor, measure)
|
||||||
elif item.tag == 'backup':
|
elif item.tag == 'backup':
|
||||||
self.fail('backward timeline movement requires unsupported independent voices')
|
self.fail('backward timeline movement requires unsupported independent voices')
|
||||||
elif item.tag == 'forward':
|
elif item.tag == 'forward':
|
||||||
self.children(item, {'duration','voice','staff'})
|
self.children(item, {'duration', 'voice', 'staff'})
|
||||||
self.identity(item)
|
self.identity(item)
|
||||||
duration = self.number(item.findtext('duration'), positive=True)/self.divisions
|
duration = self.number(item.findtext('duration'), positive=True) / self.divisions
|
||||||
self.sequence += 1
|
self.sequence += 1
|
||||||
from generator.model import make_event
|
from generator.model import make_event
|
||||||
events.append(make_event(f'spacer-{self.sequence}', cursor, duration, 'spacer'))
|
events.append(make_event(f'spacer-{self.sequence}', cursor, duration, 'spacer'))
|
||||||
@@ -515,10 +501,10 @@ class Importer:
|
|||||||
previous = None
|
previous = None
|
||||||
elif item.tag == 'barline':
|
elif item.tag == 'barline':
|
||||||
self.children(item, {'bar-style'})
|
self.children(item, {'bar-style'})
|
||||||
style = item.findtext('bar-style','regular')
|
style = item.findtext('bar-style', 'regular')
|
||||||
if style not in ('regular','light-light','light-heavy','heavy-light','heavy-heavy','heavy','dashed','dotted','none','tick','short'):
|
if style not in ('regular', 'light-light', 'light-heavy', 'heavy-light', 'heavy-heavy', 'heavy', 'dashed', 'dotted', 'none', 'tick', 'short'):
|
||||||
self.fail('unsupported barline')
|
self.fail('unsupported barline')
|
||||||
measure.setdefault('barlines', {})[item.get('location','right')] = style
|
measure.setdefault('barlines', {})[item.get('location', 'right')] = style
|
||||||
if not events or cursor <= 0:
|
if not events or cursor <= 0:
|
||||||
self.fail('empty or zero-duration measure')
|
self.fail('empty or zero-duration measure')
|
||||||
expected = meter_length(measure['meter'])
|
expected = meter_length(measure['meter'])
|
||||||
@@ -527,12 +513,12 @@ class Importer:
|
|||||||
self.fail(f'measure has {cursor} quarter notes, expected {expected}; mark a partial measure implicit')
|
self.fail(f'measure has {cursor} quarter notes, expected {expected}; mark a partial measure implicit')
|
||||||
measure['duration'] = rational(cursor)
|
measure['duration'] = rational(cursor)
|
||||||
measure['key_signature'], measure['time_signature'] = key_label(measure['key']), meter_label(measure['meter'])
|
measure['key_signature'], measure['time_signature'] = key_label(measure['key']), meter_label(measure['meter'])
|
||||||
measure['voices'] = [{'id':'voice-source', 'notes':events}]
|
measure['voices'] = [{'id': 'voice-source', 'notes': events}]
|
||||||
measure['harmonies'].sort(key=lambda h:fraction(h['onset']))
|
measure['harmonies'].sort(key=lambda h: fraction(h['onset']))
|
||||||
used = set()
|
used = set()
|
||||||
for onset in optional:
|
for onset in optional:
|
||||||
matches = [h for h in measure['harmonies'] if fraction(h['onset'])==onset]
|
matches = [h for h in measure['harmonies'] if fraction(h['onset']) == onset]
|
||||||
if len(matches)!=1 or onset in used:
|
if len(matches) != 1 or onset in used:
|
||||||
self.fail('optional-chord marker must identify exactly one harmony and occur only once')
|
self.fail('optional-chord marker must identify exactly one harmony and occur only once')
|
||||||
matches[0]['chord_optional'] = True
|
matches[0]['chord_optional'] = True
|
||||||
used.add(onset)
|
used.add(onset)
|
||||||
@@ -546,12 +532,11 @@ class Importer:
|
|||||||
for n in measure['voices'][0]['notes']:
|
for n in measure['voices'][0]['notes']:
|
||||||
for p in n['pitches']:
|
for p in n['pitches']:
|
||||||
p.pop('_absolute', None)
|
p.pop('_absolute', None)
|
||||||
metadata.update({'key_signature':measures[0]['key_signature'], 'time_signature':measures[0]['time_signature'], 'key':copy.deepcopy(measures[0]['key'])})
|
metadata.update({'key_signature': measures[0]['key_signature'], 'time_signature': measures[0]['time_signature'], 'key': copy.deepcopy(measures[0]['key'])})
|
||||||
result = {'schema_version':SCHEMA_VERSION, 'metadata':metadata, 'staves':[{'id':'staff-source', 'name':definitions[0].findtext('part-name','Source'), 'role':'vocal', 'clef':copy.deepcopy(measures[0]['clef']), 'tuplets':self.tuplets, 'octave_shifts':self.octaves, 'measures':measures}]}
|
result = {'schema_version': SCHEMA_VERSION, 'metadata': metadata, 'staves': [{'id': 'staff-source', 'name': definitions[0].findtext('part-name', 'Source'), 'role': 'vocal', 'clef': copy.deepcopy(measures[0]['clef']), 'tuplets': self.tuplets, 'octave_shifts': self.octaves, 'measures': measures}]}
|
||||||
validate_document(result)
|
validate_document(result)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
def load_musicxml(path: Path) -> dict:
|
def load_musicxml(path: Path) -> dict:
|
||||||
importer = Importer(Path(path))
|
importer = Importer(Path(path))
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -1,2 +1 @@
|
|||||||
"""Practice score generation."""
|
"""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.musicxml import load_musicxml
|
||||||
from generator.transposition import TARGET_KEYS, transpose_document
|
from generator.transposition import TARGET_KEYS, transpose_document
|
||||||
|
|
||||||
|
|
||||||
def practice_document(document: dict) -> dict:
|
def practice_document(document: dict) -> dict:
|
||||||
staff = source_staff(document)
|
staff = source_staff(document)
|
||||||
source_measures = staff.get("measures")
|
source_measures = staff.get("measures")
|
||||||
@@ -36,11 +35,9 @@ def practice_document(document: dict) -> dict:
|
|||||||
octave_shifts=staff.get('octave_shifts', []),
|
octave_shifts=staff.get('octave_shifts', []),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def key_file_name(key: str) -> str:
|
def key_file_name(key: str) -> str:
|
||||||
return slugify(key.replace("#", "sharp")) + ".json"
|
return slugify(key.replace("#", "sharp")) + ".json"
|
||||||
|
|
||||||
|
|
||||||
def generate_chord_practice(source_path: Path, source_root: Path, output_dir: Path) -> int:
|
def generate_chord_practice(source_path: Path, source_root: Path, output_dir: Path) -> int:
|
||||||
document = practice_document(load_musicxml(source_path))
|
document = practice_document(load_musicxml(source_path))
|
||||||
metadata = document.get("metadata", {})
|
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)
|
write_json(output_dir / "chord" / lesson_group / exercise_id / key_file_name(key), document)
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
|
|
||||||
def generate_melody_practice(source_path: Path, source_root: Path, output_dir: Path) -> int:
|
def generate_melody_practice(source_path: Path, source_root: Path, output_dir: Path) -> int:
|
||||||
base_document = practice_document(load_musicxml(source_path))
|
base_document = practice_document(load_musicxml(source_path))
|
||||||
relative = source_path.relative_to(source_root)
|
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
|
count += 1
|
||||||
return count
|
return count
|
||||||
|
|
||||||
|
|
||||||
def generate_practice_scores(source_dir: Path, output_dir: Path) -> int:
|
def generate_practice_scores(source_dir: Path, output_dir: Path) -> int:
|
||||||
if not source_dir.exists():
|
if not source_dir.exists():
|
||||||
return 0
|
return 0
|
||||||
count = 0
|
count = 0
|
||||||
chord_root = source_dir / "chord"
|
chord_root = source_dir / "chord"
|
||||||
if chord_root.exists():
|
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)
|
count += generate_chord_practice(path, chord_root, output_dir)
|
||||||
melody_root = source_dir / "melody"
|
melody_root = source_dir / "melody"
|
||||||
if melody_root.exists():
|
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)
|
count += generate_melody_practice(path, melody_root, output_dir)
|
||||||
return count
|
return count
|
||||||
|
|||||||
+12
-27
@@ -7,51 +7,41 @@ from pathlib import Path
|
|||||||
|
|
||||||
from generator.model import SCHEMA_VERSION, validate_document
|
from generator.model import SCHEMA_VERSION, validate_document
|
||||||
|
|
||||||
|
|
||||||
def load_json(path: Path) -> dict:
|
def load_json(path: Path) -> dict:
|
||||||
document = json.loads(path.read_text(encoding='utf-8'))
|
document = json.loads(path.read_text(encoding='utf-8'))
|
||||||
validate_document(document)
|
validate_document(document)
|
||||||
return document
|
return document
|
||||||
|
|
||||||
|
|
||||||
def write_json(path: Path, document: dict) -> None:
|
def write_json(path: Path, document: dict) -> None:
|
||||||
validate_document(document)
|
validate_document(document)
|
||||||
path.parent.mkdir(parents=True, exist_ok=True)
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
path.write_text(json.dumps(document, indent=2, ensure_ascii=False) + '\n', encoding='utf-8')
|
path.write_text(json.dumps(document, indent=2, ensure_ascii=False) + '\n', encoding='utf-8')
|
||||||
|
|
||||||
|
|
||||||
def slugify(value: str) -> str:
|
def slugify(value: str) -> str:
|
||||||
return re.sub(r'[^a-z0-9]+', '-', value.strip().lower()).strip('-') or 'score'
|
return re.sub(r'[^a-z0-9]+', '-', value.strip().lower()).strip('-') or 'score'
|
||||||
|
|
||||||
|
|
||||||
def human_label(value: str) -> str:
|
def human_label(value: str) -> str:
|
||||||
return ' '.join(v.capitalize() for v in value.replace('_',' ').replace('-',' ').split()) or 'General'
|
return ' '.join(v.capitalize() for v in value.replace('_', ' ').replace('-', ' ').split()) or 'General'
|
||||||
|
|
||||||
|
|
||||||
def source_staff(document: dict) -> dict:
|
def source_staff(document: dict) -> dict:
|
||||||
if len(document.get('staves', [])) != 1:
|
if len(document.get('staves', [])) != 1:
|
||||||
raise ValueError('exactly one source staff is required')
|
raise ValueError('exactly one source staff is required')
|
||||||
return document['staves'][0]
|
return document['staves'][0]
|
||||||
|
|
||||||
|
|
||||||
def first_voice(measure: dict) -> dict:
|
def first_voice(measure: dict) -> dict:
|
||||||
if len(measure.get('voices', [])) != 1:
|
if len(measure.get('voices', [])) != 1:
|
||||||
raise ValueError('exactly one source voice is required')
|
raise ValueError('exactly one source voice is required')
|
||||||
return measure['voices'][0]
|
return measure['voices'][0]
|
||||||
|
|
||||||
|
|
||||||
def measure_notes(measure: dict) -> list[dict]:
|
def measure_notes(measure: dict) -> list[dict]:
|
||||||
return first_voice(measure)['notes']
|
return first_voice(measure)['notes']
|
||||||
|
|
||||||
|
|
||||||
def leading_pickup_measure_index(measures: list[dict]) -> int | None:
|
def leading_pickup_measure_index(measures: list[dict]) -> int | None:
|
||||||
return 0 if len(measures)>1 and measures[0].get('implicit') else None
|
return 0 if len(measures) > 1 and measures[0].get('implicit') else None
|
||||||
|
|
||||||
|
|
||||||
def body_start_measure_index(measures: list[dict]) -> int:
|
def body_start_measure_index(measures: list[dict]) -> int:
|
||||||
return 1 if leading_pickup_measure_index(measures) is not None else 0
|
return 1 if leading_pickup_measure_index(measures) is not None else 0
|
||||||
|
|
||||||
|
|
||||||
def copy_source_voice(measure: dict, *, strip_lyrics: bool) -> dict:
|
def copy_source_voice(measure: dict, *, strip_lyrics: bool) -> dict:
|
||||||
voice = copy.deepcopy(first_voice(measure))
|
voice = copy.deepcopy(first_voice(measure))
|
||||||
if strip_lyrics:
|
if strip_lyrics:
|
||||||
@@ -60,13 +50,11 @@ def copy_source_voice(measure: dict, *, strip_lyrics: bool) -> dict:
|
|||||||
note.pop('lyric_notation', None)
|
note.pop('lyric_notation', None)
|
||||||
return voice
|
return voice
|
||||||
|
|
||||||
|
|
||||||
def make_measure(measure_id: str, source: dict, notes: list[dict], *, section_label: str = '') -> dict:
|
def make_measure(measure_id: str, source: dict, notes: list[dict], *, section_label: str = '') -> dict:
|
||||||
result = copy.deepcopy(source)
|
result = copy.deepcopy(source)
|
||||||
result.update({'id':measure_id, 'section_label':section_label, 'voices':[{'id':'voice-arranged', 'notes':notes}]})
|
result.update({'id': measure_id, 'section_label': section_label, 'voices': [{'id': 'voice-arranged', 'notes': notes}]})
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
def reidentify(measures: list[dict], source_tuplets: list[dict]) -> list[dict]:
|
def reidentify(measures: list[dict], source_tuplets: list[dict]) -> list[dict]:
|
||||||
"""Clone-local links survive; links cut by an arrangement boundary become attacks."""
|
"""Clone-local links survive; links cut by an arrangement boundary become attacks."""
|
||||||
tone_map, event_map = {}, {}
|
tone_map, event_map = {}, {}
|
||||||
@@ -74,47 +62,44 @@ def reidentify(measures: list[dict], source_tuplets: list[dict]) -> list[dict]:
|
|||||||
for measure in measures:
|
for measure in measures:
|
||||||
for ni, note in enumerate(measure_notes(measure)):
|
for ni, note in enumerate(measure_notes(measure)):
|
||||||
old = note['id']
|
old = note['id']
|
||||||
note['id'] = f'{measure["id"]}-n{ni+1}'
|
note['id'] = f'{measure["id"]}-n{ni + 1}'
|
||||||
event_map[old] = note['id']
|
event_map[old] = note['id']
|
||||||
for pi, pitch in enumerate(note['pitches']):
|
for pi, pitch in enumerate(note['pitches']):
|
||||||
old_pitch = pitch['id']
|
old_pitch = pitch['id']
|
||||||
pitch['id'] = f'{note["id"]}-p{pi+1}'
|
pitch['id'] = f'{note["id"]}-p{pi + 1}'
|
||||||
tone_map[old_pitch] = pitch['id']
|
tone_map[old_pitch] = pitch['id']
|
||||||
for measure in measures:
|
for measure in measures:
|
||||||
for note in measure_notes(measure):
|
for note in measure_notes(measure):
|
||||||
for p in note['pitches']:
|
for p in note['pitches']:
|
||||||
for field in ('tie_from','tie_to'):
|
for field in ('tie_from', 'tie_to'):
|
||||||
if p.get(field) in tone_map:
|
if p.get(field) in tone_map:
|
||||||
p[field] = tone_map[p[field]]
|
p[field] = tone_map[p[field]]
|
||||||
else:
|
else:
|
||||||
p.pop(field,None)
|
p.pop(field, None)
|
||||||
if note.get('tremolo', {}).get('partner'):
|
if note.get('tremolo', {}).get('partner'):
|
||||||
partner = note['tremolo']['partner']
|
partner = note['tremolo']['partner']
|
||||||
if partner in event_map:
|
if partner in event_map:
|
||||||
note['tremolo']['partner'] = event_map[partner]
|
note['tremolo']['partner'] = event_map[partner]
|
||||||
else:
|
else:
|
||||||
note['tremolo'] = {**note['tremolo'], 'type':'single'}
|
note['tremolo'] = {**note['tremolo'], 'type': 'single'}
|
||||||
note['tremolo'].pop('partner',None)
|
note['tremolo'].pop('partner', None)
|
||||||
for source in source_tuplets:
|
for source in source_tuplets:
|
||||||
members = [n for m in measures for n in measure_notes(m) if source['id'] in n.get('tuplet_ids', [])]
|
members = [n for m in measures for n in measure_notes(m) if source['id'] in n.get('tuplet_ids', [])]
|
||||||
if not members:
|
if not members:
|
||||||
continue
|
continue
|
||||||
t = copy.deepcopy(source)
|
t = copy.deepcopy(source)
|
||||||
t.update({'id':f'{measures[0]["id"]}-{source["id"]}', 'start':members[0]['id'], 'end':members[-1]['id'], 'continued_start':source['start'] not in event_map, 'continued_end':source['end'] not in event_map})
|
t.update({'id': f'{measures[0]["id"]}-{source["id"]}', 'start': members[0]['id'], 'end': members[-1]['id'], 'continued_start': source['start'] not in event_map, 'continued_end': source['end'] not in event_map})
|
||||||
for n in members:
|
for n in members:
|
||||||
n['tuplet_ids'] = [t['id'] if v==source['id'] else v for v in n['tuplet_ids']]
|
n['tuplet_ids'] = [t['id'] if v == source['id'] else v for v in n['tuplet_ids']]
|
||||||
tuplets.append(t)
|
tuplets.append(t)
|
||||||
return tuplets
|
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:
|
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 []}]}
|
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:
|
def count_json_files(root: Path) -> int:
|
||||||
return sum(1 for p in root.rglob('*.json') if p.is_file())
|
return sum(1 for p in root.rglob('*.json') if p.is_file())
|
||||||
|
|
||||||
|
|
||||||
def validate_generated_tree(root: Path) -> None:
|
def validate_generated_tree(root: Path) -> None:
|
||||||
for path in root.rglob('*.json'):
|
for path in root.rglob('*.json'):
|
||||||
load_json(path)
|
load_json(path)
|
||||||
|
|||||||
+67
-72
@@ -3,83 +3,78 @@ from __future__ import annotations
|
|||||||
DEFAULT_STYLE = "Medium Swing"
|
DEFAULT_STYLE = "Medium Swing"
|
||||||
|
|
||||||
BUILT_IN_STYLES = (
|
BUILT_IN_STYLES = (
|
||||||
# Jazz
|
# Jazz
|
||||||
"Afro 12/8",
|
"Afro 12/8",
|
||||||
"Ballad Double Time Feel",
|
"Ballad Double Time Feel",
|
||||||
"Ballad Even",
|
"Ballad Even",
|
||||||
"Ballad Melodic",
|
"Ballad Melodic",
|
||||||
"Ballad Swing",
|
"Ballad Swing",
|
||||||
"Blue Note",
|
"Blue Note",
|
||||||
"Bossa Nova",
|
"Bossa Nova",
|
||||||
"Doo Doo Cats",
|
"Doo Doo Cats",
|
||||||
"Double Time Swing",
|
"Double Time Swing",
|
||||||
"Even 8ths",
|
"Even 8ths",
|
||||||
"Even 8ths Open",
|
"Even 8ths Open",
|
||||||
"Even 16ths",
|
"Even 16ths",
|
||||||
"Guitar Trio",
|
"Guitar Trio",
|
||||||
"Gypsy Jazz",
|
"Gypsy Jazz",
|
||||||
"Latin",
|
"Latin",
|
||||||
"Latin/Swing",
|
"Latin/Swing",
|
||||||
"Long Notes",
|
"Long Notes",
|
||||||
"Medium Swing",
|
"Medium Swing",
|
||||||
"Medium Up Swing",
|
"Medium Up Swing",
|
||||||
"Medium Up Swing 2",
|
"Medium Up Swing 2",
|
||||||
"New Orleans Swing",
|
"New Orleans Swing",
|
||||||
"Second Line",
|
"Second Line",
|
||||||
"Slow Swing",
|
"Slow Swing",
|
||||||
"Swing Two/Four",
|
"Swing Two/Four",
|
||||||
"Trad Jazz",
|
"Trad Jazz",
|
||||||
"Up Tempo Swing",
|
"Up Tempo Swing",
|
||||||
"Up Tempo Swing 2",
|
"Up Tempo Swing 2",
|
||||||
# Latin
|
# Latin
|
||||||
"Argentina: Tango",
|
"Argentina: Tango",
|
||||||
"Brazil: Bossa Acoustic",
|
"Brazil: Bossa Acoustic",
|
||||||
"Brazil: Bossa Electric",
|
"Brazil: Bossa Electric",
|
||||||
"Brazil: Samba",
|
"Brazil: Samba",
|
||||||
"Cuba: Bolero",
|
"Cuba: Bolero",
|
||||||
"Cuba: Cha Cha Cha",
|
"Cuba: Cha Cha Cha",
|
||||||
"Cuba: Son Montuno 2-3",
|
"Cuba: Son Montuno 2-3",
|
||||||
"Cuba: Son Montuno 3-2",
|
"Cuba: Son Montuno 3-2",
|
||||||
# Pop
|
# Pop
|
||||||
"Bluegrass",
|
"Bluegrass",
|
||||||
"Country",
|
"Country",
|
||||||
"Disco",
|
"Disco",
|
||||||
"Funk",
|
"Funk",
|
||||||
"Glam Funk",
|
"Glam Funk",
|
||||||
"House",
|
"House",
|
||||||
"Reggae",
|
"Reggae",
|
||||||
"RnB",
|
"RnB",
|
||||||
"Rock",
|
"Rock",
|
||||||
"Rock 12/8",
|
"Rock 12/8",
|
||||||
"Shuffle",
|
"Shuffle",
|
||||||
"Slow Rock",
|
"Slow Rock",
|
||||||
"Smooth",
|
"Smooth",
|
||||||
"Soul",
|
"Soul",
|
||||||
"Virtual Funk",
|
"Virtual Funk",
|
||||||
)
|
)
|
||||||
|
|
||||||
BUILT_IN_STYLE_SET = frozenset(BUILT_IN_STYLES)
|
BUILT_IN_STYLE_SET = frozenset(BUILT_IN_STYLES)
|
||||||
|
|
||||||
STYLE_ALIASES = {
|
STYLE_ALIASES = {
|
||||||
"Med.": DEFAULT_STYLE,
|
"Med.": DEFAULT_STYLE,
|
||||||
"Med. Swing": DEFAULT_STYLE,
|
"Med. Swing": DEFAULT_STYLE,
|
||||||
"Medium": DEFAULT_STYLE,
|
"Medium": DEFAULT_STYLE,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def canonical_style(value: object) -> str:
|
def canonical_style(value: object) -> str:
|
||||||
if value is None:
|
if value is None:
|
||||||
return DEFAULT_STYLE
|
return DEFAULT_STYLE
|
||||||
|
if not isinstance(value, str):
|
||||||
if not isinstance(value, str):
|
raise ValueError("style must be a string")
|
||||||
raise ValueError("style must be a string")
|
style = value.strip()
|
||||||
|
if not style:
|
||||||
style = value.strip()
|
return DEFAULT_STYLE
|
||||||
if not style:
|
style = STYLE_ALIASES.get(style, style)
|
||||||
return DEFAULT_STYLE
|
if style not in BUILT_IN_STYLE_SET:
|
||||||
|
raise ValueError(f"style {value!r} is not a supported player style")
|
||||||
style = STYLE_ALIASES.get(style, style)
|
return style
|
||||||
if style not in BUILT_IN_STYLE_SET:
|
|
||||||
raise ValueError(f"style {value!r} is not a supported player style")
|
|
||||||
|
|
||||||
return style
|
|
||||||
|
|||||||
+3
-12
@@ -8,14 +8,12 @@ from generator.score import load_json, measure_notes
|
|||||||
from generator.transposition import TARGET_KEYS
|
from generator.transposition import TARGET_KEYS
|
||||||
from generator.model import fraction, pitch_name
|
from generator.model import fraction, pitch_name
|
||||||
|
|
||||||
|
|
||||||
def _arranged_staff(document: dict[str, Any]) -> dict[str, Any]:
|
def _arranged_staff(document: dict[str, Any]) -> dict[str, Any]:
|
||||||
staves = document.get("staves")
|
staves = document.get("staves")
|
||||||
if not isinstance(staves, list) or not staves or not isinstance(staves[0], dict):
|
if not isinstance(staves, list) or not staves or not isinstance(staves[0], dict):
|
||||||
raise ValueError("generated score document must contain an arranged staff")
|
raise ValueError("generated score document must contain an arranged staff")
|
||||||
return staves[0]
|
return staves[0]
|
||||||
|
|
||||||
|
|
||||||
def _measure_summary(staff: dict[str, Any], measure: dict[str, Any], measure_number: int) -> dict[str, Any]:
|
def _measure_summary(staff: dict[str, Any], measure: dict[str, Any], measure_number: int) -> dict[str, Any]:
|
||||||
notes = measure_notes(measure)
|
notes = measure_notes(measure)
|
||||||
summary: dict[str, Any] = {
|
summary: dict[str, Any] = {
|
||||||
@@ -28,14 +26,13 @@ def _measure_summary(staff: dict[str, Any], measure: dict[str, Any], measure_num
|
|||||||
if staff.get("arrangement_part_type") == "vocal":
|
if staff.get("arrangement_part_type") == "vocal":
|
||||||
summary["vocal_skeleton"] = [
|
summary["vocal_skeleton"] = [
|
||||||
f'{fraction(note["onset"])}+{fraction(note["duration"])} '
|
f'{fraction(note["onset"])}+{fraction(note["duration"])} '
|
||||||
+ note['written']['type'] + '.'*note['written'].get('dots',0)
|
+ note['written']['type'] + '.' * note['written'].get('dots', 0)
|
||||||
+ ' ' + note['kind'] + ' ' + ','.join(pitch_name(p)+str(p['octave']) for p in note['pitches'])
|
+ ' ' + note['kind'] + ' ' + ','.join(pitch_name(p) + str(p['octave']) for p in note['pitches'])
|
||||||
+ ' ' + note.get('lyric','')
|
+ ' ' + note.get('lyric', '')
|
||||||
for note in notes
|
for note in notes
|
||||||
]
|
]
|
||||||
return summary
|
return summary
|
||||||
|
|
||||||
|
|
||||||
def performance_document_summary(document: dict[str, Any]) -> dict[str, Any]:
|
def performance_document_summary(document: dict[str, Any]) -> dict[str, Any]:
|
||||||
metadata = document.get("metadata")
|
metadata = document.get("metadata")
|
||||||
if not isinstance(metadata, dict):
|
if not isinstance(metadata, dict):
|
||||||
@@ -63,14 +60,12 @@ def performance_document_summary(document: dict[str, Any]) -> dict[str, Any]:
|
|||||||
"measures": measure_summaries,
|
"measures": measure_summaries,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def performance_tree_summary(root: Path) -> dict[str, Any]:
|
def performance_tree_summary(root: Path) -> dict[str, Any]:
|
||||||
return {
|
return {
|
||||||
path.name: performance_document_summary(load_json(path))
|
path.name: performance_document_summary(load_json(path))
|
||||||
for path in sorted(root.glob("*.json"))
|
for path in sorted(root.glob("*.json"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def validate_expected_performance_summary(expected_path: Path, generated_songs_dir: Path) -> None:
|
def validate_expected_performance_summary(expected_path: Path, generated_songs_dir: Path) -> None:
|
||||||
if not expected_path.exists():
|
if not expected_path.exists():
|
||||||
raise ValueError(f"missing generator compatibility summary: {expected_path}")
|
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)
|
actual = performance_tree_summary(generated_songs_dir)
|
||||||
if actual == expected:
|
if actual == expected:
|
||||||
return
|
return
|
||||||
|
|
||||||
expected_names = set(expected.keys()) if isinstance(expected, dict) else set()
|
expected_names = set(expected.keys()) if isinstance(expected, dict) else set()
|
||||||
actual_names = set(actual.keys())
|
actual_names = set(actual.keys())
|
||||||
missing = sorted(expected_names - actual_names)
|
missing = sorted(expected_names - actual_names)
|
||||||
extra = sorted(actual_names - expected_names)
|
extra = sorted(actual_names - expected_names)
|
||||||
if missing or extra:
|
if missing or extra:
|
||||||
raise ValueError(f"performance generator drift: missing={missing}, extra={extra}")
|
raise ValueError(f"performance generator drift: missing={missing}, extra={extra}")
|
||||||
|
|
||||||
for name in sorted(actual_names):
|
for name in sorted(actual_names):
|
||||||
if actual[name] != expected[name]:
|
if actual[name] != expected[name]:
|
||||||
raise ValueError(f"performance generator drift in {name}; update generator logic or refresh the expected summary intentionally")
|
raise ValueError(f"performance generator drift in {name}; update generator logic or refresh the expected summary intentionally")
|
||||||
raise ValueError("performance generator drift")
|
raise ValueError("performance generator drift")
|
||||||
|
|
||||||
|
|
||||||
def validate_practice_variants(generated_practice_dir: Path) -> None:
|
def validate_practice_variants(generated_practice_dir: Path) -> None:
|
||||||
if not generated_practice_dir.exists():
|
if not generated_practice_dir.exists():
|
||||||
return
|
return
|
||||||
@@ -105,7 +97,6 @@ def validate_practice_variants(generated_practice_dir: Path) -> None:
|
|||||||
continue
|
continue
|
||||||
if json_names != expected_names:
|
if json_names != expected_names:
|
||||||
raise ValueError(f"melody practice generator drift in {exercise_dir}: expected {sorted(expected_names)}, found {sorted(json_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"
|
chord_root = generated_practice_dir / "chord"
|
||||||
if chord_root.exists():
|
if chord_root.exists():
|
||||||
for exercise_dir in sorted(path for path in chord_root.rglob("*") if path.is_dir()):
|
for exercise_dir in sorted(path for path in chord_root.rglob("*") if path.is_dir()):
|
||||||
|
|||||||
+17
-27
@@ -6,22 +6,18 @@ from dataclasses import dataclass
|
|||||||
from generator import harmony
|
from generator import harmony
|
||||||
from generator.model import LETTERS, NATURAL, key_from_fifths, key_label, parse_pitch_name, pitch_number
|
from generator.model import LETTERS, NATURAL, key_from_fifths, key_label, parse_pitch_name, pitch_number
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class KeyProfile:
|
class KeyProfile:
|
||||||
name: str
|
name: str
|
||||||
root: int
|
root: int
|
||||||
fifths: 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))))
|
||||||
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)
|
TARGET_KEYS = tuple(p.name for p in KEY_PROFILES)
|
||||||
|
|
||||||
|
|
||||||
def mod12(value: int) -> int:
|
def mod12(value: int) -> int:
|
||||||
return value % 12
|
return value % 12
|
||||||
|
|
||||||
|
|
||||||
def find_key(name: str) -> KeyProfile:
|
def find_key(name: str) -> KeyProfile:
|
||||||
tonic = parse_pitch_name(name)
|
tonic = parse_pitch_name(name)
|
||||||
for fifths in range(-14, 15):
|
for fifths in range(-14, 15):
|
||||||
@@ -30,11 +26,9 @@ def find_key(name: str) -> KeyProfile:
|
|||||||
return KeyProfile(name, (NATURAL[tonic['letter']] + tonic['alter']) % 12, fifths)
|
return KeyProfile(name, (NATURAL[tonic['letter']] + tonic['alter']) % 12, fifths)
|
||||||
raise ValueError(f'unsupported key signature: {name}')
|
raise ValueError(f'unsupported key signature: {name}')
|
||||||
|
|
||||||
|
|
||||||
def signed_interval(source: KeyProfile, target: KeyProfile) -> int:
|
def signed_interval(source: KeyProfile, target: KeyProfile) -> int:
|
||||||
value = (target.root-source.root) % 12
|
value = (target.root - source.root) % 12
|
||||||
return value-12 if value > 6 else value
|
return value - 12 if value > 6 else value
|
||||||
|
|
||||||
|
|
||||||
def interval_between(source: dict, target: dict) -> tuple[int, int]:
|
def interval_between(source: dict, target: dict) -> tuple[int, int]:
|
||||||
semitones = (NATURAL[target['letter']] + target['alter'] - NATURAL[source['letter']] - source['alter']) % 12
|
semitones = (NATURAL[target['letter']] + target['alter'] - NATURAL[source['letter']] - source['alter']) % 12
|
||||||
@@ -45,7 +39,6 @@ def interval_between(source: dict, target: dict) -> tuple[int, int]:
|
|||||||
steps -= 7
|
steps -= 7
|
||||||
return steps, semitones
|
return steps, semitones
|
||||||
|
|
||||||
|
|
||||||
def shift_pitch(pitch: dict, steps: int, semitones: int) -> dict:
|
def shift_pitch(pitch: dict, steps: int, semitones: int) -> dict:
|
||||||
output = copy.deepcopy(pitch)
|
output = copy.deepcopy(pitch)
|
||||||
original_octave = pitch.get('octave', 4)
|
original_octave = pitch.get('octave', 4)
|
||||||
@@ -53,32 +46,30 @@ def shift_pitch(pitch: dict, steps: int, semitones: int) -> dict:
|
|||||||
octave, index = divmod(diatonic, 7)
|
octave, index = divmod(diatonic, 7)
|
||||||
output['letter'] = LETTERS[index]
|
output['letter'] = LETTERS[index]
|
||||||
output['octave'] = octave
|
output['octave'] = octave
|
||||||
output['alter'] = pitch_number({**pitch, 'octave':original_octave}) + semitones - (12 * (octave+1) + NATURAL[output['letter']])
|
output['alter'] = pitch_number({**pitch, 'octave': original_octave}) + semitones - (12 * (octave + 1) + NATURAL[output['letter']])
|
||||||
if 'octave' not in pitch:
|
if 'octave' not in pitch:
|
||||||
output.pop('octave')
|
output.pop('octave')
|
||||||
if 'accidental_display' in output:
|
if 'accidental_display' in output:
|
||||||
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')
|
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
|
return output
|
||||||
|
|
||||||
|
|
||||||
def shift_key(key: dict, steps: int, semitones: int) -> dict:
|
def shift_key(key: dict, steps: int, semitones: int) -> dict:
|
||||||
tonic = shift_pitch(key['tonic'], steps, semitones) if key.get('tonic') else None
|
tonic = shift_pitch(key['tonic'], steps, semitones) if key.get('tonic') else None
|
||||||
alterations = [0] * 7
|
alterations = [0] * 7
|
||||||
for index, letter in enumerate(LETTERS):
|
for index, letter in enumerate(LETTERS):
|
||||||
p = shift_pitch({'letter':letter, 'alter':key['alterations'][index]}, steps, semitones)
|
p = shift_pitch({'letter': letter, 'alter': key['alterations'][index]}, steps, semitones)
|
||||||
alterations[LETTERS.index(p['letter'])] = p['alter']
|
alterations[LETTERS.index(p['letter'])] = p['alter']
|
||||||
result = {**copy.deepcopy(key), 'tonic':tonic, 'alterations':alterations}
|
result = {**copy.deepcopy(key), 'tonic': tonic, 'alterations': alterations}
|
||||||
if not key.get('custom') and tonic:
|
if not key.get('custom') and tonic:
|
||||||
for fifths in range(-14,15):
|
for fifths in range(-14, 15):
|
||||||
candidate = key_from_fifths(fifths, key['mode'])
|
candidate = key_from_fifths(fifths, key['mode'])
|
||||||
if candidate['tonic']==tonic and candidate['alterations']==alterations:
|
if candidate['tonic'] == tonic and candidate['alterations'] == alterations:
|
||||||
result['fifths'] = fifths
|
result['fifths'] = fifths
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
result['custom'] = True
|
result['custom'] = True
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
def transpose_document(document: dict, target_key_name: str) -> dict:
|
def transpose_document(document: dict, target_key_name: str) -> dict:
|
||||||
output = copy.deepcopy(document)
|
output = copy.deepcopy(document)
|
||||||
metadata = output['metadata']
|
metadata = output['metadata']
|
||||||
@@ -99,7 +90,7 @@ def transpose_document(document: dict, target_key_name: str) -> dict:
|
|||||||
if 'key' in change:
|
if 'key' in change:
|
||||||
change['key'] = shift_key(change['key'], steps, semitones)
|
change['key'] = shift_key(change['key'], steps, semitones)
|
||||||
for h in measure.get('harmonies', []):
|
for h in measure.get('harmonies', []):
|
||||||
for field in ('root','bass'):
|
for field in ('root', 'bass'):
|
||||||
if field in h:
|
if field in h:
|
||||||
h[field] = shift_pitch(h[field], steps, semitones)
|
h[field] = shift_pitch(h[field], steps, semitones)
|
||||||
h['symbol'] = harmony.symbol(h)
|
h['symbol'] = harmony.symbol(h)
|
||||||
@@ -108,11 +99,10 @@ def transpose_document(document: dict, target_key_name: str) -> dict:
|
|||||||
note['pitches'] = [shift_pitch(p, steps, semitones) for p in note.get('pitches', [])]
|
note['pitches'] = [shift_pitch(p, steps, semitones) for p in note.get('pitches', [])]
|
||||||
return output
|
return output
|
||||||
|
|
||||||
|
|
||||||
def concert_document(document: dict) -> dict:
|
def concert_document(document: dict) -> dict:
|
||||||
"""Normalize MusicXML instrument transposition before concert-pitch arranging."""
|
"""Normalize MusicXML instrument transposition before concert-pitch arranging."""
|
||||||
output = copy.deepcopy(document)
|
output = copy.deepcopy(document)
|
||||||
zero = {'diatonic':0, 'chromatic':0, 'octave_change':0}
|
zero = {'diatonic': 0, 'chromatic': 0, 'octave_change': 0}
|
||||||
for staff in output['staves']:
|
for staff in output['staves']:
|
||||||
for measure in staff['measures']:
|
for measure in staff['measures']:
|
||||||
initial = measure['transpose']
|
initial = measure['transpose']
|
||||||
@@ -124,21 +114,21 @@ def concert_document(document: dict) -> dict:
|
|||||||
for change in changes:
|
for change in changes:
|
||||||
if fraction(change['onset']) <= onset and 'transpose' in change:
|
if fraction(change['onset']) <= onset and 'transpose' in change:
|
||||||
state = change['transpose']
|
state = change['transpose']
|
||||||
return state['diatonic']+7*state['octave_change'], state['chromatic']+12*state['octave_change']
|
return state['diatonic'] + 7 * state['octave_change'], state['chromatic'] + 12 * state['octave_change']
|
||||||
|
|
||||||
from generator.model import fraction
|
from generator.model import fraction
|
||||||
measure['key'] = shift_key(measure['key'], *interval(0))
|
measure['key'] = shift_key(measure['key'], *interval(0))
|
||||||
measure['key_signature'] = key_label(measure['key'])
|
measure['key_signature'] = key_label(measure['key'])
|
||||||
for n in measure['voices'][0]['notes']:
|
for n in measure['voices'][0]['notes']:
|
||||||
n['pitches'] = [shift_pitch(p,*interval(fraction(n['onset']))) for p in n['pitches']]
|
n['pitches'] = [shift_pitch(p, *interval(fraction(n['onset']))) for p in n['pitches']]
|
||||||
for h in measure['harmonies']:
|
for h in measure['harmonies']:
|
||||||
for field in ('root','bass'):
|
for field in ('root', 'bass'):
|
||||||
if field in h:
|
if field in h:
|
||||||
h[field] = shift_pitch(h[field],*interval(fraction(h['onset'])))
|
h[field] = shift_pitch(h[field], *interval(fraction(h['onset'])))
|
||||||
h['symbol'] = harmony.symbol(h)
|
h['symbol'] = harmony.symbol(h)
|
||||||
for change in measure.get('attributes',[]):
|
for change in measure.get('attributes', []):
|
||||||
if 'key' in change:
|
if 'key' in change:
|
||||||
change['key'] = shift_key(change['key'],*interval(fraction(change['onset'])))
|
change['key'] = shift_key(change['key'], *interval(fraction(change['onset'])))
|
||||||
if 'transpose' in change:
|
if 'transpose' in change:
|
||||||
change['transpose'] = copy.deepcopy(zero)
|
change['transpose'] = copy.deepcopy(zero)
|
||||||
measure['transpose'] = copy.deepcopy(zero)
|
measure['transpose'] = copy.deepcopy(zero)
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
#include <modulus/types.hpp>
|
#include <modulus/types.hpp>
|
||||||
#include <vamp_style.hpp>
|
#include <vamp_style.hpp>
|
||||||
|
|
||||||
|
|
||||||
namespace vamp_pico {
|
namespace vamp_pico {
|
||||||
|
|
||||||
constexpr auto display_width = std::int32_t{320};
|
constexpr auto display_width = std::int32_t{320};
|
||||||
@@ -20,9 +21,9 @@ public:
|
|||||||
auto init() -> void;
|
auto init() -> void;
|
||||||
auto fill(std::uint16_t color) -> void;
|
auto fill(std::uint16_t color) -> void;
|
||||||
auto fill_rect(std::int32_t x, std::int32_t y, std::int32_t width, std::int32_t height, std::uint16_t color) -> void;
|
auto fill_rect(std::int32_t x, std::int32_t y, std::int32_t width, std::int32_t height, std::uint16_t color) -> void;
|
||||||
auto draw_text(std::int32_t x, std::int32_t y, std::char_t const* text, std::uint16_t color, std::int32_t scale = 2) -> void;
|
auto draw_text(std::int32_t x, std::int32_t y, std::char_t const *text, std::uint16_t color, std::int32_t scale = 2) -> void;
|
||||||
auto show_boot(std::char_t const* line1, std::char_t const* line2, std::uint16_t accent) -> void;
|
auto show_boot(std::char_t const *line1, std::char_t const *line2, std::uint16_t accent) -> void;
|
||||||
auto show_remote(vamp_style::color status, std::char_t const* title, std::char_t const* part, std::uint32_t page, std::uint32_t pages, std::bool_t active) -> void;
|
auto show_remote(vamp_style::color status, std::char_t const *title, std::char_t const *part, std::uint32_t page, std::uint32_t pages, std::bool_t active) -> void;
|
||||||
private:
|
private:
|
||||||
std::bool_t remote_valid_{};
|
std::bool_t remote_valid_{};
|
||||||
vamp_style::color last_status_{};
|
vamp_style::color last_status_{};
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
#include <vamp_protocol/page_control.hpp>
|
#include <vamp_protocol/page_control.hpp>
|
||||||
#include <modulus/types.hpp>
|
#include <modulus/types.hpp>
|
||||||
|
|
||||||
|
|
||||||
namespace vamp_pico {
|
namespace vamp_pico {
|
||||||
|
|
||||||
struct remote_display_state {
|
struct remote_display_state {
|
||||||
@@ -17,9 +18,9 @@ struct remote_display_state {
|
|||||||
std::uint32_t pages{};
|
std::uint32_t pages{};
|
||||||
};
|
};
|
||||||
|
|
||||||
auto parse_remote_display_body(std::string_view body, vamp_pico::remote_display_state& state) -> std::bool_t;
|
auto parse_remote_display_body(std::string_view body, vamp_pico::remote_display_state &state) -> std::bool_t;
|
||||||
auto post_remote_command(vamp_protocol::page_command command, vamp_pico::remote_display_state& state) -> std::bool_t;
|
auto post_remote_command(vamp_protocol::page_command command, vamp_pico::remote_display_state &state) -> std::bool_t;
|
||||||
auto heartbeat() -> std::bool_t;
|
auto heartbeat() -> std::bool_t;
|
||||||
auto fetch_remote_display(vamp_pico::remote_display_state& state) -> std::bool_t;
|
auto fetch_remote_display(vamp_pico::remote_display_state &state) -> std::bool_t;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
#include <modulus/types.hpp>
|
#include <modulus/types.hpp>
|
||||||
|
|
||||||
|
|
||||||
namespace vamp_pico {
|
namespace vamp_pico {
|
||||||
|
|
||||||
struct touch_point {
|
struct touch_point {
|
||||||
@@ -13,7 +14,7 @@ struct touch_point {
|
|||||||
|
|
||||||
struct touch_controller {
|
struct touch_controller {
|
||||||
auto init() -> void;
|
auto init() -> void;
|
||||||
auto read(touch_point& point) -> std::bool_t;
|
auto read(touch_point &point) -> std::bool_t;
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
#include <modulus/types.hpp>
|
#include <modulus/types.hpp>
|
||||||
|
|
||||||
|
|
||||||
namespace vamp_protocol {
|
namespace vamp_protocol {
|
||||||
|
|
||||||
constexpr std::char_t root_path[] = "/";
|
constexpr std::char_t root_path[] = "/";
|
||||||
@@ -20,9 +21,9 @@ enum class page_command {
|
|||||||
SELECT,
|
SELECT,
|
||||||
};
|
};
|
||||||
|
|
||||||
auto page_command_path(vamp_protocol::page_command command) -> std::char_t const*;
|
auto page_command_path(vamp_protocol::page_command command) -> std::char_t const *;
|
||||||
auto page_command_legacy_path(vamp_protocol::page_command command) -> std::char_t const*;
|
auto page_command_legacy_path(vamp_protocol::page_command command) -> std::char_t const *;
|
||||||
auto page_command_name(vamp_protocol::page_command command) -> std::char_t const*;
|
auto page_command_name(vamp_protocol::page_command command) -> std::char_t const *;
|
||||||
auto page_command_display_label(vamp_protocol::page_command command) -> std::char_t const*;
|
auto page_command_display_label(vamp_protocol::page_command command) -> std::char_t const *;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
#include <modulus/types.hpp>
|
#include <modulus/types.hpp>
|
||||||
|
|
||||||
|
|
||||||
namespace vamp_style {
|
namespace vamp_style {
|
||||||
|
|
||||||
struct color {
|
struct color {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
#include <modulus/types.hpp>
|
#include <modulus/types.hpp>
|
||||||
|
|
||||||
|
|
||||||
namespace vamp_style {
|
namespace vamp_style {
|
||||||
|
|
||||||
constexpr auto ui_font_path = std::string_view{"/usr/share/product-vamp/fonts/petaluma/PetalumaScript.otf"};
|
constexpr auto ui_font_path = std::string_view{"/usr/share/product-vamp/fonts/petaluma/PetalumaScript.otf"};
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
#include <modulus/types.hpp>
|
#include <modulus/types.hpp>
|
||||||
|
|
||||||
|
|
||||||
namespace vamp_zero {
|
namespace vamp_zero {
|
||||||
|
|
||||||
struct command_line {
|
struct command_line {
|
||||||
@@ -19,7 +20,7 @@ struct command_line {
|
|||||||
std::bool_t ensure_database{};
|
std::bool_t ensure_database{};
|
||||||
};
|
};
|
||||||
|
|
||||||
auto parse_command_line(std::int32_t argc, std::char_t** argv) -> std::expected<vamp_zero::command_line, modulus::error>;
|
auto parse_command_line(std::int32_t argc, std::char_t **argv) -> std::expected<vamp_zero::command_line, modulus::error>;
|
||||||
auto run(vamp_zero::command_line command) -> std::expected<void, modulus::error>;
|
auto run(vamp_zero::command_line command) -> std::expected<void, modulus::error>;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,20 +11,18 @@
|
|||||||
#include <vamp_zero/config.hpp>
|
#include <vamp_zero/config.hpp>
|
||||||
#include <vamp_zero/types.hpp>
|
#include <vamp_zero/types.hpp>
|
||||||
|
|
||||||
|
|
||||||
namespace vamp_zero {
|
namespace vamp_zero {
|
||||||
|
|
||||||
class audio_controller {
|
class audio_controller {
|
||||||
public:
|
public:
|
||||||
audio_controller(vamp_zero::audio_config config, sonus::engine engine);
|
audio_controller(vamp_zero::audio_config config, sonus::engine engine);
|
||||||
audio_controller(vamp_zero::audio_controller const&) = delete;
|
audio_controller(vamp_zero::audio_controller const &) = delete;
|
||||||
auto operator=(vamp_zero::audio_controller const&) -> vamp_zero::audio_controller& = delete;
|
auto operator=(vamp_zero::audio_controller const &) -> vamp_zero::audio_controller & = delete;
|
||||||
~audio_controller();
|
~audio_controller();
|
||||||
|
auto sync(vamp_zero::app_state const &state) -> void;
|
||||||
auto sync(vamp_zero::app_state const& state) -> void;
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
auto make_metronome_config(vamp_zero::app_state const& state) const -> sonus::metronome_config;
|
auto make_metronome_config(vamp_zero::app_state const &state) const -> sonus::metronome_config;
|
||||||
|
|
||||||
vamp_zero::audio_config config_{};
|
vamp_zero::audio_config config_{};
|
||||||
sonus::engine engine_;
|
sonus::engine engine_;
|
||||||
mutable std::mutex mutex_{};
|
mutable std::mutex mutex_{};
|
||||||
@@ -32,6 +30,6 @@ private:
|
|||||||
std::uint32_t last_metronome_bpm_{};
|
std::uint32_t last_metronome_bpm_{};
|
||||||
};
|
};
|
||||||
|
|
||||||
auto create_audio_controller(vamp_zero::audio_config const& config) -> std::expected<std::unique_ptr<vamp_zero::audio_controller>, modulus::error>;
|
auto create_audio_controller(vamp_zero::audio_config const &config) -> std::expected<std::unique_ptr<vamp_zero::audio_controller>, modulus::error>;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
#include <vamp_style.hpp>
|
#include <vamp_style.hpp>
|
||||||
|
|
||||||
|
|
||||||
namespace vamp_zero {
|
namespace vamp_zero {
|
||||||
|
|
||||||
auto to_luma(vamp_style::color value) -> luma::color;
|
auto to_luma(vamp_style::color value) -> luma::color;
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
#include <modulus/types.hpp>
|
#include <modulus/types.hpp>
|
||||||
#include <vamp_zero/types.hpp>
|
#include <vamp_zero/types.hpp>
|
||||||
|
|
||||||
|
|
||||||
namespace vamp_zero {
|
namespace vamp_zero {
|
||||||
|
|
||||||
struct runtime_config {
|
struct runtime_config {
|
||||||
@@ -67,7 +68,7 @@ struct config {
|
|||||||
luma::application_config display{.title = "Product Vamp"};
|
luma::application_config display{.title = "Product Vamp"};
|
||||||
};
|
};
|
||||||
|
|
||||||
auto parse_config(boost::json::value const& value) -> std::expected<vamp_zero::config, modulus::error>;
|
auto parse_config(boost::json::value const &value) -> std::expected<vamp_zero::config, modulus::error>;
|
||||||
auto load_config(std::string const& path) -> std::expected<vamp_zero::config, modulus::error>;
|
auto load_config(std::string const &path) -> std::expected<vamp_zero::config, modulus::error>;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
#include <vamp_zero/config.hpp>
|
#include <vamp_zero/config.hpp>
|
||||||
#include <vamp_zero/types.hpp>
|
#include <vamp_zero/types.hpp>
|
||||||
|
|
||||||
|
|
||||||
namespace vamp_zero {
|
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>>;
|
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>
|
#include <modulus.hpp>
|
||||||
|
|
||||||
|
|
||||||
namespace vamp_zero {
|
namespace vamp_zero {
|
||||||
|
|
||||||
enum class error_code : std::int32_t {
|
enum class error_code : std::int32_t {
|
||||||
|
|||||||
@@ -12,8 +12,8 @@
|
|||||||
namespace vamp_zero {
|
namespace vamp_zero {
|
||||||
|
|
||||||
auto parse_score_document(std::string_view json, std::string source_path = {}) -> std::expected<vamp_zero::score_document, modulus::error>;
|
auto parse_score_document(std::string_view json, std::string source_path = {}) -> std::expected<vamp_zero::score_document, modulus::error>;
|
||||||
auto load_performance_library(std::string const& assets_path) -> std::expected<vamp_zero::library_state, modulus::error>;
|
auto load_performance_library(std::string const &assets_path) -> std::expected<vamp_zero::library_state, modulus::error>;
|
||||||
auto load_practice_library(std::string const& assets_path) -> std::expected<vamp_zero::practice_library, modulus::error>;
|
auto load_practice_library(std::string const &assets_path) -> std::expected<vamp_zero::practice_library, modulus::error>;
|
||||||
auto group_library_parts(std::vector<vamp_zero::library_part> parts) -> vamp_zero::library_state;
|
auto group_library_parts(std::vector<vamp_zero::library_part> parts) -> vamp_zero::library_state;
|
||||||
auto group_practice_exercises(std::vector<vamp_zero::practice_exercise> exercises) -> vamp_zero::practice_library;
|
auto group_practice_exercises(std::vector<vamp_zero::practice_exercise> exercises) -> vamp_zero::practice_library;
|
||||||
|
|
||||||
|
|||||||
@@ -9,21 +9,22 @@
|
|||||||
|
|
||||||
#include <vamp_zero/types.hpp>
|
#include <vamp_zero/types.hpp>
|
||||||
|
|
||||||
|
|
||||||
namespace vamp_zero {
|
namespace vamp_zero {
|
||||||
|
|
||||||
auto selected_group(vamp_zero::app_state const& state) -> vamp_zero::song_group const*;
|
auto selected_group(vamp_zero::app_state const &state) -> vamp_zero::song_group const *;
|
||||||
auto selected_part(vamp_zero::app_state const& state) -> vamp_zero::library_part const*;
|
auto selected_part(vamp_zero::app_state const &state) -> vamp_zero::library_part const *;
|
||||||
auto active_part(vamp_zero::app_state const& state) -> vamp_zero::library_part const*;
|
auto active_part(vamp_zero::app_state const &state) -> vamp_zero::library_part const *;
|
||||||
auto selected_practice_type(vamp_zero::app_state const& state) -> vamp_zero::practice_type_group const*;
|
auto selected_practice_type(vamp_zero::app_state const &state) -> vamp_zero::practice_type_group const *;
|
||||||
auto selected_practice_lesson(vamp_zero::app_state const& state) -> vamp_zero::practice_lesson_group const*;
|
auto selected_practice_lesson(vamp_zero::app_state const &state) -> vamp_zero::practice_lesson_group const *;
|
||||||
auto selected_practice_exercise(vamp_zero::app_state const& state) -> vamp_zero::practice_exercise const*;
|
auto selected_practice_exercise(vamp_zero::app_state const &state) -> vamp_zero::practice_exercise const *;
|
||||||
auto active_practice(vamp_zero::app_state const& state) -> vamp_zero::practice_exercise const*;
|
auto active_practice(vamp_zero::app_state const &state) -> vamp_zero::practice_exercise const *;
|
||||||
auto supports_key_selection(vamp_zero::practice_exercise const& exercise) -> std::bool_t;
|
auto supports_key_selection(vamp_zero::practice_exercise const &exercise) -> std::bool_t;
|
||||||
auto selected_practice_variant(vamp_zero::app_state const& state, vamp_zero::practice_exercise const& exercise) -> vamp_zero::practice_exercise::score_variant const*;
|
auto selected_practice_variant(vamp_zero::app_state const &state, vamp_zero::practice_exercise const &exercise) -> vamp_zero::practice_exercise::score_variant const *;
|
||||||
auto selected_practice_key(vamp_zero::app_state const& state, vamp_zero::practice_exercise const& exercise) -> std::optional<std::string_view>;
|
auto selected_practice_key(vamp_zero::app_state const &state, vamp_zero::practice_exercise const &exercise) -> std::optional<std::string_view>;
|
||||||
auto practice_key_row_count(vamp_zero::app_state const& state) -> std::size_t;
|
auto practice_key_row_count(vamp_zero::app_state const &state) -> std::size_t;
|
||||||
auto effective_practice_key(vamp_zero::app_state const& state, vamp_zero::practice_exercise const& exercise) -> std::string;
|
auto effective_practice_key(vamp_zero::app_state const &state, vamp_zero::practice_exercise const &exercise) -> std::string;
|
||||||
auto active_pages(vamp_zero::app_state const& state) -> std::size_t;
|
auto active_pages(vamp_zero::app_state const &state) -> std::size_t;
|
||||||
auto practice_fallback_menu(vamp_zero::app_state const& state) -> vamp_zero::app_mode;
|
auto practice_fallback_menu(vamp_zero::app_state const &state) -> vamp_zero::app_mode;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,10 +3,15 @@
|
|||||||
#include <expected>
|
#include <expected>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <string_view>
|
#include <string_view>
|
||||||
|
|
||||||
#include <modulus.hpp>
|
#include <modulus.hpp>
|
||||||
|
|
||||||
#include <vamp_zero/types.hpp>
|
#include <vamp_zero/types.hpp>
|
||||||
|
|
||||||
|
|
||||||
namespace vamp_zero {
|
namespace vamp_zero {
|
||||||
|
|
||||||
auto parse_notation_document(std::string_view json, std::string source_path) -> std::expected<score_document, modulus::error>;
|
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,11 +10,12 @@
|
|||||||
#include <vamp_protocol/page_control.hpp>
|
#include <vamp_protocol/page_control.hpp>
|
||||||
#include <vamp_zero/types.hpp>
|
#include <vamp_zero/types.hpp>
|
||||||
|
|
||||||
|
|
||||||
namespace vamp_zero {
|
namespace vamp_zero {
|
||||||
|
|
||||||
auto apply_app_command(vamp_zero::app_state state, vamp_protocol::page_command command) -> vamp_zero::app_state;
|
auto apply_app_command(vamp_zero::app_state state, vamp_protocol::page_command command) -> vamp_zero::app_state;
|
||||||
auto app_status_json(vamp_zero::app_state const& state) -> boost::json::value;
|
auto app_status_json(vamp_zero::app_state const &state) -> boost::json::value;
|
||||||
auto remote_display_text(vamp_zero::app_state const& state) -> std::string;
|
auto remote_display_text(vamp_zero::app_state const &state) -> std::string;
|
||||||
auto validate_app_state(vamp_zero::app_state const& state) -> std::expected<void, modulus::error>;
|
auto validate_app_state(vamp_zero::app_state const &state) -> std::expected<void, modulus::error>;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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/config.hpp>
|
||||||
#include <vamp_zero/types.hpp>
|
#include <vamp_zero/types.hpp>
|
||||||
|
|
||||||
|
|
||||||
namespace vamp_zero {
|
namespace vamp_zero {
|
||||||
|
|
||||||
class control_state {
|
class control_state {
|
||||||
public:
|
public:
|
||||||
using observer = std::function<void(vamp_zero::app_state)>;
|
|
||||||
|
|
||||||
control_state(vamp_zero::config config, vamp_zero::app_state state);
|
control_state(vamp_zero::config config, vamp_zero::app_state state);
|
||||||
auto config() const -> vamp_zero::config;
|
auto config() const -> vamp_zero::config;
|
||||||
auto snapshot() const -> vamp_zero::app_state;
|
auto snapshot() const -> vamp_zero::app_state;
|
||||||
auto apply(vamp_protocol::page_command command) -> vamp_zero::app_state;
|
auto apply(vamp_protocol::page_command command) -> vamp_zero::app_state;
|
||||||
auto heartbeat() -> vamp_zero::app_state;
|
auto heartbeat() -> vamp_zero::app_state;
|
||||||
auto refresh_connection() -> std::optional<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:
|
private:
|
||||||
auto pico_connected_locked() const -> std::bool_t;
|
auto pico_connected_locked() const -> std::bool_t;
|
||||||
auto snapshot_locked() const -> vamp_zero::app_state;
|
auto snapshot_locked() const -> vamp_zero::app_state;
|
||||||
@@ -33,10 +32,10 @@ private:
|
|||||||
vamp_zero::config config_{};
|
vamp_zero::config config_{};
|
||||||
vamp_zero::app_state state_{};
|
vamp_zero::app_state state_{};
|
||||||
std::chrono::steady_clock::time_point last_heartbeat_{};
|
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;
|
auto make_router(vamp_zero::control_state &state) -> ligo::service_router;
|
||||||
auto portal_status_json(vamp_zero::config const& config, vamp_zero::app_state const& state) -> boost::json::value;
|
auto portal_status_json(vamp_zero::config const &config, vamp_zero::app_state const &state) -> boost::json::value;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
|
|
||||||
#include <vamp_zero/types.hpp>
|
#include <vamp_zero/types.hpp>
|
||||||
|
|
||||||
|
|
||||||
namespace vamp_zero {
|
namespace vamp_zero {
|
||||||
|
|
||||||
enum class score_accidental_mark {
|
enum class score_accidental_mark {
|
||||||
@@ -100,11 +101,11 @@ struct score_system_range {
|
|||||||
constexpr auto score_measures_per_system = std::size_t{4};
|
constexpr auto score_measures_per_system = std::size_t{4};
|
||||||
constexpr auto score_systems_per_page = std::size_t{3};
|
constexpr auto score_systems_per_page = std::size_t{3};
|
||||||
|
|
||||||
auto notated_accidentals_for_measure(vamp_zero::score_measure const& measure, std::string_view inherited_key_signature) -> std::vector<vamp_zero::score_accidental_mark>;
|
auto notated_accidentals_for_measure(vamp_zero::score_measure const &measure, std::string_view inherited_key_signature) -> std::vector<vamp_zero::score_accidental_mark>;
|
||||||
auto score_system_ranges(vamp_zero::score_document const& score) -> std::vector<vamp_zero::score_system_range>;
|
auto score_system_ranges(vamp_zero::score_document const &score) -> std::vector<vamp_zero::score_system_range>;
|
||||||
auto score_page_count(vamp_zero::score_document const& score) -> std::size_t;
|
auto score_page_count(vamp_zero::score_document const &score) -> std::size_t;
|
||||||
auto layout_score_page(luma::rect bounds, vamp_zero::score_document const& score, std::size_t page_index) -> vamp_zero::score_page_layout;
|
auto layout_score_page(luma::rect bounds, vamp_zero::score_document const &score, std::size_t page_index) -> vamp_zero::score_page_layout;
|
||||||
auto draw_score_layout(luma::draw_context& context, vamp_zero::score_page_layout const& layout) -> void;
|
auto draw_score_layout(luma::draw_context &context, vamp_zero::score_page_layout const &layout) -> void;
|
||||||
auto draw_score_page(luma::draw_context& context, luma::rect bounds, vamp_zero::score_document const& score, std::size_t page_index) -> void;
|
auto draw_score_page(luma::draw_context &context, luma::rect bounds, vamp_zero::score_document const &score, std::size_t page_index) -> void;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,12 +10,15 @@
|
|||||||
|
|
||||||
#include <modulus/types.hpp>
|
#include <modulus/types.hpp>
|
||||||
|
|
||||||
|
|
||||||
namespace vamp_zero {
|
namespace vamp_zero {
|
||||||
|
|
||||||
struct score_time {
|
struct score_time {
|
||||||
std::int64_t numerator{};
|
std::int64_t numerator{};
|
||||||
std::int64_t denominator{1};
|
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 {
|
struct score_written_duration {
|
||||||
@@ -181,7 +184,6 @@ struct practice_exercise {
|
|||||||
std::size_t measure_count{};
|
std::size_t measure_count{};
|
||||||
std::size_t total_pages{};
|
std::size_t total_pages{};
|
||||||
};
|
};
|
||||||
|
|
||||||
std::string id{};
|
std::string id{};
|
||||||
std::string practice_type_id{};
|
std::string practice_type_id{};
|
||||||
std::string practice_type_title{};
|
std::string practice_type_title{};
|
||||||
@@ -261,7 +263,7 @@ struct app_state {
|
|||||||
|
|
||||||
auto to_string(vamp_zero::hardware_mode value) -> std::string;
|
auto to_string(vamp_zero::hardware_mode value) -> std::string;
|
||||||
auto to_string(vamp_zero::app_mode value) -> std::string;
|
auto to_string(vamp_zero::app_mode value) -> std::string;
|
||||||
auto practice_key_names() -> std::vector<std::string_view> const&;
|
auto practice_key_names() -> std::vector<std::string_view> const &;
|
||||||
auto practice_type_supports_key_selection(std::string_view practice_type_id) -> std::bool_t;
|
auto practice_type_supports_key_selection(std::string_view practice_type_id) -> std::bool_t;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,23 +32,21 @@ struct ui_state {
|
|||||||
|
|
||||||
class zero_ui {
|
class zero_ui {
|
||||||
public:
|
public:
|
||||||
using command_handler = std::function<void(vamp_protocol::page_command)>;
|
zero_ui(boost::asio::any_io_executor executor, luma::application_config config, std::function<void(vamp_protocol::page_command)> command_handler = {});
|
||||||
|
|
||||||
zero_ui(boost::asio::any_io_executor executor, luma::application_config config, command_handler command_handler = {});
|
|
||||||
auto run() -> std::expected<void, modulus::error>;
|
auto run() -> std::expected<void, modulus::error>;
|
||||||
auto stop() -> void;
|
auto stop() -> void;
|
||||||
auto publish(vamp_zero::ui_state state) -> std::expected<void, modulus::error>;
|
auto publish(vamp_zero::ui_state state) -> std::expected<void, modulus::error>;
|
||||||
auto snapshot() const -> vamp_zero::ui_state;
|
auto snapshot() const -> vamp_zero::ui_state;
|
||||||
private:
|
private:
|
||||||
auto handle_touch(luma::touch_event const& event) -> std::bool_t;
|
auto handle_touch(luma::touch_event const &event) -> std::bool_t;
|
||||||
auto send_touch_command(vamp_protocol::page_command command) -> void;
|
auto send_touch_command(vamp_protocol::page_command command) -> void;
|
||||||
auto arm_pending_tap() -> void;
|
auto arm_pending_tap() -> void;
|
||||||
auto cancel_pending_tap_locked() -> void;
|
auto cancel_pending_tap_locked() -> void;
|
||||||
auto make_root(vamp_zero::ui_state const& state) -> std::expected<std::unique_ptr<luma::control>, modulus::error>;
|
auto make_root(vamp_zero::ui_state const &state) -> std::expected<std::unique_ptr<luma::control>, modulus::error>;
|
||||||
boost::asio::any_io_executor executor_;
|
boost::asio::any_io_executor executor_;
|
||||||
boost::asio::steady_timer tap_timer_;
|
boost::asio::steady_timer tap_timer_;
|
||||||
luma::application application_;
|
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_{};
|
std::shared_ptr<vamp_zero::score_draw_cache> score_cache_{};
|
||||||
mutable std::mutex mutex_{};
|
mutable std::mutex mutex_{};
|
||||||
vamp_zero::ui_state state_{};
|
vamp_zero::ui_state state_{};
|
||||||
@@ -60,6 +58,6 @@ private:
|
|||||||
std::bool_t pending_tap_{};
|
std::bool_t pending_tap_{};
|
||||||
};
|
};
|
||||||
|
|
||||||
auto make_ui_state(vamp_zero::config const& config, vamp_zero::app_state app) -> vamp_zero::ui_state;
|
auto make_ui_state(vamp_zero::config const &config, vamp_zero::app_state app) -> vamp_zero::ui_state;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-10
@@ -20,6 +20,7 @@ COMMANDS = (
|
|||||||
"package",
|
"package",
|
||||||
"lsp",
|
"lsp",
|
||||||
"clangd",
|
"clangd",
|
||||||
|
"format",
|
||||||
"run",
|
"run",
|
||||||
)
|
)
|
||||||
COMMON_DIR = pathlib.Path("build") / "build_common"
|
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 / TOOLS_DIR / "run.py").is_file()
|
||||||
and (root / RUNTIME_DIR / "modulus" / "cli.py").is_file()
|
and (root / RUNTIME_DIR / "modulus" / "cli.py").is_file()
|
||||||
and (root / RUNTIME_DIR / "modulus" / "export.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():
|
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):
|
def run_with_retries(command, attempts=3, input_text=None, display_command=None, env=None):
|
||||||
completed = None
|
completed = None
|
||||||
for attempt in range(1, attempts + 1):
|
for attempt in range(1, attempts + 1):
|
||||||
completed = run(
|
completed = run(command, input_text=input_text, display_command=display_command, env=env, check=False)
|
||||||
command,
|
|
||||||
input_text=input_text,
|
|
||||||
display_command=display_command,
|
|
||||||
env=env,
|
|
||||||
check=False,
|
|
||||||
)
|
|
||||||
if completed.returncode == 0:
|
if completed.returncode == 0:
|
||||||
return completed
|
return completed
|
||||||
if attempt != attempts:
|
if attempt != attempts:
|
||||||
@@ -204,9 +202,11 @@ RUN python3 -m venv /opt/modulus-python
|
|||||||
ENV PATH=/opt/modulus-python/bin:$PATH
|
ENV PATH=/opt/modulus-python/bin:$PATH
|
||||||
|
|
||||||
COPY tools/requirements.txt /tmp/vamp-build-requirements.txt
|
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 \\
|
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()}"
|
LABEL org.khodo.product-vamp.build-requirements="{build_requirements_digest()}"
|
||||||
|
|
||||||
@@ -487,9 +487,9 @@ def run_package(argv):
|
|||||||
|
|
||||||
def delegate(command, argv):
|
def delegate(command, argv):
|
||||||
root = repo_root()
|
root = repo_root()
|
||||||
if command == "clangd":
|
if command in {"clangd", "format"}:
|
||||||
if not runtime_ready(root):
|
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
|
return 1
|
||||||
else:
|
else:
|
||||||
ensure_runtime(root)
|
ensure_runtime(root)
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ DEFAULT_REGISTRY_URL = "https://git.thyme-labs.com/api/packages/khodo/generic"
|
|||||||
DEFAULT_VERSION = "latest"
|
DEFAULT_VERSION = "latest"
|
||||||
PAGE_LIMIT = 50
|
PAGE_LIMIT = 50
|
||||||
|
|
||||||
|
|
||||||
def read_config(path):
|
def read_config(path):
|
||||||
try:
|
try:
|
||||||
data = json.loads(path.read_text(encoding="utf-8"))
|
data = json.loads(path.read_text(encoding="utf-8"))
|
||||||
@@ -33,7 +32,6 @@ def read_config(path):
|
|||||||
registry_url = DEFAULT_REGISTRY_URL
|
registry_url = DEFAULT_REGISTRY_URL
|
||||||
return name, registry_url
|
return name, registry_url
|
||||||
|
|
||||||
|
|
||||||
def parse_registry_url(registry_url):
|
def parse_registry_url(registry_url):
|
||||||
parsed = urllib.parse.urlparse(registry_url.rstrip("/"))
|
parsed = urllib.parse.urlparse(registry_url.rstrip("/"))
|
||||||
parts = [urllib.parse.unquote(part) for part in parsed.path.split("/") if part]
|
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 ""
|
prefix = "/" + "/".join(urllib.parse.quote(part, safe="") for part in parts[:-4]) if parts[:-4] else ""
|
||||||
return parsed, prefix, owner
|
return parsed, prefix, owner
|
||||||
|
|
||||||
|
|
||||||
def build_url(parsed, path, query=""):
|
def build_url(parsed, path, query=""):
|
||||||
return urllib.parse.urlunparse((
|
return urllib.parse.urlunparse((
|
||||||
parsed.scheme,
|
parsed.scheme,
|
||||||
@@ -54,11 +51,9 @@ def build_url(parsed, path, query=""):
|
|||||||
"",
|
"",
|
||||||
))
|
))
|
||||||
|
|
||||||
|
|
||||||
def quote_path(*parts):
|
def quote_path(*parts):
|
||||||
return "/".join(urllib.parse.quote(part, safe="") for part in parts)
|
return "/".join(urllib.parse.quote(part, safe="") for part in parts)
|
||||||
|
|
||||||
|
|
||||||
def version_list_url(parsed, prefix, owner, package_name, page):
|
def version_list_url(parsed, prefix, owner, package_name, page):
|
||||||
path = f"{prefix}/api/v1/packages/{quote_path(owner, 'generic', package_name)}"
|
path = f"{prefix}/api/v1/packages/{quote_path(owner, 'generic', package_name)}"
|
||||||
query = urllib.parse.urlencode({
|
query = urllib.parse.urlencode({
|
||||||
@@ -67,12 +62,10 @@ def version_list_url(parsed, prefix, owner, package_name, page):
|
|||||||
})
|
})
|
||||||
return build_url(parsed, path, query)
|
return build_url(parsed, path, query)
|
||||||
|
|
||||||
|
|
||||||
def version_delete_url(parsed, prefix, owner, package_name, version):
|
def version_delete_url(parsed, prefix, owner, package_name, version):
|
||||||
path = f"{prefix}/api/packages/{quote_path(owner, 'generic', package_name, version)}"
|
path = f"{prefix}/api/packages/{quote_path(owner, 'generic', package_name, version)}"
|
||||||
return build_url(parsed, path)
|
return build_url(parsed, path)
|
||||||
|
|
||||||
|
|
||||||
def file_upload_url(registry_url, package_name, version, path):
|
def file_upload_url(registry_url, package_name, version, path):
|
||||||
return "/".join((
|
return "/".join((
|
||||||
registry_url.rstrip("/"),
|
registry_url.rstrip("/"),
|
||||||
@@ -81,12 +74,10 @@ def file_upload_url(registry_url, package_name, version, path):
|
|||||||
urllib.parse.quote(path.name, safe=""),
|
urllib.parse.quote(path.name, safe=""),
|
||||||
))
|
))
|
||||||
|
|
||||||
|
|
||||||
def authorization(user, token):
|
def authorization(user, token):
|
||||||
encoded = base64.b64encode(f"{user}:{token}".encode("utf-8")).decode("ascii")
|
encoded = base64.b64encode(f"{user}:{token}".encode("utf-8")).decode("ascii")
|
||||||
return f"Basic {encoded}"
|
return f"Basic {encoded}"
|
||||||
|
|
||||||
|
|
||||||
def request(method, url, auth, data=None, headers=None, missing_ok=False):
|
def request(method, url, auth, data=None, headers=None, missing_ok=False):
|
||||||
request_headers = {
|
request_headers = {
|
||||||
"Authorization": auth,
|
"Authorization": auth,
|
||||||
@@ -106,7 +97,6 @@ def request(method, url, auth, data=None, headers=None, missing_ok=False):
|
|||||||
except urllib.error.URLError as error:
|
except urllib.error.URLError as error:
|
||||||
raise SystemExit(f"{method} {url} failed: {error.reason}") from error
|
raise SystemExit(f"{method} {url} failed: {error.reason}") from error
|
||||||
|
|
||||||
|
|
||||||
def list_versions(parsed, prefix, owner, package_name, auth):
|
def list_versions(parsed, prefix, owner, package_name, auth):
|
||||||
versions = []
|
versions = []
|
||||||
page = 1
|
page = 1
|
||||||
@@ -128,7 +118,6 @@ def list_versions(parsed, prefix, owner, package_name, auth):
|
|||||||
page += 1
|
page += 1
|
||||||
return sorted(set(versions))
|
return sorted(set(versions))
|
||||||
|
|
||||||
|
|
||||||
def delete_version(parsed, prefix, owner, package_name, version, auth):
|
def delete_version(parsed, prefix, owner, package_name, version, auth):
|
||||||
url = version_delete_url(parsed, prefix, owner, package_name, version)
|
url = version_delete_url(parsed, prefix, owner, package_name, version)
|
||||||
response = request("DELETE", url, auth, missing_ok=True)
|
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}")
|
raise SystemExit(f"DELETE {url} returned HTTP {response.status}")
|
||||||
print(f"Deleted package version: {package_name}/{version}")
|
print(f"Deleted package version: {package_name}/{version}")
|
||||||
|
|
||||||
|
|
||||||
def discover_artifacts(dist_dir, package_name, version):
|
def discover_artifacts(dist_dir, package_name, version):
|
||||||
archives = sorted(dist_dir.glob(f"{package_name}-{version}-*.tar.gz"))
|
archives = sorted(dist_dir.glob(f"{package_name}-{version}-*.tar.gz"))
|
||||||
artifacts = []
|
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))
|
raise SystemExit("missing artifact files: " + ", ".join(str(path) for path in missing))
|
||||||
return artifacts
|
return artifacts
|
||||||
|
|
||||||
|
|
||||||
def upload_file(registry_url, package_name, version, path, auth):
|
def upload_file(registry_url, package_name, version, path, auth):
|
||||||
url = file_upload_url(registry_url, package_name, version, path)
|
url = file_upload_url(registry_url, package_name, version, path)
|
||||||
headers = {
|
headers = {
|
||||||
@@ -170,7 +157,6 @@ def upload_file(registry_url, package_name, version, path, auth):
|
|||||||
raise SystemExit(f"PUT {url} returned HTTP {response.status}")
|
raise SystemExit(f"PUT {url} returned HTTP {response.status}")
|
||||||
print(f"Uploaded package file: {package_name}/{version}/{path.name}")
|
print(f"Uploaded package file: {package_name}/{version}/{path.name}")
|
||||||
|
|
||||||
|
|
||||||
def parse_args(argv):
|
def parse_args(argv):
|
||||||
parser = argparse.ArgumentParser(description="Replace Product Vamp's rolling Gitea generic package.")
|
parser = argparse.ArgumentParser(description="Replace Product Vamp's rolling Gitea generic package.")
|
||||||
parser.add_argument("--config", type=pathlib.Path, default=pathlib.Path("modulus.json"))
|
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)
|
parser.add_argument("--token", default=None)
|
||||||
return parser.parse_args(argv)
|
return parser.parse_args(argv)
|
||||||
|
|
||||||
|
|
||||||
def main(argv):
|
def main(argv):
|
||||||
args = parse_args(argv)
|
args = parse_args(argv)
|
||||||
package_name, registry_url = read_config(args.config)
|
package_name, registry_url = read_config(args.config)
|
||||||
@@ -189,30 +174,23 @@ def main(argv):
|
|||||||
auth = authorization(user, token)
|
auth = authorization(user, token)
|
||||||
parsed, prefix, owner = parse_registry_url(registry_url)
|
parsed, prefix, owner = parse_registry_url(registry_url)
|
||||||
artifacts = discover_artifacts(args.dist_dir, package_name, args.version)
|
artifacts = discover_artifacts(args.dist_dir, package_name, args.version)
|
||||||
|
|
||||||
existing_versions = list_versions(parsed, prefix, owner, package_name, auth)
|
existing_versions = list_versions(parsed, prefix, owner, package_name, auth)
|
||||||
|
|
||||||
# Gitea rejects duplicate file names within a generic package version, so
|
# Gitea rejects duplicate file names within a generic package version, so
|
||||||
# replacing the rolling release requires deleting the old version first.
|
# replacing the rolling release requires deleting the old version first.
|
||||||
if args.version in existing_versions:
|
if args.version in existing_versions:
|
||||||
delete_version(parsed, prefix, owner, package_name, args.version, auth)
|
delete_version(parsed, prefix, owner, package_name, args.version, auth)
|
||||||
|
|
||||||
for artifact in artifacts:
|
for artifact in artifacts:
|
||||||
upload_file(registry_url, package_name, args.version, artifact, auth)
|
upload_file(registry_url, package_name, args.version, artifact, auth)
|
||||||
|
|
||||||
for version in existing_versions:
|
for version in existing_versions:
|
||||||
if version != args.version:
|
if version != args.version:
|
||||||
delete_version(parsed, prefix, owner, package_name, version, auth)
|
delete_version(parsed, prefix, owner, package_name, version, auth)
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
def os_environ(name):
|
def os_environ(name):
|
||||||
value = os.environ.get(name)
|
value = os.environ.get(name)
|
||||||
if not value:
|
if not value:
|
||||||
raise SystemExit(f"{name} is required")
|
raise SystemExit(f"{name} is required")
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
raise SystemExit(main(sys.argv[1:]))
|
raise SystemExit(main(sys.argv[1:]))
|
||||||
|
|||||||
+20
-16
@@ -12,6 +12,7 @@
|
|||||||
#include <vamp_pico/display.hpp>
|
#include <vamp_pico/display.hpp>
|
||||||
#include <vamp_style/font.hpp>
|
#include <vamp_style/font.hpp>
|
||||||
|
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
constexpr auto pin_lcd_cs = std::uint32_t{9};
|
constexpr auto pin_lcd_cs = std::uint32_t{9};
|
||||||
@@ -37,7 +38,7 @@ auto command(std::uchar_t value) -> void {
|
|||||||
deselect();
|
deselect();
|
||||||
}
|
}
|
||||||
|
|
||||||
auto data(std::uchar_t const* values, std::size_t count) -> void {
|
auto data(std::uchar_t const *values, std::size_t count) -> void {
|
||||||
gpio_put(pin_lcd_dc, 1);
|
gpio_put(pin_lcd_dc, 1);
|
||||||
select();
|
select();
|
||||||
spi_write_blocking(spi1, values, count);
|
spi_write_blocking(spi1, values, count);
|
||||||
@@ -77,14 +78,14 @@ auto same_color(vamp_style::color left, vamp_style::color right) -> std::bool_t
|
|||||||
}
|
}
|
||||||
|
|
||||||
template <std::size_t Count>
|
template <std::size_t Count>
|
||||||
auto same_text(std::array<std::char_t, Count> const& cached, std::char_t const* text) -> std::bool_t {
|
auto same_text(std::array<std::char_t, Count> const &cached, std::char_t const *text) -> std::bool_t {
|
||||||
auto const* value = text == nullptr ? "" : text;
|
auto const *value = text == nullptr ? "" : text;
|
||||||
return std::strncmp(cached.data(), value, Count) == 0;
|
return std::strncmp(cached.data(), value, Count) == 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
template <std::size_t Count>
|
template <std::size_t Count>
|
||||||
auto copy_text(std::array<std::char_t, Count>& cached, std::char_t const* text) -> void {
|
auto copy_text(std::array<std::char_t, Count> &cached, std::char_t const *text) -> void {
|
||||||
auto const* value = text == nullptr ? "" : text;
|
auto const *value = text == nullptr ? "" : text;
|
||||||
auto count = std::min(Count - 1U, std::strlen(value));
|
auto count = std::min(Count - 1U, std::strlen(value));
|
||||||
std::memcpy(cached.data(), value, count);
|
std::memcpy(cached.data(), value, count);
|
||||||
cached[count] = '\0';
|
cached[count] = '\0';
|
||||||
@@ -108,7 +109,7 @@ auto blend(vamp_style::color foreground, vamp_style::color background, std::uint
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
auto draw_centered_text(vamp_pico::display& display, std::int32_t y, std::char_t const* text, std::uint16_t color, std::int32_t preferred_scale, std::int32_t max_width) -> void {
|
auto draw_centered_text(vamp_pico::display &display, std::int32_t y, std::char_t const *text, std::uint16_t color, std::int32_t preferred_scale, std::int32_t max_width) -> void {
|
||||||
if (text == nullptr || text[0] == '\0') {
|
if (text == nullptr || text[0] == '\0') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -189,7 +190,7 @@ auto vamp_pico::display::fill_rect(std::int32_t x, std::int32_t y, std::int32_t
|
|||||||
deselect();
|
deselect();
|
||||||
}
|
}
|
||||||
|
|
||||||
auto vamp_pico::display::draw_text(std::int32_t x, std::int32_t y, std::char_t const* text, std::uint16_t color, std::int32_t scale) -> void {
|
auto vamp_pico::display::draw_text(std::int32_t x, std::int32_t y, std::char_t const *text, std::uint16_t color, std::int32_t scale) -> void {
|
||||||
if (text == nullptr || scale <= 0) {
|
if (text == nullptr || scale <= 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -210,14 +211,16 @@ auto vamp_pico::display::draw_text(std::int32_t x, std::int32_t y, std::char_t c
|
|||||||
auto col = std::int32_t{};
|
auto col = std::int32_t{};
|
||||||
while (col < static_cast<std::int32_t>(glyph.width)) {
|
while (col < static_cast<std::int32_t>(glyph.width)) {
|
||||||
auto row_offset = static_cast<std::size_t>(row * vamp_style::ui_font_max_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;
|
++col;
|
||||||
}
|
}
|
||||||
auto start = col;
|
auto start = col;
|
||||||
auto alpha = start < static_cast<std::int32_t>(glyph.width)
|
auto alpha = start < static_cast<std::int32_t>(glyph.width)
|
||||||
? glyph.alpha[row_offset + static_cast<std::size_t>(start)]
|
? glyph.alpha[row_offset + static_cast<std::size_t>(start)]
|
||||||
: std::uint8_t{};
|
: 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;
|
++col;
|
||||||
}
|
}
|
||||||
if (col > start) {
|
if (col > start) {
|
||||||
@@ -234,7 +237,7 @@ auto vamp_pico::display::draw_text(std::int32_t x, std::int32_t y, std::char_t c
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
auto vamp_pico::display::show_boot(std::char_t const* line1, std::char_t const* line2, std::uint16_t accent) -> void {
|
auto vamp_pico::display::show_boot(std::char_t const *line1, std::char_t const *line2, std::uint16_t accent) -> void {
|
||||||
remote_valid_ = false;
|
remote_valid_ = false;
|
||||||
fill(vamp_style::rgb565(vamp_style::background));
|
fill(vamp_style::rgb565(vamp_style::background));
|
||||||
fill_rect(0, 0, vamp_pico::display_width, 28, accent);
|
fill_rect(0, 0, vamp_pico::display_width, 28, accent);
|
||||||
@@ -247,11 +250,12 @@ auto vamp_pico::display::show_boot(std::char_t const* line1, std::char_t const*
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
auto vamp_pico::display::show_remote(vamp_style::color status, std::char_t const* title, std::char_t const* part, std::uint32_t page, std::uint32_t pages, std::bool_t active) -> void {
|
auto vamp_pico::display::show_remote(vamp_style::color status, std::char_t const *title, std::char_t const *part, std::uint32_t page, std::uint32_t pages, std::bool_t active) -> void {
|
||||||
auto const* title_text = title == nullptr ? "" : title;
|
auto const *title_text = title == nullptr ? "" : title;
|
||||||
auto const* part_text = part == nullptr ? "" : part;
|
auto const *part_text = part == nullptr ? "" : part;
|
||||||
auto status_changed = !remote_valid_ || !same_color(last_status_, status);
|
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_) {
|
if (!remote_valid_) {
|
||||||
fill(vamp_style::rgb565(vamp_style::background));
|
fill(vamp_style::rgb565(vamp_style::background));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,22 @@
|
|||||||
#include <vamp_pico/http_client.hpp>
|
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <array>
|
#include <array>
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
|
|
||||||
#include "lwip/ip_addr.h"
|
#include <lwip/ip_addr.h>
|
||||||
#include "lwip/pbuf.h"
|
#include <lwip/pbuf.h>
|
||||||
#include "lwip/tcp.h"
|
#include <lwip/tcp.h>
|
||||||
#include "pico/cyw43_arch.h"
|
#include <pico/cyw43_arch.h>
|
||||||
#include "pico/stdlib.h"
|
#include <pico/stdlib.h>
|
||||||
|
|
||||||
|
#include <vamp_pico/http_client.hpp>
|
||||||
|
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
struct request_state {
|
struct request_state {
|
||||||
tcp_pcb* pcb{};
|
tcp_pcb *pcb{};
|
||||||
std::bool_t done{};
|
std::bool_t done{};
|
||||||
std::bool_t status_known{};
|
std::bool_t status_known{};
|
||||||
std::bool_t success{};
|
std::bool_t success{};
|
||||||
@@ -25,11 +26,11 @@ struct request_state {
|
|||||||
std::size_t response_size{};
|
std::size_t response_size{};
|
||||||
};
|
};
|
||||||
|
|
||||||
auto response_is_success(std::char_t const* data, std::size_t size) -> std::bool_t {
|
auto response_is_success(std::char_t const *data, std::size_t size) -> std::bool_t {
|
||||||
return size >= 10U && (std::memcmp(data, "HTTP/1.1 2", 10U) == 0 || std::memcmp(data, "HTTP/1.0 2", 10U) == 0);
|
return size >= 10U && (std::memcmp(data, "HTTP/1.1 2", 10U) == 0 || std::memcmp(data, "HTTP/1.0 2", 10U) == 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
auto clear_callbacks(tcp_pcb* pcb) -> void {
|
auto clear_callbacks(tcp_pcb *pcb) -> void {
|
||||||
tcp_arg(pcb, nullptr);
|
tcp_arg(pcb, nullptr);
|
||||||
tcp_recv(pcb, nullptr);
|
tcp_recv(pcb, nullptr);
|
||||||
tcp_sent(pcb, nullptr);
|
tcp_sent(pcb, nullptr);
|
||||||
@@ -37,14 +38,13 @@ auto clear_callbacks(tcp_pcb* pcb) -> void {
|
|||||||
tcp_err(pcb, nullptr);
|
tcp_err(pcb, nullptr);
|
||||||
}
|
}
|
||||||
|
|
||||||
auto close_request(request_state& state, std::bool_t success) -> err_t {
|
auto close_request(request_state &state, std::bool_t success) -> err_t {
|
||||||
state.success = success;
|
state.success = success;
|
||||||
state.done = true;
|
state.done = true;
|
||||||
if (state.pcb == nullptr) {
|
if (state.pcb == nullptr) {
|
||||||
return ERR_OK;
|
return ERR_OK;
|
||||||
}
|
}
|
||||||
|
auto *pcb = state.pcb;
|
||||||
auto* pcb = state.pcb;
|
|
||||||
state.pcb = nullptr;
|
state.pcb = nullptr;
|
||||||
clear_callbacks(pcb);
|
clear_callbacks(pcb);
|
||||||
if (tcp_close(pcb) == ERR_OK) {
|
if (tcp_close(pcb) == ERR_OK) {
|
||||||
@@ -54,7 +54,7 @@ auto close_request(request_state& state, std::bool_t success) -> err_t {
|
|||||||
return ERR_ABRT;
|
return ERR_ABRT;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto append_response(request_state& state, pbuf* buffer) -> void {
|
auto append_response(request_state &state, pbuf *buffer) -> void {
|
||||||
auto remaining = sizeof(state.response) - state.response_size - 1U;
|
auto remaining = sizeof(state.response) - state.response_size - 1U;
|
||||||
if (remaining != 0U) {
|
if (remaining != 0U) {
|
||||||
auto copied = pbuf_copy_partial(buffer, state.response + state.response_size, std::min<std::uint16_t>(buffer->tot_len, static_cast<std::uint16_t>(remaining)), 0);
|
auto copied = pbuf_copy_partial(buffer, state.response + state.response_size, std::min<std::uint16_t>(buffer->tot_len, static_cast<std::uint16_t>(remaining)), 0);
|
||||||
@@ -67,8 +67,8 @@ auto append_response(request_state& state, pbuf* buffer) -> void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
auto handle_receive(void* argument, tcp_pcb* pcb, pbuf* buffer, err_t error) -> err_t {
|
auto handle_receive(void *argument, tcp_pcb *pcb, pbuf *buffer, err_t error) -> err_t {
|
||||||
auto* state = static_cast<request_state*>(argument);
|
auto *state = static_cast<request_state *>(argument);
|
||||||
if (state == nullptr) {
|
if (state == nullptr) {
|
||||||
if (buffer != nullptr) {
|
if (buffer != nullptr) {
|
||||||
pbuf_free(buffer);
|
pbuf_free(buffer);
|
||||||
@@ -86,8 +86,8 @@ auto handle_receive(void* argument, tcp_pcb* pcb, pbuf* buffer, err_t error) ->
|
|||||||
return ERR_OK;
|
return ERR_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto handle_connected(void* argument, tcp_pcb* pcb, err_t error) -> err_t {
|
auto handle_connected(void *argument, tcp_pcb *pcb, err_t error) -> err_t {
|
||||||
auto* state = static_cast<request_state*>(argument);
|
auto *state = static_cast<request_state *>(argument);
|
||||||
if (state == nullptr) {
|
if (state == nullptr) {
|
||||||
return ERR_ARG;
|
return ERR_ARG;
|
||||||
}
|
}
|
||||||
@@ -95,7 +95,6 @@ auto handle_connected(void* argument, tcp_pcb* pcb, err_t error) -> err_t {
|
|||||||
auto closed = close_request(*state, false);
|
auto closed = close_request(*state, false);
|
||||||
return closed == ERR_ABRT ? closed : error;
|
return closed == ERR_ABRT ? closed : error;
|
||||||
}
|
}
|
||||||
|
|
||||||
tcp_arg(pcb, state);
|
tcp_arg(pcb, state);
|
||||||
tcp_recv(pcb, handle_receive);
|
tcp_recv(pcb, handle_receive);
|
||||||
auto written = tcp_write(pcb, state->request, static_cast<std::uint16_t>(state->request_size), TCP_WRITE_FLAG_COPY);
|
auto written = tcp_write(pcb, state->request, static_cast<std::uint16_t>(state->request_size), TCP_WRITE_FLAG_COPY);
|
||||||
@@ -111,8 +110,8 @@ auto handle_connected(void* argument, tcp_pcb* pcb, err_t error) -> err_t {
|
|||||||
return ERR_OK;
|
return ERR_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto handle_error(void* argument, err_t) -> void {
|
auto handle_error(void *argument, err_t) -> void {
|
||||||
auto* state = static_cast<request_state*>(argument);
|
auto *state = static_cast<request_state *>(argument);
|
||||||
if (state == nullptr) {
|
if (state == nullptr) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -121,43 +120,25 @@ auto handle_error(void* argument, err_t) -> void {
|
|||||||
state->done = true;
|
state->done = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto run_request(std::char_t const* method, std::char_t const* path, std::char_t const* body, std::char_t* response, std::size_t response_capacity, std::uint32_t timeout_ms) -> std::bool_t {
|
auto run_request(std::char_t const *method, std::char_t const *path, std::char_t const *body, std::char_t *response, std::size_t response_capacity, std::uint32_t timeout_ms) -> std::bool_t {
|
||||||
auto state = request_state{};
|
auto state = request_state{};
|
||||||
auto body_size = body == nullptr ? std::size_t{} : std::strlen(body);
|
auto body_size = body == nullptr ? std::size_t{} : std::strlen(body);
|
||||||
auto written = std::int32_t{};
|
auto written = std::int32_t{};
|
||||||
if (body_size == 0U) {
|
if (body_size == 0U) {
|
||||||
written = std::snprintf(
|
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);
|
||||||
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 {
|
} else {
|
||||||
written = std::snprintf(
|
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: "
|
||||||
state.request,
|
"close\r\n\r\n%s",
|
||||||
sizeof(state.request),
|
method, path, VAMP_ZERO_HOST, VAMP_ZERO_PORT, body_size, body);
|
||||||
"%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)) {
|
if (written <= 0 || static_cast<std::size_t>(written) >= sizeof(state.request)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
state.request_size = static_cast<std::size_t>(written);
|
state.request_size = static_cast<std::size_t>(written);
|
||||||
|
|
||||||
auto address = ip_addr_t{};
|
auto address = ip_addr_t{};
|
||||||
if (ipaddr_aton(VAMP_ZERO_HOST, &address) == 0) {
|
if (ipaddr_aton(VAMP_ZERO_HOST, &address) == 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
cyw43_arch_lwip_begin();
|
cyw43_arch_lwip_begin();
|
||||||
state.pcb = tcp_new_ip_type(IPADDR_TYPE_V4);
|
state.pcb = tcp_new_ip_type(IPADDR_TYPE_V4);
|
||||||
if (state.pcb == nullptr) {
|
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) {
|
if (connected != ERR_OK) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto deadline = make_timeout_time_ms(timeout_ms);
|
auto deadline = make_timeout_time_ms(timeout_ms);
|
||||||
while (!state.done && !time_reached(deadline)) {
|
while (!state.done && !time_reached(deadline)) {
|
||||||
sleep_ms(5);
|
sleep_ms(5);
|
||||||
@@ -192,14 +172,14 @@ auto run_request(std::char_t const* method, std::char_t const* path, std::char_t
|
|||||||
return state.success;
|
return state.success;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto body_start(std::char_t const* response) -> std::char_t const* {
|
auto body_start(std::char_t const *response) -> std::char_t const * {
|
||||||
auto* marker = std::strstr(response, "\r\n\r\n");
|
auto *marker = std::strstr(response, "\r\n\r\n");
|
||||||
return marker == nullptr ? response : marker + 4;
|
return marker == nullptr ? response : marker + 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
auto vamp_pico::post_remote_command(vamp_protocol::page_command command, vamp_pico::remote_display_state& state) -> std::bool_t {
|
auto vamp_pico::post_remote_command(vamp_protocol::page_command command, vamp_pico::remote_display_state &state) -> std::bool_t {
|
||||||
auto response = std::array<std::char_t, 1024>{};
|
auto response = std::array<std::char_t, 1024>{};
|
||||||
if (!run_request("POST", vamp_protocol::page_command_path(command), "{}", response.data(), response.size(), 700)) {
|
if (!run_request("POST", vamp_protocol::page_command_path(command), "{}", response.data(), response.size(), 700)) {
|
||||||
return false;
|
return false;
|
||||||
@@ -214,7 +194,7 @@ auto vamp_pico::heartbeat() -> std::bool_t {
|
|||||||
return run_request("POST", vamp_protocol::remote_heartbeat_path, "{}", nullptr, 0, 700);
|
return run_request("POST", vamp_protocol::remote_heartbeat_path, "{}", nullptr, 0, 700);
|
||||||
}
|
}
|
||||||
|
|
||||||
auto vamp_pico::fetch_remote_display(vamp_pico::remote_display_state& state) -> std::bool_t {
|
auto vamp_pico::fetch_remote_display(vamp_pico::remote_display_state &state) -> std::bool_t {
|
||||||
auto response = std::array<std::char_t, 1024>{};
|
auto response = std::array<std::char_t, 1024>{};
|
||||||
if (!run_request("GET", vamp_protocol::remote_display_path, nullptr, response.data(), response.size(), 1000)) {
|
if (!run_request("GET", vamp_protocol::remote_display_path, nullptr, response.data(), response.size(), 1000)) {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
+8
-16
@@ -1,10 +1,11 @@
|
|||||||
|
#include <pico/cyw43_arch.h>
|
||||||
|
#include <pico/stdlib.h>
|
||||||
|
|
||||||
#include <vamp_pico/display.hpp>
|
#include <vamp_pico/display.hpp>
|
||||||
#include <vamp_pico/http_client.hpp>
|
#include <vamp_pico/http_client.hpp>
|
||||||
#include <vamp_pico/touch.hpp>
|
#include <vamp_pico/touch.hpp>
|
||||||
#include <vamp_protocol/page_control.hpp>
|
#include <vamp_protocol/page_control.hpp>
|
||||||
|
|
||||||
#include "pico/cyw43_arch.h"
|
|
||||||
#include "pico/stdlib.h"
|
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
@@ -34,15 +35,15 @@ auto color_for(link_status status) -> vamp_style::color {
|
|||||||
return vamp_style::retrying;
|
return vamp_style::retrying;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto render(vamp_pico::display& display, link_status status, vamp_pico::remote_display_state const& remote) -> void {
|
auto render(vamp_pico::display &display, link_status status, vamp_pico::remote_display_state const &remote) -> void {
|
||||||
display.show_remote(color_for(status), remote.title, remote.part, remote.page, remote.pages, remote.active);
|
display.show_remote(color_for(status), remote.title, remote.part, remote.page, remote.pages, remote.active);
|
||||||
}
|
}
|
||||||
|
|
||||||
auto clear_remote(vamp_pico::remote_display_state& remote) -> void {
|
auto clear_remote(vamp_pico::remote_display_state &remote) -> void {
|
||||||
remote = vamp_pico::remote_display_state{};
|
remote = vamp_pico::remote_display_state{};
|
||||||
}
|
}
|
||||||
|
|
||||||
auto connect_wifi(vamp_pico::display& display) -> std::bool_t {
|
auto connect_wifi(vamp_pico::display &display) -> std::bool_t {
|
||||||
auto empty = vamp_pico::remote_display_state{};
|
auto empty = vamp_pico::remote_display_state{};
|
||||||
render(display, link_status::CONNECTING, empty);
|
render(display, link_status::CONNECTING, empty);
|
||||||
if (cyw43_arch_init() != 0) {
|
if (cyw43_arch_init() != 0) {
|
||||||
@@ -59,7 +60,7 @@ auto connect_wifi(vamp_pico::display& display) -> std::bool_t {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto send_command(vamp_protocol::page_command command, link_status& status, vamp_pico::remote_display_state& remote, vamp_pico::display& display) -> void {
|
auto send_command(vamp_protocol::page_command command, link_status &status, vamp_pico::remote_display_state &remote, vamp_pico::display &display) -> void {
|
||||||
if (!vamp_pico::post_remote_command(command, remote)) {
|
if (!vamp_pico::post_remote_command(command, remote)) {
|
||||||
status = link_status::RETRYING;
|
status = link_status::RETRYING;
|
||||||
clear_remote(remote);
|
clear_remote(remote);
|
||||||
@@ -70,23 +71,20 @@ auto send_command(vamp_protocol::page_command command, link_status& status, vamp
|
|||||||
render(display, status, remote);
|
render(display, status, remote);
|
||||||
}
|
}
|
||||||
|
|
||||||
auto update_gesture(gesture_state& gesture, std::bool_t touching, link_status& status, vamp_pico::remote_display_state& remote, vamp_pico::display& display) -> void {
|
auto update_gesture(gesture_state &gesture, std::bool_t touching, link_status &status, vamp_pico::remote_display_state &remote, vamp_pico::display &display) -> void {
|
||||||
constexpr auto hold_us = std::int64_t{700000};
|
constexpr auto hold_us = std::int64_t{700000};
|
||||||
constexpr auto double_tap_us = std::int64_t{220000};
|
constexpr auto double_tap_us = std::int64_t{220000};
|
||||||
auto now = get_absolute_time();
|
auto now = get_absolute_time();
|
||||||
|
|
||||||
if (touching && !gesture.touching) {
|
if (touching && !gesture.touching) {
|
||||||
gesture.touching = true;
|
gesture.touching = true;
|
||||||
gesture.hold_sent = false;
|
gesture.hold_sent = false;
|
||||||
gesture.down_time = now;
|
gesture.down_time = now;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (touching && !gesture.hold_sent && absolute_time_diff_us(gesture.down_time, now) >= hold_us) {
|
if (touching && !gesture.hold_sent && absolute_time_diff_us(gesture.down_time, now) >= hold_us) {
|
||||||
gesture.hold_sent = true;
|
gesture.hold_sent = true;
|
||||||
gesture.pending_tap = false;
|
gesture.pending_tap = false;
|
||||||
send_command(vamp_protocol::page_command::SELECT, status, remote, display);
|
send_command(vamp_protocol::page_command::SELECT, status, remote, display);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!touching && gesture.touching) {
|
if (!touching && gesture.touching) {
|
||||||
gesture.touching = false;
|
gesture.touching = false;
|
||||||
if (!gesture.hold_sent) {
|
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) {
|
if (!touching && gesture.pending_tap && absolute_time_diff_us(gesture.last_up_time, now) > double_tap_us) {
|
||||||
gesture.pending_tap = false;
|
gesture.pending_tap = false;
|
||||||
send_command(vamp_protocol::page_command::NEXT, status, remote, display);
|
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 {
|
auto main() -> int {
|
||||||
stdio_init_all();
|
stdio_init_all();
|
||||||
|
|
||||||
auto display = vamp_pico::display{};
|
auto display = vamp_pico::display{};
|
||||||
auto touch = vamp_pico::touch_controller{};
|
auto touch = vamp_pico::touch_controller{};
|
||||||
display.init();
|
display.init();
|
||||||
touch.init();
|
touch.init();
|
||||||
|
|
||||||
if (!connect_wifi(display)) {
|
if (!connect_wifi(display)) {
|
||||||
while (true) {
|
while (true) {
|
||||||
sleep_ms(1000);
|
sleep_ms(1000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
auto remote = vamp_pico::remote_display_state{};
|
auto remote = vamp_pico::remote_display_state{};
|
||||||
auto status = link_status::RETRYING;
|
auto status = link_status::RETRYING;
|
||||||
auto gesture = gesture_state{};
|
auto gesture = gesture_state{};
|
||||||
auto last_poll = get_absolute_time();
|
auto last_poll = get_absolute_time();
|
||||||
auto poll_now = std::bool_t{true};
|
auto poll_now = std::bool_t{true};
|
||||||
render(display, status, remote);
|
render(display, status, remote);
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
auto point = vamp_pico::touch_point{};
|
auto point = vamp_pico::touch_point{};
|
||||||
update_gesture(gesture, touch.read(point), status, remote, display);
|
update_gesture(gesture, touch.read(point), status, remote, display);
|
||||||
|
|
||||||
auto now = get_absolute_time();
|
auto now = get_absolute_time();
|
||||||
if (poll_now || absolute_time_diff_us(last_poll, now) >= 1000000) {
|
if (poll_now || absolute_time_diff_us(last_poll, now) >= 1000000) {
|
||||||
poll_now = false;
|
poll_now = false;
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
#include <vamp_pico/http_client.hpp>
|
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <charconv>
|
#include <charconv>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <limits>
|
#include <limits>
|
||||||
#include <system_error>
|
#include <system_error>
|
||||||
|
|
||||||
|
#include <vamp_pico/http_client.hpp>
|
||||||
|
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
auto copy_value(std::string_view value, std::char_t* output, std::size_t capacity) -> void {
|
auto copy_value(std::string_view value, std::char_t *output, std::size_t capacity) -> void {
|
||||||
if (capacity == 0U) {
|
if (capacity == 0U) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -17,13 +18,13 @@ auto copy_value(std::string_view value, std::char_t* output, std::size_t capacit
|
|||||||
output[count] = '\0';
|
output[count] = '\0';
|
||||||
}
|
}
|
||||||
|
|
||||||
auto parse_uint(std::string_view value, std::uint32_t& output) -> std::bool_t {
|
auto parse_uint(std::string_view value, std::uint32_t &output) -> std::bool_t {
|
||||||
if (value.empty()) {
|
if (value.empty()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
auto parsed = std::uint64_t{};
|
auto parsed = std::uint64_t{};
|
||||||
auto const* begin = value.data();
|
auto const *begin = value.data();
|
||||||
auto const* end = value.data() + value.size();
|
auto const *end = value.data() + value.size();
|
||||||
auto result = std::from_chars(begin, end, parsed, 10);
|
auto result = std::from_chars(begin, end, parsed, 10);
|
||||||
if (result.ec != std::errc{} || result.ptr != end || parsed > std::numeric_limits<std::uint32_t>::max()) {
|
if (result.ec != std::errc{} || result.ptr != end || parsed > std::numeric_limits<std::uint32_t>::max()) {
|
||||||
return false;
|
return false;
|
||||||
@@ -41,7 +42,7 @@ auto strip_cr(std::string_view line) -> std::string_view {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
auto vamp_pico::parse_remote_display_body(std::string_view body, vamp_pico::remote_display_state& state) -> std::bool_t {
|
auto vamp_pico::parse_remote_display_body(std::string_view body, vamp_pico::remote_display_state &state) -> std::bool_t {
|
||||||
auto parsed = vamp_pico::remote_display_state{};
|
auto parsed = vamp_pico::remote_display_state{};
|
||||||
auto cursor = std::size_t{};
|
auto cursor = std::size_t{};
|
||||||
while (cursor <= body.size()) {
|
while (cursor <= body.size()) {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
#include <vamp_pico/display.hpp>
|
#include <vamp_pico/display.hpp>
|
||||||
#include <vamp_pico/touch.hpp>
|
#include <vamp_pico/touch.hpp>
|
||||||
|
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
constexpr auto pin_tp_rst = std::uint32_t{5};
|
constexpr auto pin_tp_rst = std::uint32_t{5};
|
||||||
@@ -34,13 +35,14 @@ auto vamp_pico::touch_controller::init() -> void {
|
|||||||
gpio_pull_up(pin_tp_int);
|
gpio_pull_up(pin_tp_int);
|
||||||
}
|
}
|
||||||
|
|
||||||
auto vamp_pico::touch_controller::read(vamp_pico::touch_point& point) -> std::bool_t {
|
auto vamp_pico::touch_controller::read(vamp_pico::touch_point &point) -> std::bool_t {
|
||||||
auto reg = std::uchar_t{0x02};
|
auto reg = std::uchar_t{0x02};
|
||||||
auto data = std::array<std::uchar_t, 5>{};
|
auto data = std::array<std::uchar_t, 5>{};
|
||||||
if (i2c_write_blocking(i2c1, cst816_address, ®, 1, true) != 1) {
|
if (i2c_write_blocking(i2c1, cst816_address, ®, 1, true) != 1) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (i2c_read_blocking(i2c1, cst816_address, data.data(), data.size(), false) != static_cast<std::int32_t>(data.size())) {
|
if (i2c_read_blocking(i2c1, cst816_address, data.data(), data.size(), false) !=
|
||||||
|
static_cast<std::int32_t>(data.size())) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
auto fingers = data[0] & 0x0f;
|
auto fingers = data[0] & 0x0f;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#include <vamp_protocol/page_control.hpp>
|
#include <vamp_protocol/page_control.hpp>
|
||||||
|
|
||||||
auto vamp_protocol::page_command_path(vamp_protocol::page_command command) -> std::char_t const* {
|
|
||||||
|
auto vamp_protocol::page_command_path(vamp_protocol::page_command command) -> std::char_t const * {
|
||||||
switch (command) {
|
switch (command) {
|
||||||
case vamp_protocol::page_command::PREVIOUS:
|
case vamp_protocol::page_command::PREVIOUS:
|
||||||
return vamp_protocol::remote_previous_path;
|
return vamp_protocol::remote_previous_path;
|
||||||
@@ -12,7 +13,7 @@ auto vamp_protocol::page_command_path(vamp_protocol::page_command command) -> st
|
|||||||
return vamp_protocol::remote_next_path;
|
return vamp_protocol::remote_next_path;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto vamp_protocol::page_command_legacy_path(vamp_protocol::page_command command) -> std::char_t const* {
|
auto vamp_protocol::page_command_legacy_path(vamp_protocol::page_command command) -> std::char_t const * {
|
||||||
switch (command) {
|
switch (command) {
|
||||||
case vamp_protocol::page_command::PREVIOUS:
|
case vamp_protocol::page_command::PREVIOUS:
|
||||||
return vamp_protocol::previous_page_path;
|
return vamp_protocol::previous_page_path;
|
||||||
@@ -24,7 +25,7 @@ auto vamp_protocol::page_command_legacy_path(vamp_protocol::page_command command
|
|||||||
return vamp_protocol::next_page_path;
|
return vamp_protocol::next_page_path;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto vamp_protocol::page_command_name(vamp_protocol::page_command command) -> std::char_t const* {
|
auto vamp_protocol::page_command_name(vamp_protocol::page_command command) -> std::char_t const * {
|
||||||
switch (command) {
|
switch (command) {
|
||||||
case vamp_protocol::page_command::PREVIOUS:
|
case vamp_protocol::page_command::PREVIOUS:
|
||||||
return "previous";
|
return "previous";
|
||||||
@@ -36,7 +37,7 @@ auto vamp_protocol::page_command_name(vamp_protocol::page_command command) -> st
|
|||||||
return "next";
|
return "next";
|
||||||
}
|
}
|
||||||
|
|
||||||
auto vamp_protocol::page_command_display_label(vamp_protocol::page_command command) -> std::char_t const* {
|
auto vamp_protocol::page_command_display_label(vamp_protocol::page_command command) -> std::char_t const * {
|
||||||
switch (command) {
|
switch (command) {
|
||||||
case vamp_protocol::page_command::PREVIOUS:
|
case vamp_protocol::page_command::PREVIOUS:
|
||||||
return "PREV";
|
return "PREV";
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
|
||||||
|
|
||||||
auto vamp_style::rgb565(vamp_style::color value) -> std::uint16_t {
|
auto vamp_style::rgb565(vamp_style::color value) -> std::uint16_t {
|
||||||
auto r = static_cast<std::uint16_t>((value.r >> 3U) & 0x1fU);
|
auto r = static_cast<std::uint16_t>((value.r >> 3U) & 0x1fU);
|
||||||
auto g = static_cast<std::uint16_t>((value.g >> 2U) & 0x3fU);
|
auto g = static_cast<std::uint16_t>((value.g >> 2U) & 0x3fU);
|
||||||
|
|||||||
+37
-37
@@ -1,5 +1,3 @@
|
|||||||
#include <vamp_zero/app.hpp>
|
|
||||||
|
|
||||||
#include <csignal>
|
#include <csignal>
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
@@ -26,6 +24,7 @@
|
|||||||
#include <ligo.hpp>
|
#include <ligo.hpp>
|
||||||
#include <modulus.hpp>
|
#include <modulus.hpp>
|
||||||
|
|
||||||
|
#include <vamp_zero/app.hpp>
|
||||||
#include <vamp_zero/config.hpp>
|
#include <vamp_zero/config.hpp>
|
||||||
#include <vamp_zero/audio.hpp>
|
#include <vamp_zero/audio.hpp>
|
||||||
#include <vamp_zero/database.hpp>
|
#include <vamp_zero/database.hpp>
|
||||||
@@ -35,6 +34,7 @@
|
|||||||
#include <vamp_zero/portal.hpp>
|
#include <vamp_zero/portal.hpp>
|
||||||
#include <vamp_zero/ui.hpp>
|
#include <vamp_zero/ui.hpp>
|
||||||
|
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
auto invalid(std::string message) -> modulus::error {
|
auto invalid(std::string message) -> modulus::error {
|
||||||
@@ -45,7 +45,7 @@ auto usage() -> std::string {
|
|||||||
return "usage: vamp-zero [--config PATH] [--check-config] [--health-check] [--version] [--once] [--ensure-database]";
|
return "usage: vamp-zero [--config PATH] [--check-config] [--health-check] [--version] [--once] [--ensure-database]";
|
||||||
}
|
}
|
||||||
|
|
||||||
auto bind_port(vamp_zero::config const& config) -> std::uint16_t {
|
auto bind_port(vamp_zero::config const &config) -> std::uint16_t {
|
||||||
auto endpoint = ligo::http::server_endpoint_from_config(config.control.endpoint);
|
auto endpoint = ligo::http::server_endpoint_from_config(config.control.endpoint);
|
||||||
if (!endpoint) {
|
if (!endpoint) {
|
||||||
return 8080;
|
return 8080;
|
||||||
@@ -53,7 +53,7 @@ auto bind_port(vamp_zero::config const& config) -> std::uint16_t {
|
|||||||
return endpoint->port == 0 ? 8080 : endpoint->port;
|
return endpoint->port == 0 ? 8080 : endpoint->port;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto write_text_atomic(std::filesystem::path const& destination, std::string const& text) -> std::expected<void, modulus::error> {
|
auto write_text_atomic(std::filesystem::path const &destination, std::string const &text) -> std::expected<void, modulus::error> {
|
||||||
auto error = std::error_code{};
|
auto error = std::error_code{};
|
||||||
if (!destination.parent_path().empty()) {
|
if (!destination.parent_path().empty()) {
|
||||||
std::filesystem::create_directories(destination.parent_path(), error);
|
std::filesystem::create_directories(destination.parent_path(), error);
|
||||||
@@ -80,7 +80,7 @@ auto write_text_atomic(std::filesystem::path const& destination, std::string con
|
|||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
auto write_heartbeat(vamp_zero::config const& config) -> std::expected<void, modulus::error> {
|
auto write_heartbeat(vamp_zero::config const &config) -> std::expected<void, modulus::error> {
|
||||||
auto heartbeat = boost::json::object{};
|
auto heartbeat = boost::json::object{};
|
||||||
heartbeat["version"] = VAMP_VERSION;
|
heartbeat["version"] = VAMP_VERSION;
|
||||||
heartbeat["git_sha"] = VAMP_GIT_SHA;
|
heartbeat["git_sha"] = VAMP_GIT_SHA;
|
||||||
@@ -89,14 +89,14 @@ auto write_heartbeat(vamp_zero::config const& config) -> std::expected<void, mod
|
|||||||
return write_text_atomic(config.runtime.heartbeat_path, boost::json::serialize(heartbeat));
|
return write_text_atomic(config.runtime.heartbeat_path, boost::json::serialize(heartbeat));
|
||||||
}
|
}
|
||||||
|
|
||||||
auto ensure_database(vamp_zero::config const& config) -> std::expected<void, modulus::error> {
|
auto ensure_database(vamp_zero::config const &config) -> std::expected<void, modulus::error> {
|
||||||
auto io = boost::asio::io_context{};
|
auto io = boost::asio::io_context{};
|
||||||
auto future = boost::asio::co_spawn(io, vamp_zero::async_ensure_database(io.get_executor(), config.database), boost::asio::use_future);
|
auto future = boost::asio::co_spawn(io, vamp_zero::async_ensure_database(io.get_executor(), config.database), boost::asio::use_future);
|
||||||
io.run();
|
io.run();
|
||||||
return future.get();
|
return future.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
auto make_server_endpoint(vamp_zero::config const& config) -> std::expected<ligo::http::endpoint, modulus::error> {
|
auto make_server_endpoint(vamp_zero::config const &config) -> std::expected<ligo::http::endpoint, modulus::error> {
|
||||||
auto endpoint = ligo::http::server_endpoint_from_config(config.control.endpoint);
|
auto endpoint = ligo::http::server_endpoint_from_config(config.control.endpoint);
|
||||||
if (!endpoint) {
|
if (!endpoint) {
|
||||||
return std::unexpected(endpoint.error());
|
return std::unexpected(endpoint.error());
|
||||||
@@ -107,14 +107,14 @@ auto make_server_endpoint(vamp_zero::config const& config) -> std::expected<ligo
|
|||||||
return endpoint;
|
return endpoint;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto import_library(vamp_zero::config const& config) -> std::expected<void, modulus::error> {
|
auto import_library(vamp_zero::config const &config) -> std::expected<void, modulus::error> {
|
||||||
auto io = boost::asio::io_context{};
|
auto io = boost::asio::io_context{};
|
||||||
auto future = boost::asio::co_spawn(io, vamp_zero::async_import_catalogs(io.get_executor(), config.database, config.runtime.assets_path), boost::asio::use_future);
|
auto future = boost::asio::co_spawn(io, vamp_zero::async_import_catalogs(io.get_executor(), config.database, config.runtime.assets_path), boost::asio::use_future);
|
||||||
io.run();
|
io.run();
|
||||||
return future.get();
|
return future.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
auto load_initial_state(vamp_zero::config const& config) -> std::expected<vamp_zero::app_state, modulus::error> {
|
auto load_initial_state(vamp_zero::config const &config) -> std::expected<vamp_zero::app_state, modulus::error> {
|
||||||
auto io = boost::asio::io_context{};
|
auto io = boost::asio::io_context{};
|
||||||
auto performance_future = boost::asio::co_spawn(io, vamp_zero::async_load_performance_library(io.get_executor(), config.database), boost::asio::use_future);
|
auto performance_future = boost::asio::co_spawn(io, vamp_zero::async_load_performance_library(io.get_executor(), config.database), boost::asio::use_future);
|
||||||
auto practice_future = boost::asio::co_spawn(io, vamp_zero::async_load_practice_library(io.get_executor(), config.database), boost::asio::use_future);
|
auto practice_future = boost::asio::co_spawn(io, vamp_zero::async_load_practice_library(io.get_executor(), config.database), boost::asio::use_future);
|
||||||
@@ -130,14 +130,15 @@ auto load_initial_state(vamp_zero::config const& config) -> std::expected<vamp_z
|
|||||||
auto state = vamp_zero::app_state{
|
auto state = vamp_zero::app_state{
|
||||||
.performance = std::move(*performance),
|
.performance = std::move(*performance),
|
||||||
.practice = std::move(*practice),
|
.practice = std::move(*practice),
|
||||||
.audio = {
|
.audio =
|
||||||
.track_enabled = config.audio.track_enabled,
|
{
|
||||||
.metronome_enabled = config.audio.metronome_enabled,
|
.track_enabled = config.audio.track_enabled,
|
||||||
.metronome_bpm = config.audio.metronome_bpm,
|
.metronome_enabled = config.audio.metronome_enabled,
|
||||||
.metronome_min_bpm = config.audio.metronome_min_bpm,
|
.metronome_bpm = config.audio.metronome_bpm,
|
||||||
.metronome_max_bpm = config.audio.metronome_max_bpm,
|
.metronome_min_bpm = config.audio.metronome_min_bpm,
|
||||||
.metronome_step = config.audio.metronome_step,
|
.metronome_max_bpm = config.audio.metronome_max_bpm,
|
||||||
},
|
.metronome_step = config.audio.metronome_step,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
if (auto valid = vamp_zero::validate_app_state(state); !valid) {
|
if (auto valid = vamp_zero::validate_app_state(state); !valid) {
|
||||||
return std::unexpected(valid.error());
|
return std::unexpected(valid.error());
|
||||||
@@ -145,21 +146,20 @@ auto load_initial_state(vamp_zero::config const& config) -> std::expected<vamp_z
|
|||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto run_headless(vamp_zero::config const& config, vamp_zero::control_state& state, vamp_zero::audio_controller* audio) -> std::expected<void, modulus::error> {
|
auto run_headless(vamp_zero::config const &config, vamp_zero::control_state &state, vamp_zero::audio_controller *audio) -> std::expected<void, modulus::error> {
|
||||||
auto io = boost::asio::io_context{};
|
auto io = boost::asio::io_context{};
|
||||||
auto endpoint = make_server_endpoint(config);
|
auto endpoint = make_server_endpoint(config);
|
||||||
if (!endpoint) {
|
if (!endpoint) {
|
||||||
return std::unexpected(endpoint.error());
|
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();
|
auto started = server.start();
|
||||||
if (!started) {
|
if (!started) {
|
||||||
return std::unexpected(started.error());
|
return std::unexpected(started.error());
|
||||||
}
|
}
|
||||||
if (audio != nullptr) {
|
if (audio != nullptr) {
|
||||||
state.set_observer([audio](vamp_zero::app_state app) {
|
state.set_observer([audio](vamp_zero::app_state app) { audio->sync(app); });
|
||||||
audio->sync(app);
|
|
||||||
});
|
|
||||||
audio->sync(state.snapshot());
|
audio->sync(state.snapshot());
|
||||||
}
|
}
|
||||||
auto heartbeat = write_heartbeat(config);
|
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());
|
return std::unexpected(heartbeat.error());
|
||||||
}
|
}
|
||||||
auto signals = boost::asio::signal_set{io, SIGINT, SIGTERM};
|
auto signals = boost::asio::signal_set{io, SIGINT, SIGTERM};
|
||||||
signals.async_wait([&](auto const&, std::int32_t) {
|
signals.async_wait([&](auto const &, std::int32_t) {
|
||||||
server.stop();
|
server.stop();
|
||||||
io.stop();
|
io.stop();
|
||||||
});
|
});
|
||||||
@@ -177,15 +177,15 @@ auto run_headless(vamp_zero::config const& config, vamp_zero::control_state& sta
|
|||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
auto run_display(vamp_zero::config const& config, vamp_zero::control_state& state, vamp_zero::audio_controller* audio) -> std::expected<void, modulus::error> {
|
auto run_display(vamp_zero::config const &config, vamp_zero::control_state &state, vamp_zero::audio_controller *audio) -> std::expected<void, modulus::error> {
|
||||||
auto io = boost::asio::io_context{};
|
auto io = boost::asio::io_context{};
|
||||||
auto endpoint = make_server_endpoint(config);
|
auto endpoint = make_server_endpoint(config);
|
||||||
if (!endpoint) {
|
if (!endpoint) {
|
||||||
return std::unexpected(endpoint.error());
|
return std::unexpected(endpoint.error());
|
||||||
}
|
}
|
||||||
auto ui = vamp_zero::zero_ui{io.get_executor(), config.display, [&state](vamp_protocol::page_command command) {
|
auto ui = vamp_zero::zero_ui{io.get_executor(), config.display, [&state](vamp_protocol::page_command command) {
|
||||||
static_cast<void>(state.apply(command));
|
static_cast<void>(state.apply(command));
|
||||||
}};
|
}};
|
||||||
state.set_observer([&ui, &config, audio](vamp_zero::app_state app) {
|
state.set_observer([&ui, &config, audio](vamp_zero::app_state app) {
|
||||||
if (audio != nullptr) {
|
if (audio != nullptr) {
|
||||||
audio->sync(app);
|
audio->sync(app);
|
||||||
@@ -202,7 +202,8 @@ auto run_display(vamp_zero::config const& config, vamp_zero::control_state& stat
|
|||||||
if (!published) {
|
if (!published) {
|
||||||
return std::unexpected(published.error());
|
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();
|
auto started = server.start();
|
||||||
if (!started) {
|
if (!started) {
|
||||||
return std::unexpected(started.error());
|
return std::unexpected(started.error());
|
||||||
@@ -216,7 +217,7 @@ auto run_display(vamp_zero::config const& config, vamp_zero::control_state& stat
|
|||||||
auto schedule_connection_refresh = std::function<void()>{};
|
auto schedule_connection_refresh = std::function<void()>{};
|
||||||
schedule_connection_refresh = [&] {
|
schedule_connection_refresh = [&] {
|
||||||
connection_timer.expires_after(std::chrono::milliseconds{500});
|
connection_timer.expires_after(std::chrono::milliseconds{500});
|
||||||
connection_timer.async_wait([&](auto const& error) {
|
connection_timer.async_wait([&](auto const &error) {
|
||||||
if (error) {
|
if (error) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -226,7 +227,7 @@ auto run_display(vamp_zero::config const& config, vamp_zero::control_state& stat
|
|||||||
};
|
};
|
||||||
schedule_connection_refresh();
|
schedule_connection_refresh();
|
||||||
auto signals = boost::asio::signal_set{io, SIGINT, SIGTERM};
|
auto signals = boost::asio::signal_set{io, SIGINT, SIGTERM};
|
||||||
signals.async_wait([&](auto const&, std::int32_t) {
|
signals.async_wait([&](auto const &, std::int32_t) {
|
||||||
connection_timer.cancel();
|
connection_timer.cancel();
|
||||||
server.stop();
|
server.stop();
|
||||||
ui.stop();
|
ui.stop();
|
||||||
@@ -247,12 +248,12 @@ auto run_display(vamp_zero::config const& config, vamp_zero::control_state& stat
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
auto vamp_zero::parse_command_line(std::int32_t argc, std::char_t** argv) -> std::expected<vamp_zero::command_line, modulus::error> {
|
auto vamp_zero::parse_command_line(std::int32_t argc, std::char_t **argv) -> std::expected<vamp_zero::command_line, modulus::error> {
|
||||||
if (argc < 1 || argv == nullptr) {
|
if (argc < 1 || argv == nullptr) {
|
||||||
return std::unexpected(invalid("process arguments are invalid"));
|
return std::unexpected(invalid("process arguments are invalid"));
|
||||||
}
|
}
|
||||||
auto result = vamp_zero::command_line{};
|
auto result = vamp_zero::command_line{};
|
||||||
auto require_value = [&](std::int32_t& index, std::string_view option) -> std::expected<std::string_view, modulus::error> {
|
auto require_value = [&](std::int32_t &index, std::string_view option) -> std::expected<std::string_view, modulus::error> {
|
||||||
++index;
|
++index;
|
||||||
if (index >= argc) {
|
if (index >= argc) {
|
||||||
return std::unexpected(invalid(std::string{option} + " requires a value"));
|
return std::unexpected(invalid(std::string{option} + " requires a value"));
|
||||||
@@ -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}));
|
return std::unexpected(invalid("unknown argument: " + std::string{argument}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
auto command_count = static_cast<std::uint32_t>(result.check_config ? 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.health_check ? 1 : 0) +
|
||||||
+ static_cast<std::uint32_t>(result.print_version ? 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.exit_after_startup ? 1 : 0) +
|
||||||
+ static_cast<std::uint32_t>(result.ensure_database ? 1 : 0);
|
static_cast<std::uint32_t>(result.ensure_database ? 1 : 0);
|
||||||
if (command_count > 1U) {
|
if (command_count > 1U) {
|
||||||
return std::unexpected(invalid("only one command flag may be selected"));
|
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";
|
std::cout << "ok\n";
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
auto ensured = ensure_database(*config);
|
auto ensured = ensure_database(*config);
|
||||||
if (!ensured) {
|
if (!ensured) {
|
||||||
return ensured;
|
return ensured;
|
||||||
|
|||||||
+10
-14
@@ -1,5 +1,3 @@
|
|||||||
#include <vamp_zero/audio.hpp>
|
|
||||||
|
|
||||||
#include <expected>
|
#include <expected>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
@@ -9,29 +7,31 @@
|
|||||||
#include <modulus.hpp>
|
#include <modulus.hpp>
|
||||||
#include <sonus.hpp>
|
#include <sonus.hpp>
|
||||||
|
|
||||||
|
#include <vamp_zero/audio.hpp>
|
||||||
#include <vamp_zero/navigation.hpp>
|
#include <vamp_zero/navigation.hpp>
|
||||||
|
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
auto score_active(vamp_zero::app_state const& state) -> std::bool_t {
|
auto score_active(vamp_zero::app_state const &state) -> std::bool_t {
|
||||||
if (state.mode == vamp_zero::app_mode::PERFORMANCE_SCORE) {
|
if (state.mode == vamp_zero::app_mode::PERFORMANCE_SCORE) {
|
||||||
return vamp_zero::active_part(state) != nullptr;
|
return vamp_zero::active_part(state) != nullptr;
|
||||||
}
|
}
|
||||||
if (state.mode == vamp_zero::app_mode::PRACTICE_SCORE) {
|
if (state.mode == vamp_zero::app_mode::PRACTICE_SCORE) {
|
||||||
auto const* exercise = vamp_zero::active_practice(state);
|
auto const *exercise = vamp_zero::active_practice(state);
|
||||||
return exercise != nullptr && vamp_zero::selected_practice_variant(state, *exercise) != nullptr;
|
return exercise != nullptr && vamp_zero::selected_practice_variant(state, *exercise) != nullptr;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto log_audio_error(std::string const& action, modulus::error const& error) -> void {
|
auto log_audio_error(std::string const &action, modulus::error const &error) -> void {
|
||||||
modulus::log_error(action + ": " + error.describe());
|
modulus::log_error(action + ": " + error.describe());
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
vamp_zero::audio_controller::audio_controller(vamp_zero::audio_config config, sonus::engine engine)
|
vamp_zero::audio_controller::audio_controller(vamp_zero::audio_config config, sonus::engine engine)
|
||||||
: config_{config}, engine_{std::move(engine)} {}
|
: config_{config}, engine_{std::move(engine)} {}
|
||||||
|
|
||||||
vamp_zero::audio_controller::~audio_controller() {
|
vamp_zero::audio_controller::~audio_controller() {
|
||||||
auto lock = std::scoped_lock{mutex_};
|
auto lock = std::scoped_lock{mutex_};
|
||||||
@@ -40,7 +40,7 @@ vamp_zero::audio_controller::~audio_controller() {
|
|||||||
engine_.stop();
|
engine_.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
auto vamp_zero::audio_controller::make_metronome_config(vamp_zero::app_state const& state) const -> sonus::metronome_config {
|
auto vamp_zero::audio_controller::make_metronome_config(vamp_zero::app_state const &state) const -> sonus::metronome_config {
|
||||||
auto result = sonus::metronome_config{};
|
auto result = sonus::metronome_config{};
|
||||||
result.bpm = state.audio.metronome_bpm;
|
result.bpm = state.audio.metronome_bpm;
|
||||||
result.beats_per_bar = config_.beats_per_bar;
|
result.beats_per_bar = config_.beats_per_bar;
|
||||||
@@ -48,7 +48,7 @@ auto vamp_zero::audio_controller::make_metronome_config(vamp_zero::app_state con
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto vamp_zero::audio_controller::sync(vamp_zero::app_state const& state) -> void {
|
auto vamp_zero::audio_controller::sync(vamp_zero::app_state const &state) -> void {
|
||||||
auto lock = std::scoped_lock{mutex_};
|
auto lock = std::scoped_lock{mutex_};
|
||||||
auto const should_run_metronome = state.audio.metronome_enabled && score_active(state);
|
auto const should_run_metronome = state.audio.metronome_enabled && score_active(state);
|
||||||
if (!should_run_metronome) {
|
if (!should_run_metronome) {
|
||||||
@@ -58,7 +58,6 @@ auto vamp_zero::audio_controller::sync(vamp_zero::app_state const& state) -> voi
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto metronome = make_metronome_config(state);
|
auto metronome = make_metronome_config(state);
|
||||||
if (!metronome_running_) {
|
if (!metronome_running_) {
|
||||||
auto started = engine_.start_metronome(metronome);
|
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;
|
last_metronome_bpm_ = metronome.bpm;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (last_metronome_bpm_ != metronome.bpm) {
|
if (last_metronome_bpm_ != metronome.bpm) {
|
||||||
auto updated = engine_.update_metronome(metronome);
|
auto updated = engine_.update_metronome(metronome);
|
||||||
if (!updated) {
|
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) {
|
if (!config.enabled) {
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto sonus_config = sonus::audio_config{};
|
auto sonus_config = sonus::audio_config{};
|
||||||
sonus_config.sample_rate = 48000;
|
sonus_config.sample_rate = 48000;
|
||||||
sonus_config.channels = 2;
|
sonus_config.channels = 2;
|
||||||
sonus_config.master_volume = config.master_volume;
|
sonus_config.master_volume = config.master_volume;
|
||||||
|
|
||||||
auto engine = sonus::engine::create(sonus_config);
|
auto engine = sonus::engine::create(sonus_config);
|
||||||
if (!engine) {
|
if (!engine) {
|
||||||
return std::unexpected(engine.error());
|
return std::unexpected(engine.error());
|
||||||
@@ -103,6 +100,5 @@ auto vamp_zero::create_audio_controller(vamp_zero::audio_config const& config) -
|
|||||||
if (!started) {
|
if (!started) {
|
||||||
return std::unexpected(started.error());
|
return std::unexpected(started.error());
|
||||||
}
|
}
|
||||||
|
|
||||||
return std::make_unique<vamp_zero::audio_controller>(config, std::move(*engine));
|
return std::make_unique<vamp_zero::audio_controller>(config, std::move(*engine));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#include <vamp_zero/color.hpp>
|
#include <vamp_zero/color.hpp>
|
||||||
|
|
||||||
|
|
||||||
auto vamp_zero::to_luma(vamp_style::color value) -> luma::color {
|
auto vamp_zero::to_luma(vamp_style::color value) -> luma::color {
|
||||||
return {
|
return {
|
||||||
.r = value.r,
|
.r = value.r,
|
||||||
|
|||||||
+83
-33
@@ -1,10 +1,6 @@
|
|||||||
#include <vamp_zero/config.hpp>
|
|
||||||
|
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <expected>
|
#include <expected>
|
||||||
#include <fstream>
|
|
||||||
#include <iterator>
|
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <string_view>
|
#include <string_view>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
@@ -14,8 +10,10 @@
|
|||||||
#include <boost/json/value.hpp>
|
#include <boost/json/value.hpp>
|
||||||
#include <boost/system/error_code.hpp>
|
#include <boost/system/error_code.hpp>
|
||||||
|
|
||||||
|
#include <vamp_zero/config.hpp>
|
||||||
#include <vamp_zero/errors.hpp>
|
#include <vamp_zero/errors.hpp>
|
||||||
|
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
constexpr auto json_options = modulus::json_config::options{
|
constexpr auto json_options = modulus::json_config::options{
|
||||||
@@ -28,14 +26,14 @@ auto invalid(std::string message) -> modulus::error {
|
|||||||
return vamp_zero::make_error(vamp_zero::error_code::INVALID_ARGUMENT, std::move(message));
|
return vamp_zero::make_error(vamp_zero::error_code::INVALID_ARGUMENT, std::move(message));
|
||||||
}
|
}
|
||||||
|
|
||||||
auto require_object(boost::json::value const& value, std::string_view name) -> std::expected<boost::json::object const*, modulus::error> {
|
auto require_object(boost::json::value const &value, std::string_view name) -> std::expected<boost::json::object const *, modulus::error> {
|
||||||
if (!value.is_object()) {
|
if (!value.is_object()) {
|
||||||
return std::unexpected(invalid(std::string{name} + " must be an object"));
|
return std::unexpected(invalid(std::string{name} + " must be an object"));
|
||||||
}
|
}
|
||||||
return &value.as_object();
|
return &value.as_object();
|
||||||
}
|
}
|
||||||
|
|
||||||
auto optional_object(boost::json::object const& object, std::string_view field) -> std::expected<boost::json::object const*, modulus::error> {
|
auto optional_object(boost::json::object const &object, std::string_view field) -> std::expected<boost::json::object const *, modulus::error> {
|
||||||
auto found = object.find(field);
|
auto found = object.find(field);
|
||||||
if (found == object.end()) {
|
if (found == object.end()) {
|
||||||
return nullptr;
|
return nullptr;
|
||||||
@@ -43,7 +41,7 @@ auto optional_object(boost::json::object const& object, std::string_view field)
|
|||||||
return require_object(found->value(), field);
|
return require_object(found->value(), field);
|
||||||
}
|
}
|
||||||
|
|
||||||
auto optional_string(boost::json::object const& object, std::string_view field, std::string fallback) -> std::expected<std::string, modulus::error> {
|
auto optional_string(boost::json::object const &object, std::string_view field, std::string fallback) -> std::expected<std::string, modulus::error> {
|
||||||
auto found = object.find(field);
|
auto found = object.find(field);
|
||||||
if (found == object.end()) {
|
if (found == object.end()) {
|
||||||
return fallback;
|
return fallback;
|
||||||
@@ -58,7 +56,7 @@ auto optional_string(boost::json::object const& object, std::string_view field,
|
|||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto optional_bool(boost::json::object const& object, std::string_view field, std::bool_t fallback) -> std::expected<std::bool_t, modulus::error> {
|
auto optional_bool(boost::json::object const &object, std::string_view field, std::bool_t fallback) -> std::expected<std::bool_t, modulus::error> {
|
||||||
auto found = object.find(field);
|
auto found = object.find(field);
|
||||||
if (found == object.end()) {
|
if (found == object.end()) {
|
||||||
return fallback;
|
return fallback;
|
||||||
@@ -69,7 +67,7 @@ auto optional_bool(boost::json::object const& object, std::string_view field, st
|
|||||||
return found->value().as_bool();
|
return found->value().as_bool();
|
||||||
}
|
}
|
||||||
|
|
||||||
auto optional_uint32(boost::json::object const& object, std::string_view field, std::uint32_t fallback, std::uint32_t min_value, std::uint32_t max_value) -> std::expected<std::uint32_t, modulus::error> {
|
auto optional_uint32(boost::json::object const &object, std::string_view field, std::uint32_t fallback, std::uint32_t min_value, std::uint32_t max_value) -> std::expected<std::uint32_t, modulus::error> {
|
||||||
auto found = object.find(field);
|
auto found = object.find(field);
|
||||||
if (found == object.end()) {
|
if (found == object.end()) {
|
||||||
return fallback;
|
return fallback;
|
||||||
@@ -92,7 +90,7 @@ auto optional_uint32(boost::json::object const& object, std::string_view field,
|
|||||||
return static_cast<std::uint32_t>(value);
|
return static_cast<std::uint32_t>(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
auto optional_float(boost::json::object const& object, std::string_view field, std::float32_t fallback, std::float32_t min_value, std::float32_t max_value) -> std::expected<std::float32_t, modulus::error> {
|
auto optional_float(boost::json::object const &object, std::string_view field, std::float32_t fallback, std::float32_t min_value, std::float32_t max_value) -> std::expected<std::float32_t, modulus::error> {
|
||||||
auto found = object.find(field);
|
auto found = object.find(field);
|
||||||
if (found == object.end()) {
|
if (found == object.end()) {
|
||||||
return fallback;
|
return fallback;
|
||||||
@@ -113,7 +111,7 @@ auto optional_float(boost::json::object const& object, std::string_view field, s
|
|||||||
return static_cast<std::float32_t>(value);
|
return static_cast<std::float32_t>(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
auto parse_hardware(std::string const& value) -> std::expected<vamp_zero::hardware_mode, modulus::error> {
|
auto parse_hardware(std::string const &value) -> std::expected<vamp_zero::hardware_mode, modulus::error> {
|
||||||
if (value == "fake") {
|
if (value == "fake") {
|
||||||
return vamp_zero::hardware_mode::FAKE;
|
return vamp_zero::hardware_mode::FAKE;
|
||||||
}
|
}
|
||||||
@@ -123,7 +121,7 @@ auto parse_hardware(std::string const& value) -> std::expected<vamp_zero::hardwa
|
|||||||
return std::unexpected(invalid("runtime.hardware must be fake or real"));
|
return std::unexpected(invalid("runtime.hardware must be fake or real"));
|
||||||
}
|
}
|
||||||
|
|
||||||
auto parse_runtime(boost::json::object const& root, vamp_zero::runtime_config fallback) -> std::expected<vamp_zero::runtime_config, modulus::error> {
|
auto parse_runtime(boost::json::object const &root, vamp_zero::runtime_config fallback) -> std::expected<vamp_zero::runtime_config, modulus::error> {
|
||||||
auto object = optional_object(root, "runtime");
|
auto object = optional_object(root, "runtime");
|
||||||
if (!object) {
|
if (!object) {
|
||||||
return std::unexpected(object.error());
|
return std::unexpected(object.error());
|
||||||
@@ -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 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 assets_path = optional_string(**object, "assets_path", fallback.assets_path);
|
||||||
auto heartbeat_path = optional_string(**object, "heartbeat_path", fallback.heartbeat_path);
|
auto heartbeat_path = optional_string(**object, "heartbeat_path", fallback.heartbeat_path);
|
||||||
if (!hardware_text || !headless || !exit_after_startup || !assets_path || !heartbeat_path) {
|
if (!hardware_text) {
|
||||||
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()))));
|
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);
|
auto hardware = parse_hardware(*hardware_text);
|
||||||
if (!hardware) {
|
if (!hardware) {
|
||||||
@@ -152,7 +162,7 @@ auto parse_runtime(boost::json::object const& root, vamp_zero::runtime_config fa
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
auto parse_database(boost::json::object const& root, vamp_zero::database_config fallback) -> std::expected<vamp_zero::database_config, modulus::error> {
|
auto parse_database(boost::json::object const &root, vamp_zero::database_config fallback) -> std::expected<vamp_zero::database_config, modulus::error> {
|
||||||
auto object = optional_object(root, "database");
|
auto object = optional_object(root, "database");
|
||||||
if (!object) {
|
if (!object) {
|
||||||
return std::unexpected(object.error());
|
return std::unexpected(object.error());
|
||||||
@@ -169,7 +179,7 @@ auto parse_database(boost::json::object const& root, vamp_zero::database_config
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
auto parse_control(boost::json::object const& root, vamp_zero::control_config fallback) -> std::expected<vamp_zero::control_config, modulus::error> {
|
auto parse_control(boost::json::object const &root, vamp_zero::control_config fallback) -> std::expected<vamp_zero::control_config, modulus::error> {
|
||||||
auto object = optional_object(root, "control");
|
auto object = optional_object(root, "control");
|
||||||
if (!object) {
|
if (!object) {
|
||||||
return std::unexpected(object.error());
|
return std::unexpected(object.error());
|
||||||
@@ -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 ap_interface = optional_string(**object, "ap_interface", result.ap_interface);
|
||||||
auto zero_ip = optional_string(**object, "zero_ip", result.zero_ip);
|
auto zero_ip = optional_string(**object, "zero_ip", result.zero_ip);
|
||||||
auto zero_cidr = optional_string(**object, "zero_cidr", result.zero_cidr);
|
auto zero_cidr = optional_string(**object, "zero_cidr", result.zero_cidr);
|
||||||
if (!ap_ssid || !ap_passphrase || !ap_interface || !zero_ip || !zero_cidr) {
|
if (!ap_ssid) {
|
||||||
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()))));
|
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) {
|
if (ap_passphrase->size() < 8U) {
|
||||||
return std::unexpected(invalid("control.ap_passphrase must be at least 8 characters"));
|
return std::unexpected(invalid("control.ap_passphrase must be at least 8 characters"));
|
||||||
@@ -207,7 +229,7 @@ auto parse_control(boost::json::object const& root, vamp_zero::control_config fa
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto parse_audio(boost::json::object const& root, vamp_zero::audio_config fallback) -> std::expected<vamp_zero::audio_config, modulus::error> {
|
auto parse_audio(boost::json::object const &root, vamp_zero::audio_config fallback) -> std::expected<vamp_zero::audio_config, modulus::error> {
|
||||||
auto object = optional_object(root, "audio");
|
auto object = optional_object(root, "audio");
|
||||||
if (!object) {
|
if (!object) {
|
||||||
return std::unexpected(object.error());
|
return std::unexpected(object.error());
|
||||||
@@ -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 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 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);
|
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) {
|
if (!enabled) {
|
||||||
return std::unexpected(!enabled ? enabled.error()
|
return std::unexpected(enabled.error());
|
||||||
: (!track_enabled ? track_enabled.error()
|
}
|
||||||
: (!metronome_enabled ? metronome_enabled.error()
|
if (!track_enabled) {
|
||||||
: (!metronome_bpm ? metronome_bpm.error()
|
return std::unexpected(track_enabled.error());
|
||||||
: (!metronome_min_bpm ? metronome_min_bpm.error()
|
}
|
||||||
: (!metronome_max_bpm ? metronome_max_bpm.error()
|
if (!metronome_enabled) {
|
||||||
: (!metronome_step ? metronome_step.error()
|
return std::unexpected(metronome_enabled.error());
|
||||||
: (!beats_per_bar ? beats_per_bar.error()
|
}
|
||||||
: (!master_volume ? master_volume.error() : metronome_volume.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) {
|
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"));
|
return std::unexpected(invalid("audio.metronome_min_bpm must be less than or equal to audio.metronome_max_bpm"));
|
||||||
@@ -258,7 +299,7 @@ auto parse_audio(boost::json::object const& root, vamp_zero::audio_config fallba
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
auto vamp_zero::parse_config(boost::json::value const& value) -> std::expected<vamp_zero::config, modulus::error> {
|
auto vamp_zero::parse_config(boost::json::value const &value) -> std::expected<vamp_zero::config, modulus::error> {
|
||||||
auto root = require_object(value, "Product Vamp config root");
|
auto root = require_object(value, "Product Vamp config root");
|
||||||
if (!root) {
|
if (!root) {
|
||||||
return std::unexpected(root.error());
|
return std::unexpected(root.error());
|
||||||
@@ -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 database = parse_database(**root, result.database);
|
||||||
auto control = parse_control(**root, result.control);
|
auto control = parse_control(**root, result.control);
|
||||||
auto audio = parse_audio(**root, result.audio);
|
auto audio = parse_audio(**root, result.audio);
|
||||||
if (!runtime || !database || !control || !audio) {
|
if (!runtime) {
|
||||||
return std::unexpected(!runtime ? runtime.error() : (!database ? database.error() : (!control ? control.error() : audio.error())));
|
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.runtime = std::move(*runtime);
|
||||||
result.database = std::move(*database);
|
result.database = std::move(*database);
|
||||||
@@ -292,7 +342,7 @@ auto vamp_zero::parse_config(boost::json::value const& value) -> std::expected<v
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto vamp_zero::load_config(std::string const& path) -> std::expected<vamp_zero::config, modulus::error> {
|
auto vamp_zero::load_config(std::string const &path) -> std::expected<vamp_zero::config, modulus::error> {
|
||||||
auto value = modulus::json_config::load_value(path, json_options);
|
auto value = modulus::json_config::load_value(path, json_options);
|
||||||
if (!value) {
|
if (!value) {
|
||||||
return std::unexpected(value.error());
|
return std::unexpected(value.error());
|
||||||
|
|||||||
+48
-42
@@ -1,6 +1,3 @@
|
|||||||
#include <vamp_zero/database.hpp>
|
|
||||||
|
|
||||||
#include <algorithm>
|
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <expected>
|
#include <expected>
|
||||||
@@ -16,16 +13,19 @@
|
|||||||
|
|
||||||
#include <noto.hpp>
|
#include <noto.hpp>
|
||||||
|
|
||||||
|
#include <vamp_zero/database.hpp>
|
||||||
#include <vamp_zero/errors.hpp>
|
#include <vamp_zero/errors.hpp>
|
||||||
#include <vamp_zero/library.hpp>
|
#include <vamp_zero/library.hpp>
|
||||||
#include <vamp_zero/score.hpp>
|
#include <vamp_zero/score.hpp>
|
||||||
|
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
constexpr auto application_id = std::int64_t{0x56414D50};
|
constexpr auto application_id = std::int64_t{0x56414D50};
|
||||||
|
|
||||||
auto connection_config(vamp_zero::database_config const& config) -> noto::connection_config {
|
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.busy_timeout = std::chrono::seconds{5};
|
||||||
result.foreign_keys = true;
|
result.foreign_keys = true;
|
||||||
result.pragmas = {
|
result.pragmas = {
|
||||||
@@ -35,12 +35,12 @@ auto connection_config(vamp_zero::database_config const& config) -> noto::connec
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto text_value(noto::sqlite_value const& value) -> std::string {
|
auto text_value(noto::sqlite_value const &value) -> std::string {
|
||||||
auto text = std::get_if<std::string>(&value.value);
|
auto text = std::get_if<std::string>(&value.value);
|
||||||
return text == nullptr ? std::string{} : *text;
|
return text == nullptr ? std::string{} : *text;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto execute_schema_statement(noto::connection& connection, std::string statement) -> boost::asio::awaitable<std::expected<void, modulus::error>> {
|
auto execute_schema_statement(noto::connection &connection, std::string statement) -> boost::asio::awaitable<std::expected<void, modulus::error>> {
|
||||||
auto result = co_await connection.async_execute(std::move(statement));
|
auto result = co_await connection.async_execute(std::move(statement));
|
||||||
if (!result) {
|
if (!result) {
|
||||||
co_return std::unexpected(result.error());
|
co_return std::unexpected(result.error());
|
||||||
@@ -48,12 +48,12 @@ auto execute_schema_statement(noto::connection& connection, std::string statemen
|
|||||||
co_return std::expected<void, modulus::error>{};
|
co_return std::expected<void, modulus::error>{};
|
||||||
}
|
}
|
||||||
|
|
||||||
auto column_exists(noto::connection& connection, std::string table, std::string column) -> boost::asio::awaitable<std::expected<std::bool_t, modulus::error>> {
|
auto column_exists(noto::connection &connection, std::string table, std::string column) -> boost::asio::awaitable<std::expected<std::bool_t, modulus::error>> {
|
||||||
auto selected = co_await connection.async_execute("PRAGMA table_info(" + table + ")");
|
auto selected = co_await connection.async_execute("PRAGMA table_info(" + table + ")");
|
||||||
if (!selected) {
|
if (!selected) {
|
||||||
co_return std::unexpected(selected.error());
|
co_return std::unexpected(selected.error());
|
||||||
}
|
}
|
||||||
for (auto const& row : selected->rows) {
|
for (auto const &row : selected->rows) {
|
||||||
if (row.size() >= 2U && text_value(row[1]) == column) {
|
if (row.size() >= 2U && text_value(row[1]) == column) {
|
||||||
co_return true;
|
co_return true;
|
||||||
}
|
}
|
||||||
@@ -61,7 +61,7 @@ auto column_exists(noto::connection& connection, std::string table, std::string
|
|||||||
co_return false;
|
co_return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto ensure_column(noto::connection& connection, std::string table, std::string column, std::string alter_statement) -> boost::asio::awaitable<std::expected<void, modulus::error>> {
|
auto ensure_column(noto::connection &connection, std::string table, std::string column, std::string alter_statement) -> boost::asio::awaitable<std::expected<void, modulus::error>> {
|
||||||
auto exists = co_await column_exists(connection, std::move(table), std::move(column));
|
auto exists = co_await column_exists(connection, std::move(table), std::move(column));
|
||||||
if (!exists) {
|
if (!exists) {
|
||||||
co_return std::unexpected(exists.error());
|
co_return std::unexpected(exists.error());
|
||||||
@@ -72,12 +72,11 @@ auto ensure_column(noto::connection& connection, std::string table, std::string
|
|||||||
co_return co_await execute_schema_statement(connection, std::move(alter_statement));
|
co_return co_await execute_schema_statement(connection, std::move(alter_statement));
|
||||||
}
|
}
|
||||||
|
|
||||||
auto ensure_current_schema(noto::connection& connection) -> boost::asio::awaitable<std::expected<void, modulus::error>> {
|
auto ensure_current_schema(noto::connection &connection) -> boost::asio::awaitable<std::expected<void, modulus::error>> {
|
||||||
auto begin = co_await connection.async_execute("BEGIN IMMEDIATE");
|
auto begin = co_await connection.async_execute("BEGIN IMMEDIATE");
|
||||||
if (!begin) {
|
if (!begin) {
|
||||||
co_return std::unexpected(begin.error());
|
co_return std::unexpected(begin.error());
|
||||||
}
|
}
|
||||||
|
|
||||||
auto ensured = std::expected<void, modulus::error>{};
|
auto ensured = std::expected<void, modulus::error>{};
|
||||||
auto schema_statements = std::vector<std::string>{
|
auto schema_statements = std::vector<std::string>{
|
||||||
R"sql(
|
R"sql(
|
||||||
@@ -121,16 +120,15 @@ CREATE TABLE IF NOT EXISTS practice_exercises (
|
|||||||
)sql",
|
)sql",
|
||||||
"DROP TABLE IF EXISTS _noto_schema_migrations",
|
"DROP TABLE IF EXISTS _noto_schema_migrations",
|
||||||
};
|
};
|
||||||
|
for (auto &statement : schema_statements) {
|
||||||
for (auto& statement : schema_statements) {
|
|
||||||
if (!ensured) {
|
if (!ensured) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
ensured = co_await execute_schema_statement(connection, std::move(statement));
|
ensured = co_await execute_schema_statement(connection, std::move(statement));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ensured) {
|
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) {
|
if (ensured) {
|
||||||
ensured = co_await ensure_column(connection, "songs", "source_path", "ALTER TABLE songs ADD COLUMN source_path TEXT NOT NULL DEFAULT ''");
|
ensured = co_await ensure_column(connection, "songs", "source_path", "ALTER TABLE songs ADD COLUMN source_path TEXT NOT NULL DEFAULT ''");
|
||||||
@@ -142,39 +140,43 @@ 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))");
|
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) {
|
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) {
|
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) {
|
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) {
|
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) {
|
if (ensured) {
|
||||||
ensured = co_await ensure_column(connection, "practice_exercises", "source_path", "ALTER TABLE practice_exercises ADD COLUMN source_path TEXT NOT NULL DEFAULT ''");
|
ensured = co_await ensure_column(connection, "practice_exercises", "source_path", "ALTER TABLE practice_exercises ADD COLUMN source_path TEXT NOT NULL DEFAULT ''");
|
||||||
}
|
}
|
||||||
if (ensured) {
|
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>{
|
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 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_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_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_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)",
|
"CREATE INDEX IF NOT EXISTS idx_practice_exercises_catalog_managed ON practice_exercises(catalog_managed)",
|
||||||
};
|
};
|
||||||
for (auto& statement : index_statements) {
|
for (auto &statement : index_statements) {
|
||||||
if (!ensured) {
|
if (!ensured) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
ensured = co_await execute_schema_statement(connection, std::move(statement));
|
ensured = co_await execute_schema_statement(connection, std::move(statement));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ensured) {
|
if (ensured) {
|
||||||
auto committed = co_await connection.async_execute("COMMIT");
|
auto committed = co_await connection.async_execute("COMMIT");
|
||||||
if (!committed) {
|
if (!committed) {
|
||||||
@@ -183,7 +185,6 @@ CREATE TABLE IF NOT EXISTS practice_exercises (
|
|||||||
} else {
|
} else {
|
||||||
static_cast<void>(co_await connection.async_execute("ROLLBACK"));
|
static_cast<void>(co_await connection.async_execute("ROLLBACK"));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ensured) {
|
if (!ensured) {
|
||||||
co_return std::unexpected(ensured.error());
|
co_return std::unexpected(ensured.error());
|
||||||
}
|
}
|
||||||
@@ -194,15 +195,15 @@ CREATE TABLE IF NOT EXISTS practice_exercises (
|
|||||||
co_return std::expected<void, modulus::error>{};
|
co_return std::expected<void, modulus::error>{};
|
||||||
}
|
}
|
||||||
|
|
||||||
auto path_parts(std::filesystem::path const& path) -> std::vector<std::string> {
|
auto path_parts(std::filesystem::path const &path) -> std::vector<std::string> {
|
||||||
auto parts = std::vector<std::string>{};
|
auto parts = std::vector<std::string>{};
|
||||||
for (auto const& part : path) {
|
for (auto const &part : path) {
|
||||||
parts.push_back(part.string());
|
parts.push_back(part.string());
|
||||||
}
|
}
|
||||||
return parts;
|
return parts;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto logical_practice_exercise_id(std::string const& type_id, std::string const& lesson_id, std::string const& source_path, std::string const& fallback) -> std::string {
|
auto logical_practice_exercise_id(std::string const &type_id, std::string const &lesson_id, std::string const &source_path, std::string const &fallback) -> std::string {
|
||||||
auto parts = path_parts(std::filesystem::path{source_path});
|
auto parts = path_parts(std::filesystem::path{source_path});
|
||||||
for (auto index = std::size_t{}; index + 3U < parts.size(); ++index) {
|
for (auto index = std::size_t{}; index + 3U < parts.size(); ++index) {
|
||||||
if (parts[index] == "practice" && parts[index + 1U] == type_id && parts[index + 2U] == lesson_id) {
|
if (parts[index] == "practice" && parts[index + 1U] == type_id && parts[index + 2U] == lesson_id) {
|
||||||
@@ -217,10 +218,11 @@ auto logical_practice_exercise_id(std::string const& type_id, std::string const&
|
|||||||
}
|
}
|
||||||
|
|
||||||
auto now_ms() -> std::int64_t {
|
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>> {
|
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>> {
|
||||||
auto sql = std::string{"DELETE FROM "};
|
auto sql = std::string{"DELETE FROM "};
|
||||||
sql += table;
|
sql += table;
|
||||||
sql += " WHERE catalog_managed = 1";
|
sql += " WHERE catalog_managed = 1";
|
||||||
@@ -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 connection = noto::connection{executor, connection_config(config)};
|
||||||
auto connected = co_await connection.async_connect();
|
auto connected = co_await connection.async_connect();
|
||||||
if (!connected) {
|
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());
|
co_return std::unexpected(set.error());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
auto ensured = co_await ensure_current_schema(connection);
|
auto ensured = co_await ensure_current_schema(connection);
|
||||||
if (!ensured) {
|
if (!ensured) {
|
||||||
auto close = co_await connection.async_close();
|
auto close = co_await connection.async_close();
|
||||||
@@ -324,7 +324,7 @@ auto vamp_zero::async_import_catalogs(boost::asio::any_io_executor executor, vam
|
|||||||
auto timestamp = now_ms();
|
auto timestamp = now_ms();
|
||||||
auto song_ids = std::vector<std::string>{};
|
auto song_ids = std::vector<std::string>{};
|
||||||
song_ids.reserve(performance->parts.size());
|
song_ids.reserve(performance->parts.size());
|
||||||
for (auto const& part : performance->parts) {
|
for (auto const &part : performance->parts) {
|
||||||
if (!imported) {
|
if (!imported) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -370,11 +370,11 @@ WHERE songs.catalog_managed = 1
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
auto practice_ids = std::vector<std::string>{};
|
auto practice_ids = std::vector<std::string>{};
|
||||||
for (auto const& exercise : practice->exercises) {
|
for (auto const &exercise : practice->exercises) {
|
||||||
if (!imported) {
|
if (!imported) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
for (auto const& variant : exercise.variants) {
|
for (auto const &variant : exercise.variants) {
|
||||||
practice_ids.push_back(variant.id);
|
practice_ids.push_back(variant.id);
|
||||||
auto params = std::vector<noto::sqlite_value>{
|
auto params = std::vector<noto::sqlite_value>{
|
||||||
noto::sqlite_value{variant.id},
|
noto::sqlite_value{variant.id},
|
||||||
@@ -457,13 +457,16 @@ WHERE practice_exercises.catalog_managed = 1
|
|||||||
co_return co_await connection.async_close();
|
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 connection = noto::connection{executor, connection_config(config)};
|
||||||
auto connected = co_await connection.async_connect();
|
auto connected = co_await connection.async_connect();
|
||||||
if (!connected) {
|
if (!connected) {
|
||||||
co_return std::unexpected(connected.error());
|
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) {
|
if (!selected) {
|
||||||
auto close = co_await connection.async_close();
|
auto close = co_await connection.async_close();
|
||||||
if (!close) {
|
if (!close) {
|
||||||
@@ -472,7 +475,7 @@ auto vamp_zero::async_load_performance_library(boost::asio::any_io_executor exec
|
|||||||
co_return std::unexpected(selected.error());
|
co_return std::unexpected(selected.error());
|
||||||
}
|
}
|
||||||
auto parts = std::vector<vamp_zero::library_part>{};
|
auto parts = std::vector<vamp_zero::library_part>{};
|
||||||
for (auto const& row : selected->rows) {
|
for (auto const &row : selected->rows) {
|
||||||
if (row.size() < 7U) {
|
if (row.size() < 7U) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -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));
|
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 connection = noto::connection{executor, connection_config(config)};
|
||||||
auto connected = co_await connection.async_connect();
|
auto connected = co_await connection.async_connect();
|
||||||
if (!connected) {
|
if (!connected) {
|
||||||
co_return std::unexpected(connected.error());
|
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) {
|
if (!selected) {
|
||||||
auto close = co_await connection.async_close();
|
auto close = co_await connection.async_close();
|
||||||
if (!close) {
|
if (!close) {
|
||||||
@@ -519,7 +525,7 @@ auto vamp_zero::async_load_practice_library(boost::asio::any_io_executor executo
|
|||||||
co_return std::unexpected(selected.error());
|
co_return std::unexpected(selected.error());
|
||||||
}
|
}
|
||||||
auto exercises = std::vector<vamp_zero::practice_exercise>{};
|
auto exercises = std::vector<vamp_zero::practice_exercise>{};
|
||||||
for (auto const& row : selected->rows) {
|
for (auto const &row : selected->rows) {
|
||||||
if (row.size() < 10U) {
|
if (row.size() < 10U) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
|
#include <utility>
|
||||||
|
|
||||||
#include <vamp_zero/errors.hpp>
|
#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 {
|
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);
|
return modulus::make_error("product-vamp", static_cast<std::int32_t>(code), std::move(message), location);
|
||||||
|
|||||||
+61
-64
@@ -1,27 +1,20 @@
|
|||||||
#include <vamp_zero/library.hpp>
|
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <expected>
|
#include <expected>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <iterator>
|
#include <iterator>
|
||||||
#include <limits>
|
|
||||||
#include <sstream>
|
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <string_view>
|
#include <string_view>
|
||||||
|
#include <system_error>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
#include <vector>
|
#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/errors.hpp>
|
||||||
#include <vamp_zero/score.hpp>
|
#include <vamp_zero/score.hpp>
|
||||||
|
#include <vamp_zero/library.hpp>
|
||||||
#include <vamp_zero/notation.hpp>
|
#include <vamp_zero/notation.hpp>
|
||||||
|
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
auto invalid(std::string message) -> modulus::error {
|
auto invalid(std::string message) -> modulus::error {
|
||||||
@@ -33,7 +26,7 @@ auto human_label(std::string value) -> std::string {
|
|||||||
return "Score";
|
return "Score";
|
||||||
}
|
}
|
||||||
auto capitalize_next = std::bool_t{true};
|
auto capitalize_next = std::bool_t{true};
|
||||||
for (auto& ch : value) {
|
for (auto &ch : value) {
|
||||||
if (ch == '-' || ch == '_') {
|
if (ch == '-' || ch == '_') {
|
||||||
ch = ' ';
|
ch = ' ';
|
||||||
capitalize_next = true;
|
capitalize_next = true;
|
||||||
@@ -47,33 +40,40 @@ auto human_label(std::string value) -> std::string {
|
|||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto practice_type_label(std::string const& value) -> std::string {
|
auto practice_type_label(std::string const &value) -> std::string {
|
||||||
return human_label(value) + " Practice";
|
return human_label(value) + " Practice";
|
||||||
}
|
}
|
||||||
|
|
||||||
auto load_score_documents(std::filesystem::path const& directory, std::bool_t required) -> std::expected<std::vector<vamp_zero::score_document>, modulus::error> {
|
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{};
|
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) {
|
if (required) {
|
||||||
return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::NOT_FOUND, "score asset directory not found: " + directory.string()));
|
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>{};
|
return std::vector<vamp_zero::score_document>{};
|
||||||
}
|
}
|
||||||
|
|
||||||
auto paths = std::vector<std::filesystem::path>{};
|
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};
|
||||||
if (error) {
|
auto end = std::filesystem::directory_iterator{};
|
||||||
return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::UNAVAILABLE, "failed to scan score assets: " + error.message()));
|
while (!error && entries != end) {
|
||||||
|
auto const &entry = *entries;
|
||||||
|
if (entry.is_regular_file(error) && entry.path().extension() == ".json") {
|
||||||
|
paths.push_back(entry.path());
|
||||||
}
|
}
|
||||||
if (!entry.is_regular_file() || entry.path().extension() != ".json") {
|
if (!error) {
|
||||||
continue;
|
entries.increment(error);
|
||||||
}
|
}
|
||||||
paths.push_back(entry.path());
|
}
|
||||||
|
if (error) {
|
||||||
|
return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::UNAVAILABLE, "failed to scan score assets: " + error.message()));
|
||||||
}
|
}
|
||||||
std::sort(paths.begin(), paths.end());
|
std::sort(paths.begin(), paths.end());
|
||||||
|
|
||||||
auto documents = std::vector<vamp_zero::score_document>{};
|
auto documents = std::vector<vamp_zero::score_document>{};
|
||||||
for (auto const& path : paths) {
|
for (auto const &path : paths) {
|
||||||
auto input = std::ifstream{path, std::ios::binary};
|
auto input = std::ifstream{path, std::ios::binary};
|
||||||
if (!input) {
|
if (!input) {
|
||||||
return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::NOT_FOUND, "failed to open score JSON: " + path.string()));
|
return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::NOT_FOUND, "failed to open score JSON: " + path.string()));
|
||||||
@@ -88,15 +88,15 @@ auto load_score_documents(std::filesystem::path const& directory, std::bool_t re
|
|||||||
return documents;
|
return documents;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto path_parts(std::filesystem::path const& path) -> std::vector<std::string> {
|
auto path_parts(std::filesystem::path const &path) -> std::vector<std::string> {
|
||||||
auto parts = std::vector<std::string>{};
|
auto parts = std::vector<std::string>{};
|
||||||
for (auto const& part : path) {
|
for (auto const &part : path) {
|
||||||
parts.push_back(part.string());
|
parts.push_back(part.string());
|
||||||
}
|
}
|
||||||
return parts;
|
return parts;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto key_rank(std::string const& key) -> std::size_t {
|
auto key_rank(std::string const &key) -> std::size_t {
|
||||||
static auto const keys = std::vector<std::string>{
|
static auto const keys = std::vector<std::string>{
|
||||||
"C",
|
"C",
|
||||||
"Db",
|
"Db",
|
||||||
@@ -116,8 +116,8 @@ auto key_rank(std::string const& key) -> std::size_t {
|
|||||||
return found == keys.end() ? keys.size() : static_cast<std::size_t>(std::distance(keys.begin(), found));
|
return found == keys.end() ? keys.size() : static_cast<std::size_t>(std::distance(keys.begin(), found));
|
||||||
}
|
}
|
||||||
|
|
||||||
auto sync_primary_variant(vamp_zero::practice_exercise& exercise) -> void {
|
auto sync_primary_variant(vamp_zero::practice_exercise &exercise) -> void {
|
||||||
std::sort(exercise.variants.begin(), exercise.variants.end(), [](auto const& left, auto const& right) {
|
std::sort(exercise.variants.begin(), exercise.variants.end(), [](auto const &left, auto const &right) {
|
||||||
auto left_rank = key_rank(left.key_signature);
|
auto left_rank = key_rank(left.key_signature);
|
||||||
auto right_rank = key_rank(right.key_signature);
|
auto right_rank = key_rank(right.key_signature);
|
||||||
if (left_rank != right_rank) {
|
if (left_rank != right_rank) {
|
||||||
@@ -128,7 +128,7 @@ auto sync_primary_variant(vamp_zero::practice_exercise& exercise) -> void {
|
|||||||
if (exercise.variants.empty()) {
|
if (exercise.variants.empty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
auto const& primary = exercise.variants.front();
|
auto const &primary = exercise.variants.front();
|
||||||
exercise.key_signature = primary.key_signature;
|
exercise.key_signature = primary.key_signature;
|
||||||
exercise.source_path = primary.source_path;
|
exercise.source_path = primary.source_path;
|
||||||
exercise.source_json = primary.source_json;
|
exercise.source_json = primary.source_json;
|
||||||
@@ -136,10 +136,9 @@ auto sync_primary_variant(vamp_zero::practice_exercise& exercise) -> void {
|
|||||||
exercise.total_pages = primary.total_pages;
|
exercise.total_pages = primary.total_pages;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto same_practice_exercise(vamp_zero::practice_exercise const& left, vamp_zero::practice_exercise const& right) -> std::bool_t {
|
auto same_practice_exercise(vamp_zero::practice_exercise const &left, vamp_zero::practice_exercise const &right) -> std::bool_t {
|
||||||
return left.id == right.id
|
return left.id == right.id && left.practice_type_id == right.practice_type_id &&
|
||||||
&& left.practice_type_id == right.practice_type_id
|
left.lesson_group_id == right.lesson_group_id;
|
||||||
&& left.lesson_group_id == right.lesson_group_id;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -149,15 +148,14 @@ auto vamp_zero::parse_score_document(std::string_view json, std::string source_p
|
|||||||
}
|
}
|
||||||
|
|
||||||
auto vamp_zero::group_library_parts(std::vector<vamp_zero::library_part> parts) -> vamp_zero::library_state {
|
auto vamp_zero::group_library_parts(std::vector<vamp_zero::library_part> parts) -> vamp_zero::library_state {
|
||||||
std::sort(parts.begin(), parts.end(), [](auto const& left, auto const& right) {
|
std::sort(parts.begin(), parts.end(), [](auto const &left, auto const &right) {
|
||||||
if (left.title != right.title) {
|
if (left.title != right.title) {
|
||||||
return left.title < right.title;
|
return left.title < right.title;
|
||||||
}
|
}
|
||||||
return left.part < right.part;
|
return left.part < right.part;
|
||||||
});
|
});
|
||||||
|
|
||||||
auto groups = std::vector<vamp_zero::song_group>{};
|
auto groups = std::vector<vamp_zero::song_group>{};
|
||||||
for (auto const& part : parts) {
|
for (auto const &part : parts) {
|
||||||
if (groups.empty() || groups.back().title != part.title || groups.back().composer != part.composer) {
|
if (groups.empty() || groups.back().title != part.title || groups.back().composer != part.composer) {
|
||||||
groups.push_back({
|
groups.push_back({
|
||||||
.title = part.title,
|
.title = part.title,
|
||||||
@@ -173,7 +171,7 @@ auto vamp_zero::group_library_parts(std::vector<vamp_zero::library_part> parts)
|
|||||||
}
|
}
|
||||||
|
|
||||||
auto vamp_zero::group_practice_exercises(std::vector<vamp_zero::practice_exercise> exercises) -> vamp_zero::practice_library {
|
auto vamp_zero::group_practice_exercises(std::vector<vamp_zero::practice_exercise> exercises) -> vamp_zero::practice_library {
|
||||||
std::sort(exercises.begin(), exercises.end(), [](auto const& left, auto const& right) {
|
std::sort(exercises.begin(), exercises.end(), [](auto const &left, auto const &right) {
|
||||||
if (left.practice_type_id != right.practice_type_id) {
|
if (left.practice_type_id != right.practice_type_id) {
|
||||||
return left.practice_type_id < right.practice_type_id;
|
return left.practice_type_id < right.practice_type_id;
|
||||||
}
|
}
|
||||||
@@ -188,27 +186,20 @@ auto vamp_zero::group_practice_exercises(std::vector<vamp_zero::practice_exercis
|
|||||||
}
|
}
|
||||||
return left.source_path < right.source_path;
|
return left.source_path < right.source_path;
|
||||||
});
|
});
|
||||||
|
|
||||||
auto merged = std::vector<vamp_zero::practice_exercise>{};
|
auto merged = std::vector<vamp_zero::practice_exercise>{};
|
||||||
for (auto& exercise : exercises) {
|
for (auto &exercise : exercises) {
|
||||||
if (!merged.empty() && same_practice_exercise(merged.back(), exercise)) {
|
if (!merged.empty() && same_practice_exercise(merged.back(), exercise)) {
|
||||||
merged.back().variants.insert(
|
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.end(),
|
|
||||||
std::make_move_iterator(exercise.variants.begin()),
|
|
||||||
std::make_move_iterator(exercise.variants.end())
|
|
||||||
);
|
|
||||||
sync_primary_variant(merged.back());
|
sync_primary_variant(merged.back());
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
sync_primary_variant(exercise);
|
sync_primary_variant(exercise);
|
||||||
merged.push_back(std::move(exercise));
|
merged.push_back(std::move(exercise));
|
||||||
}
|
}
|
||||||
|
|
||||||
auto types = std::vector<vamp_zero::practice_type_group>{};
|
auto types = std::vector<vamp_zero::practice_type_group>{};
|
||||||
for (auto const& exercise : merged) {
|
for (auto const &exercise : merged) {
|
||||||
auto type = std::find_if(types.begin(), types.end(), [&](auto const& item) {
|
auto type =
|
||||||
return item.id == exercise.practice_type_id;
|
std::find_if(types.begin(), types.end(), [&](auto const &item) { return item.id == exercise.practice_type_id; });
|
||||||
});
|
|
||||||
if (type == types.end()) {
|
if (type == types.end()) {
|
||||||
types.push_back({
|
types.push_back({
|
||||||
.id = exercise.practice_type_id,
|
.id = exercise.practice_type_id,
|
||||||
@@ -216,7 +207,7 @@ auto vamp_zero::group_practice_exercises(std::vector<vamp_zero::practice_exercis
|
|||||||
});
|
});
|
||||||
type = std::prev(types.end());
|
type = std::prev(types.end());
|
||||||
}
|
}
|
||||||
auto lesson = std::find_if(type->lesson_groups.begin(), type->lesson_groups.end(), [&](auto const& item) {
|
auto lesson = std::find_if(type->lesson_groups.begin(), type->lesson_groups.end(), [&](auto const &item) {
|
||||||
return item.id == exercise.lesson_group_id;
|
return item.id == exercise.lesson_group_id;
|
||||||
});
|
});
|
||||||
if (lesson == type->lesson_groups.end()) {
|
if (lesson == type->lesson_groups.end()) {
|
||||||
@@ -234,13 +225,13 @@ auto vamp_zero::group_practice_exercises(std::vector<vamp_zero::practice_exercis
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
auto vamp_zero::load_performance_library(std::string const& assets_path) -> std::expected<vamp_zero::library_state, modulus::error> {
|
auto vamp_zero::load_performance_library(std::string const &assets_path) -> std::expected<vamp_zero::library_state, modulus::error> {
|
||||||
auto documents = load_score_documents(std::filesystem::path{assets_path} / "songs", true);
|
auto documents = load_score_documents(std::filesystem::path{assets_path} / "songs", true);
|
||||||
if (!documents) {
|
if (!documents) {
|
||||||
return std::unexpected(documents.error());
|
return std::unexpected(documents.error());
|
||||||
}
|
}
|
||||||
auto parts = std::vector<vamp_zero::library_part>{};
|
auto parts = std::vector<vamp_zero::library_part>{};
|
||||||
for (auto& document : *documents) {
|
for (auto &document : *documents) {
|
||||||
parts.push_back({
|
parts.push_back({
|
||||||
.id = document.id,
|
.id = document.id,
|
||||||
.title = document.title,
|
.title = document.title,
|
||||||
@@ -256,27 +247,34 @@ auto vamp_zero::load_performance_library(std::string const& assets_path) -> std:
|
|||||||
return vamp_zero::group_library_parts(std::move(parts));
|
return vamp_zero::group_library_parts(std::move(parts));
|
||||||
}
|
}
|
||||||
|
|
||||||
auto vamp_zero::load_practice_library(std::string const& assets_path) -> std::expected<vamp_zero::practice_library, modulus::error> {
|
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 root = std::filesystem::path{assets_path} / "practice";
|
||||||
auto error = std::error_code{};
|
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{};
|
return vamp_zero::practice_library{};
|
||||||
}
|
}
|
||||||
|
|
||||||
auto paths = std::vector<std::filesystem::path>{};
|
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};
|
||||||
if (error) {
|
auto end = std::filesystem::recursive_directory_iterator{};
|
||||||
return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::UNAVAILABLE, "failed to scan practice assets: " + error.message()));
|
while (!error && entries != end) {
|
||||||
|
auto const &entry = *entries;
|
||||||
|
if (entry.is_regular_file(error) && entry.path().extension() == ".json") {
|
||||||
|
paths.push_back(entry.path());
|
||||||
}
|
}
|
||||||
if (!entry.is_regular_file() || entry.path().extension() != ".json") {
|
if (!error) {
|
||||||
continue;
|
entries.increment(error);
|
||||||
}
|
}
|
||||||
paths.push_back(entry.path());
|
}
|
||||||
|
if (error) {
|
||||||
|
return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::UNAVAILABLE, "failed to scan practice assets: " + error.message()));
|
||||||
}
|
}
|
||||||
std::sort(paths.begin(), paths.end());
|
std::sort(paths.begin(), paths.end());
|
||||||
|
|
||||||
auto exercises = std::vector<vamp_zero::practice_exercise>{};
|
auto exercises = std::vector<vamp_zero::practice_exercise>{};
|
||||||
for (auto const& path : paths) {
|
for (auto const &path : paths) {
|
||||||
auto relative = std::filesystem::relative(path, root, error);
|
auto relative = std::filesystem::relative(path, root, error);
|
||||||
if (error) {
|
if (error) {
|
||||||
return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::UNAVAILABLE, "failed to inspect practice asset path: " + error.message()));
|
return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::UNAVAILABLE, "failed to inspect practice asset path: " + error.message()));
|
||||||
@@ -285,7 +283,6 @@ auto vamp_zero::load_practice_library(std::string const& assets_path) -> std::ex
|
|||||||
if (parts.size() != 4U) {
|
if (parts.size() != 4U) {
|
||||||
return std::unexpected(invalid("practice JSON must be stored as practice/<type>/<lesson_group>/<exercise>/<key>.json: " + path.string()));
|
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};
|
auto input = std::ifstream{path, std::ios::binary};
|
||||||
if (!input) {
|
if (!input) {
|
||||||
return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::NOT_FOUND, "failed to open practice JSON: " + path.string()));
|
return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::NOT_FOUND, "failed to open practice JSON: " + path.string()));
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
#include <vamp_zero/app.hpp>
|
|
||||||
|
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <exception>
|
#include <exception>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
||||||
auto main(std::int32_t argc, std::char_t** argv) -> std::int32_t {
|
#include <vamp_zero/app.hpp>
|
||||||
|
|
||||||
|
|
||||||
|
auto main(std::int32_t argc, std::char_t **argv) -> std::int32_t {
|
||||||
try {
|
try {
|
||||||
auto command = vamp_zero::parse_command_line(argc, argv);
|
auto command = vamp_zero::parse_command_line(argc, argv);
|
||||||
if (!command) {
|
if (!command) {
|
||||||
@@ -18,7 +19,7 @@ auto main(std::int32_t argc, std::char_t** argv) -> std::int32_t {
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
} catch (std::exception const& error) {
|
} catch (std::exception const &error) {
|
||||||
std::cerr << "vamp-zero: unhandled exception: " << error.what() << '\n';
|
std::cerr << "vamp-zero: unhandled exception: " << error.what() << '\n';
|
||||||
return 1;
|
return 1;
|
||||||
} catch (...) {
|
} catch (...) {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
|
#include <algorithm>
|
||||||
|
|
||||||
#include <vamp_zero/navigation.hpp>
|
#include <vamp_zero/navigation.hpp>
|
||||||
|
|
||||||
#include <algorithm>
|
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
@@ -10,7 +11,7 @@ auto menu_row_count(std::size_t item_count) -> std::size_t {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
auto vamp_zero::selected_group(vamp_zero::app_state const& state) -> vamp_zero::song_group const* {
|
auto vamp_zero::selected_group(vamp_zero::app_state const &state) -> vamp_zero::song_group const * {
|
||||||
if (state.selected_song_index == 0U || state.performance.groups.empty()) {
|
if (state.selected_song_index == 0U || state.performance.groups.empty()) {
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
@@ -21,8 +22,8 @@ auto vamp_zero::selected_group(vamp_zero::app_state const& state) -> vamp_zero::
|
|||||||
return &state.performance.groups[index];
|
return &state.performance.groups[index];
|
||||||
}
|
}
|
||||||
|
|
||||||
auto vamp_zero::selected_part(vamp_zero::app_state const& state) -> vamp_zero::library_part const* {
|
auto vamp_zero::selected_part(vamp_zero::app_state const &state) -> vamp_zero::library_part const * {
|
||||||
auto const* group = vamp_zero::selected_group(state);
|
auto const *group = vamp_zero::selected_group(state);
|
||||||
if (state.selected_part_index == 0U || group == nullptr || group->parts.empty()) {
|
if (state.selected_part_index == 0U || group == nullptr || group->parts.empty()) {
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
@@ -33,17 +34,17 @@ auto vamp_zero::selected_part(vamp_zero::app_state const& state) -> vamp_zero::l
|
|||||||
return &group->parts[index];
|
return &group->parts[index];
|
||||||
}
|
}
|
||||||
|
|
||||||
auto vamp_zero::active_part(vamp_zero::app_state const& state) -> vamp_zero::library_part const* {
|
auto vamp_zero::active_part(vamp_zero::app_state const &state) -> vamp_zero::library_part const * {
|
||||||
if (state.active_part_id.empty()) {
|
if (state.active_part_id.empty()) {
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
auto found = std::find_if(state.performance.parts.begin(), state.performance.parts.end(), [&](auto const& part) {
|
auto found = std::find_if(state.performance.parts.begin(), state.performance.parts.end(), [&](auto const &part) {
|
||||||
return part.id == state.active_part_id;
|
return part.id == state.active_part_id;
|
||||||
});
|
});
|
||||||
return found == state.performance.parts.end() ? nullptr : &*found;
|
return found == state.performance.parts.end() ? nullptr : &*found;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto vamp_zero::selected_practice_type(vamp_zero::app_state const& state) -> vamp_zero::practice_type_group const* {
|
auto vamp_zero::selected_practice_type(vamp_zero::app_state const &state) -> vamp_zero::practice_type_group const * {
|
||||||
if (state.selected_practice_type_index == 0U || state.practice.types.empty()) {
|
if (state.selected_practice_type_index == 0U || state.practice.types.empty()) {
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
@@ -54,8 +55,8 @@ auto vamp_zero::selected_practice_type(vamp_zero::app_state const& state) -> vam
|
|||||||
return &state.practice.types[index];
|
return &state.practice.types[index];
|
||||||
}
|
}
|
||||||
|
|
||||||
auto vamp_zero::selected_practice_lesson(vamp_zero::app_state const& state) -> vamp_zero::practice_lesson_group const* {
|
auto vamp_zero::selected_practice_lesson(vamp_zero::app_state const &state) -> vamp_zero::practice_lesson_group const * {
|
||||||
auto const* type = vamp_zero::selected_practice_type(state);
|
auto const *type = vamp_zero::selected_practice_type(state);
|
||||||
if (state.selected_practice_lesson_index == 0U || type == nullptr || type->lesson_groups.empty()) {
|
if (state.selected_practice_lesson_index == 0U || type == nullptr || type->lesson_groups.empty()) {
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
@@ -66,8 +67,8 @@ auto vamp_zero::selected_practice_lesson(vamp_zero::app_state const& state) -> v
|
|||||||
return &type->lesson_groups[index];
|
return &type->lesson_groups[index];
|
||||||
}
|
}
|
||||||
|
|
||||||
auto vamp_zero::selected_practice_exercise(vamp_zero::app_state const& state) -> vamp_zero::practice_exercise const* {
|
auto vamp_zero::selected_practice_exercise(vamp_zero::app_state const &state) -> vamp_zero::practice_exercise const * {
|
||||||
auto const* lesson = vamp_zero::selected_practice_lesson(state);
|
auto const *lesson = vamp_zero::selected_practice_lesson(state);
|
||||||
if (state.selected_practice_exercise_index == 0U || lesson == nullptr || lesson->exercises.empty()) {
|
if (state.selected_practice_exercise_index == 0U || lesson == nullptr || lesson->exercises.empty()) {
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
@@ -78,21 +79,21 @@ auto vamp_zero::selected_practice_exercise(vamp_zero::app_state const& state) ->
|
|||||||
return &lesson->exercises[index];
|
return &lesson->exercises[index];
|
||||||
}
|
}
|
||||||
|
|
||||||
auto vamp_zero::active_practice(vamp_zero::app_state const& state) -> vamp_zero::practice_exercise const* {
|
auto vamp_zero::active_practice(vamp_zero::app_state const &state) -> vamp_zero::practice_exercise const * {
|
||||||
if (state.active_practice_id.empty()) {
|
if (state.active_practice_id.empty()) {
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
auto found = std::find_if(state.practice.exercises.begin(), state.practice.exercises.end(), [&](auto const& exercise) {
|
auto found = std::find_if(state.practice.exercises.begin(),
|
||||||
return exercise.id == state.active_practice_id;
|
state.practice.exercises.end(),
|
||||||
});
|
[&](auto const &exercise) { return exercise.id == state.active_practice_id; });
|
||||||
return found == state.practice.exercises.end() ? nullptr : &*found;
|
return found == state.practice.exercises.end() ? nullptr : &*found;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto vamp_zero::supports_key_selection(vamp_zero::practice_exercise const& exercise) -> std::bool_t {
|
auto vamp_zero::supports_key_selection(vamp_zero::practice_exercise const &exercise) -> std::bool_t {
|
||||||
return vamp_zero::practice_type_supports_key_selection(exercise.practice_type_id);
|
return vamp_zero::practice_type_supports_key_selection(exercise.practice_type_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
auto vamp_zero::selected_practice_variant(vamp_zero::app_state const& state, vamp_zero::practice_exercise const& exercise) -> vamp_zero::practice_exercise::score_variant const* {
|
auto vamp_zero::selected_practice_variant(vamp_zero::app_state const &state, vamp_zero::practice_exercise const &exercise) -> vamp_zero::practice_exercise::score_variant const * {
|
||||||
if (exercise.variants.empty()) {
|
if (exercise.variants.empty()) {
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
@@ -109,43 +110,44 @@ auto vamp_zero::selected_practice_variant(vamp_zero::app_state const& state, vam
|
|||||||
return &exercise.variants[index];
|
return &exercise.variants[index];
|
||||||
}
|
}
|
||||||
|
|
||||||
auto vamp_zero::selected_practice_key(vamp_zero::app_state const& state, vamp_zero::practice_exercise const& exercise) -> std::optional<std::string_view> {
|
auto vamp_zero::selected_practice_key(vamp_zero::app_state const &state, vamp_zero::practice_exercise const &exercise) -> std::optional<std::string_view> {
|
||||||
auto const* variant = vamp_zero::selected_practice_variant(state, exercise);
|
auto const *variant = vamp_zero::selected_practice_variant(state, exercise);
|
||||||
if (variant == nullptr) {
|
if (variant == nullptr) {
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
return variant->key_signature;
|
return variant->key_signature;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto vamp_zero::practice_key_row_count(vamp_zero::app_state const& state) -> std::size_t {
|
auto vamp_zero::practice_key_row_count(vamp_zero::app_state const &state) -> std::size_t {
|
||||||
auto const* exercise = vamp_zero::selected_practice_exercise(state);
|
auto const *exercise = vamp_zero::selected_practice_exercise(state);
|
||||||
if (exercise == nullptr || !vamp_zero::supports_key_selection(*exercise)) {
|
if (exercise == nullptr || !vamp_zero::supports_key_selection(*exercise)) {
|
||||||
return 1U;
|
return 1U;
|
||||||
}
|
}
|
||||||
return menu_row_count(exercise->variants.size());
|
return menu_row_count(exercise->variants.size());
|
||||||
}
|
}
|
||||||
|
|
||||||
auto vamp_zero::effective_practice_key(vamp_zero::app_state const& state, vamp_zero::practice_exercise const& exercise) -> std::string {
|
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 variant->key_signature;
|
||||||
}
|
}
|
||||||
return exercise.key_signature.empty() ? std::string{"C"} : exercise.key_signature;
|
return exercise.key_signature.empty() ? std::string{"C"} : exercise.key_signature;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto vamp_zero::active_pages(vamp_zero::app_state const& state) -> std::size_t {
|
auto vamp_zero::active_pages(vamp_zero::app_state const &state) -> std::size_t {
|
||||||
if (state.mode == vamp_zero::app_mode::PERFORMANCE_SCORE) {
|
if (state.mode == vamp_zero::app_mode::PERFORMANCE_SCORE) {
|
||||||
auto const* part = vamp_zero::active_part(state);
|
auto const *part = vamp_zero::active_part(state);
|
||||||
return part == nullptr ? 0U : part->total_pages;
|
return part == nullptr ? 0U : part->total_pages;
|
||||||
}
|
}
|
||||||
if (state.mode == vamp_zero::app_mode::PRACTICE_SCORE) {
|
if (state.mode == vamp_zero::app_mode::PRACTICE_SCORE) {
|
||||||
auto const* exercise = vamp_zero::active_practice(state);
|
auto const *exercise = vamp_zero::active_practice(state);
|
||||||
auto const* variant = exercise == nullptr ? nullptr : vamp_zero::selected_practice_variant(state, *exercise);
|
auto const *variant = exercise == nullptr ? nullptr : vamp_zero::selected_practice_variant(state, *exercise);
|
||||||
return variant == nullptr ? 0U : variant->total_pages;
|
return variant == nullptr ? 0U : variant->total_pages;
|
||||||
}
|
}
|
||||||
return 0U;
|
return 0U;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto vamp_zero::practice_fallback_menu(vamp_zero::app_state const& state) -> vamp_zero::app_mode {
|
auto vamp_zero::practice_fallback_menu(vamp_zero::app_state const &state) -> vamp_zero::app_mode {
|
||||||
if (vamp_zero::selected_practice_lesson(state) != nullptr) {
|
if (vamp_zero::selected_practice_lesson(state) != nullptr) {
|
||||||
return vamp_zero::app_mode::PRACTICE_EXERCISE_MENU;
|
return vamp_zero::app_mode::PRACTICE_EXERCISE_MENU;
|
||||||
}
|
}
|
||||||
|
|||||||
+878
-285
File diff suppressed because it is too large
Load Diff
+101
-99
@@ -16,6 +16,7 @@
|
|||||||
#include <vamp_zero/errors.hpp>
|
#include <vamp_zero/errors.hpp>
|
||||||
#include <vamp_zero/navigation.hpp>
|
#include <vamp_zero/navigation.hpp>
|
||||||
|
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
constexpr auto main_menu_performance = std::size_t{0};
|
constexpr auto main_menu_performance = std::size_t{0};
|
||||||
@@ -51,41 +52,43 @@ auto clamp_audio_state(vamp_zero::audio_state audio) -> vamp_zero::audio_state {
|
|||||||
return audio;
|
return audio;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto increase_bpm(vamp_zero::audio_state& audio) -> void {
|
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_max_bpm
|
audio.metronome_bpm > audio.metronome_max_bpm - std::min(audio.metronome_step, audio.metronome_max_bpm)
|
||||||
: audio.metronome_bpm + audio.metronome_step;
|
? audio.metronome_max_bpm
|
||||||
|
: audio.metronome_bpm + audio.metronome_step;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto decrease_bpm(vamp_zero::audio_state& audio) -> void {
|
auto decrease_bpm(vamp_zero::audio_state &audio) -> void {
|
||||||
audio.metronome_bpm = audio.metronome_bpm < audio.metronome_min_bpm + audio.metronome_step
|
audio.metronome_bpm = audio.metronome_bpm < audio.metronome_min_bpm + audio.metronome_step
|
||||||
? audio.metronome_min_bpm
|
? audio.metronome_min_bpm
|
||||||
: audio.metronome_bpm - audio.metronome_step;
|
: audio.metronome_bpm - audio.metronome_step;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto clamp_state(vamp_zero::app_state state) -> vamp_zero::app_state {
|
auto clamp_state(vamp_zero::app_state state) -> vamp_zero::app_state {
|
||||||
state.audio = clamp_audio_state(state.audio);
|
state.audio = clamp_audio_state(state.audio);
|
||||||
state.selected_main_menu_index = std::min(state.selected_main_menu_index, main_menu_count - 1U);
|
state.selected_main_menu_index = std::min(state.selected_main_menu_index, main_menu_count - 1U);
|
||||||
state.selected_song_index = std::min(state.selected_song_index, menu_row_count(state.performance.groups.size()) - 1U);
|
state.selected_song_index = std::min(state.selected_song_index, menu_row_count(state.performance.groups.size()) - 1U);
|
||||||
if (auto const* group = selected_group(state); group != nullptr) {
|
if (auto const *group = selected_group(state); group != nullptr) {
|
||||||
state.selected_part_index = std::min(state.selected_part_index, menu_row_count(group->parts.size()) - 1U);
|
state.selected_part_index = std::min(state.selected_part_index, menu_row_count(group->parts.size()) - 1U);
|
||||||
} else {
|
} else {
|
||||||
state.selected_part_index = 0U;
|
state.selected_part_index = 0U;
|
||||||
}
|
}
|
||||||
|
state.selected_practice_type_index =
|
||||||
state.selected_practice_type_index = std::min(state.selected_practice_type_index, menu_row_count(state.practice.types.size()) - 1U);
|
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) {
|
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 {
|
} else {
|
||||||
state.selected_practice_lesson_index = 0U;
|
state.selected_practice_lesson_index = 0U;
|
||||||
}
|
}
|
||||||
if (auto const* lesson = selected_practice_lesson(state); lesson != nullptr) {
|
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 {
|
} else {
|
||||||
state.selected_practice_exercise_index = 0U;
|
state.selected_practice_exercise_index = 0U;
|
||||||
}
|
}
|
||||||
state.selected_practice_key_index = std::min(state.selected_practice_key_index, practice_key_row_count(state) - 1U);
|
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) {
|
if (state.mode == vamp_zero::app_mode::PERFORMANCE_PART_MENU && selected_group(state) == nullptr) {
|
||||||
state.mode = vamp_zero::app_mode::PERFORMANCE_SONG_MENU;
|
state.mode = vamp_zero::app_mode::PERFORMANCE_SONG_MENU;
|
||||||
}
|
}
|
||||||
@@ -93,10 +96,11 @@ auto clamp_state(vamp_zero::app_state state) -> vamp_zero::app_state {
|
|||||||
state.mode = vamp_zero::app_mode::PRACTICE_TYPE_MENU;
|
state.mode = vamp_zero::app_mode::PRACTICE_TYPE_MENU;
|
||||||
}
|
}
|
||||||
if (state.mode == vamp_zero::app_mode::PRACTICE_EXERCISE_MENU && selected_practice_lesson(state) == nullptr) {
|
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) {
|
if (state.mode == vamp_zero::app_mode::PRACTICE_KEY_MENU) {
|
||||||
auto const* exercise = selected_practice_exercise(state);
|
auto const *exercise = selected_practice_exercise(state);
|
||||||
if (exercise == nullptr) {
|
if (exercise == nullptr) {
|
||||||
state.mode = practice_fallback_menu(state);
|
state.mode = practice_fallback_menu(state);
|
||||||
} else if (!supports_key_selection(*exercise) || exercise->variants.empty()) {
|
} else if (!supports_key_selection(*exercise) || exercise->variants.empty()) {
|
||||||
@@ -104,13 +108,13 @@ auto clamp_state(vamp_zero::app_state state) -> vamp_zero::app_state {
|
|||||||
state.selected_practice_key_index = 0U;
|
state.selected_practice_key_index = 0U;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.mode == vamp_zero::app_mode::PERFORMANCE_SCORE && active_part(state) == nullptr) {
|
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.active_part_id.clear();
|
||||||
state.page_index = 0;
|
state.page_index = 0;
|
||||||
} else if (state.mode == vamp_zero::app_mode::PRACTICE_SCORE) {
|
} else if (state.mode == vamp_zero::app_mode::PRACTICE_SCORE) {
|
||||||
auto const* exercise = active_practice(state);
|
auto const *exercise = active_practice(state);
|
||||||
if (exercise == nullptr) {
|
if (exercise == nullptr) {
|
||||||
state.mode = practice_fallback_menu(state);
|
state.mode = practice_fallback_menu(state);
|
||||||
state.active_practice_id.clear();
|
state.active_practice_id.clear();
|
||||||
@@ -121,20 +125,19 @@ auto clamp_state(vamp_zero::app_state state) -> vamp_zero::app_state {
|
|||||||
state.page_index = 0;
|
state.page_index = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.mode != vamp_zero::app_mode::MAIN_MENU) {
|
if (state.mode != vamp_zero::app_mode::MAIN_MENU) {
|
||||||
state.audio.tempo_editing = false;
|
state.audio.tempo_editing = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (auto pages = active_pages(state); pages != 0U) {
|
if (auto pages = active_pages(state); pages != 0U) {
|
||||||
state.page_index = std::min(state.page_index, pages - 1U);
|
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;
|
state.page_index = 0;
|
||||||
}
|
}
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto command_json(vamp_zero::app_state const& state) -> boost::json::object {
|
auto command_json(vamp_zero::app_state const &state) -> boost::json::object {
|
||||||
auto commands = boost::json::object{};
|
auto commands = boost::json::object{};
|
||||||
commands["last"] = state.last_command;
|
commands["last"] = state.last_command;
|
||||||
commands["next_count"] = state.next_count;
|
commands["next_count"] = state.next_count;
|
||||||
@@ -144,7 +147,7 @@ auto command_json(vamp_zero::app_state const& state) -> boost::json::object {
|
|||||||
return commands;
|
return commands;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto audio_json(vamp_zero::app_state const& state) -> boost::json::object {
|
auto audio_json(vamp_zero::app_state const &state) -> boost::json::object {
|
||||||
auto audio = boost::json::object{};
|
auto audio = boost::json::object{};
|
||||||
audio["track_enabled"] = state.audio.track_enabled;
|
audio["track_enabled"] = state.audio.track_enabled;
|
||||||
audio["metronome_enabled"] = state.audio.metronome_enabled;
|
audio["metronome_enabled"] = state.audio.metronome_enabled;
|
||||||
@@ -156,7 +159,7 @@ auto audio_json(vamp_zero::app_state const& state) -> boost::json::object {
|
|||||||
return audio;
|
return audio;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto part_json(vamp_zero::library_part const& part) -> boost::json::object {
|
auto part_json(vamp_zero::library_part const &part) -> boost::json::object {
|
||||||
auto result = boost::json::object{};
|
auto result = boost::json::object{};
|
||||||
result["id"] = part.id;
|
result["id"] = part.id;
|
||||||
result["title"] = part.title;
|
result["title"] = part.title;
|
||||||
@@ -169,9 +172,9 @@ auto part_json(vamp_zero::library_part const& part) -> boost::json::object {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto exercise_json(vamp_zero::practice_exercise const& exercise) -> boost::json::object {
|
auto exercise_json(vamp_zero::practice_exercise const &exercise) -> boost::json::object {
|
||||||
auto variants = boost::json::array{};
|
auto variants = boost::json::array{};
|
||||||
for (auto const& variant : exercise.variants) {
|
for (auto const &variant : exercise.variants) {
|
||||||
auto item = boost::json::object{};
|
auto item = boost::json::object{};
|
||||||
item["id"] = variant.id;
|
item["id"] = variant.id;
|
||||||
item["key_signature"] = variant.key_signature;
|
item["key_signature"] = variant.key_signature;
|
||||||
@@ -198,9 +201,9 @@ auto exercise_json(vamp_zero::practice_exercise const& exercise) -> boost::json:
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto lesson_json(vamp_zero::practice_lesson_group const& lesson) -> boost::json::object {
|
auto lesson_json(vamp_zero::practice_lesson_group const &lesson) -> boost::json::object {
|
||||||
auto exercises = boost::json::array{};
|
auto exercises = boost::json::array{};
|
||||||
for (auto const& exercise : lesson.exercises) {
|
for (auto const &exercise : lesson.exercises) {
|
||||||
exercises.push_back(exercise_json(exercise));
|
exercises.push_back(exercise_json(exercise));
|
||||||
}
|
}
|
||||||
auto result = boost::json::object{};
|
auto result = boost::json::object{};
|
||||||
@@ -211,17 +214,17 @@ auto lesson_json(vamp_zero::practice_lesson_group const& lesson) -> boost::json:
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto practice_type_exercise_count(vamp_zero::practice_type_group const& type) -> std::size_t {
|
auto practice_type_exercise_count(vamp_zero::practice_type_group const &type) -> std::size_t {
|
||||||
auto count = std::size_t{};
|
auto count = std::size_t{};
|
||||||
for (auto const& lesson : type.lesson_groups) {
|
for (auto const &lesson : type.lesson_groups) {
|
||||||
count += lesson.exercises.size();
|
count += lesson.exercises.size();
|
||||||
}
|
}
|
||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto type_json(vamp_zero::practice_type_group const& type) -> boost::json::object {
|
auto type_json(vamp_zero::practice_type_group const &type) -> boost::json::object {
|
||||||
auto lessons = boost::json::array{};
|
auto lessons = boost::json::array{};
|
||||||
for (auto const& lesson : type.lesson_groups) {
|
for (auto const &lesson : type.lesson_groups) {
|
||||||
lessons.push_back(lesson_json(lesson));
|
lessons.push_back(lesson_json(lesson));
|
||||||
}
|
}
|
||||||
auto result = boost::json::object{};
|
auto result = boost::json::object{};
|
||||||
@@ -233,11 +236,11 @@ auto type_json(vamp_zero::practice_type_group const& type) -> boost::json::objec
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto performance_json(vamp_zero::app_state const& state) -> boost::json::object {
|
auto performance_json(vamp_zero::app_state const &state) -> boost::json::object {
|
||||||
auto groups = boost::json::array{};
|
auto groups = boost::json::array{};
|
||||||
for (auto const& group : state.performance.groups) {
|
for (auto const &group : state.performance.groups) {
|
||||||
auto parts = boost::json::array{};
|
auto parts = boost::json::array{};
|
||||||
for (auto const& part : group.parts) {
|
for (auto const &part : group.parts) {
|
||||||
parts.push_back(part_json(part));
|
parts.push_back(part_json(part));
|
||||||
}
|
}
|
||||||
auto item = boost::json::object{};
|
auto item = boost::json::object{};
|
||||||
@@ -246,7 +249,6 @@ auto performance_json(vamp_zero::app_state const& state) -> boost::json::object
|
|||||||
item["parts"] = std::move(parts);
|
item["parts"] = std::move(parts);
|
||||||
groups.push_back(std::move(item));
|
groups.push_back(std::move(item));
|
||||||
}
|
}
|
||||||
|
|
||||||
auto library = boost::json::object{};
|
auto library = boost::json::object{};
|
||||||
library["song_count"] = static_cast<std::uint64_t>(state.performance.groups.size());
|
library["song_count"] = static_cast<std::uint64_t>(state.performance.groups.size());
|
||||||
library["part_count"] = static_cast<std::uint64_t>(state.performance.parts.size());
|
library["part_count"] = static_cast<std::uint64_t>(state.performance.parts.size());
|
||||||
@@ -254,32 +256,31 @@ auto performance_json(vamp_zero::app_state const& state) -> boost::json::object
|
|||||||
return library;
|
return library;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto practice_json(vamp_zero::app_state const& state) -> boost::json::object {
|
auto practice_json(vamp_zero::app_state const &state) -> boost::json::object {
|
||||||
auto types = boost::json::array{};
|
auto types = boost::json::array{};
|
||||||
for (auto const& type : state.practice.types) {
|
for (auto const &type : state.practice.types) {
|
||||||
types.push_back(type_json(type));
|
types.push_back(type_json(type));
|
||||||
}
|
}
|
||||||
auto exercises = boost::json::array{};
|
auto exercises = boost::json::array{};
|
||||||
for (auto const& exercise : state.practice.exercises) {
|
for (auto const &exercise : state.practice.exercises) {
|
||||||
exercises.push_back(exercise_json(exercise));
|
exercises.push_back(exercise_json(exercise));
|
||||||
}
|
}
|
||||||
auto keys = boost::json::array{};
|
auto keys = boost::json::array{};
|
||||||
for (auto key : vamp_zero::practice_key_names()) {
|
for (auto key : vamp_zero::practice_key_names()) {
|
||||||
keys.push_back(boost::json::value{std::string{key}});
|
keys.push_back(boost::json::value{std::string{key}});
|
||||||
}
|
}
|
||||||
|
|
||||||
auto selection = boost::json::object{};
|
auto selection = boost::json::object{};
|
||||||
selection["type_row_index"] = static_cast<std::uint64_t>(state.selected_practice_type_index);
|
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);
|
selection["lesson_row_index"] = static_cast<std::uint64_t>(state.selected_practice_lesson_index);
|
||||||
selection["exercise_row_index"] = static_cast<std::uint64_t>(state.selected_practice_exercise_index);
|
selection["exercise_row_index"] = static_cast<std::uint64_t>(state.selected_practice_exercise_index);
|
||||||
selection["key_row_index"] = static_cast<std::uint64_t>(state.selected_practice_key_index);
|
selection["key_row_index"] = static_cast<std::uint64_t>(state.selected_practice_key_index);
|
||||||
if (auto const* type = selected_practice_type(state); type != nullptr) {
|
if (auto const *type = selected_practice_type(state); type != nullptr) {
|
||||||
selection["type"] = type_json(*type);
|
selection["type"] = type_json(*type);
|
||||||
}
|
}
|
||||||
if (auto const* lesson = selected_practice_lesson(state); lesson != nullptr) {
|
if (auto const *lesson = selected_practice_lesson(state); lesson != nullptr) {
|
||||||
selection["lesson_group"] = lesson_json(*lesson);
|
selection["lesson_group"] = lesson_json(*lesson);
|
||||||
}
|
}
|
||||||
if (auto const* exercise = selected_practice_exercise(state); exercise != nullptr) {
|
if (auto const *exercise = selected_practice_exercise(state); exercise != nullptr) {
|
||||||
selection["exercise"] = exercise_json(*exercise);
|
selection["exercise"] = exercise_json(*exercise);
|
||||||
auto variant = selected_practice_variant(state, *exercise);
|
auto variant = selected_practice_variant(state, *exercise);
|
||||||
if (variant != nullptr) {
|
if (variant != nullptr) {
|
||||||
@@ -292,12 +293,11 @@ auto practice_json(vamp_zero::app_state const& state) -> boost::json::object {
|
|||||||
selection["variant"] = std::move(item);
|
selection["variant"] = std::move(item);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (auto const* exercise = selected_practice_exercise(state); exercise != nullptr) {
|
if (auto const *exercise = selected_practice_exercise(state); exercise != nullptr) {
|
||||||
if (auto key = selected_practice_key(state, *exercise)) {
|
if (auto key = selected_practice_key(state, *exercise)) {
|
||||||
selection["key"] = std::string{*key};
|
selection["key"] = std::string{*key};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
auto practice = boost::json::object{};
|
auto practice = boost::json::object{};
|
||||||
practice["type_count"] = static_cast<std::uint64_t>(state.practice.types.size());
|
practice["type_count"] = static_cast<std::uint64_t>(state.practice.types.size());
|
||||||
practice["exercise_count"] = static_cast<std::uint64_t>(state.practice.exercises.size());
|
practice["exercise_count"] = static_cast<std::uint64_t>(state.practice.exercises.size());
|
||||||
@@ -310,7 +310,7 @@ auto practice_json(vamp_zero::app_state const& state) -> boost::json::object {
|
|||||||
}
|
}
|
||||||
|
|
||||||
auto sanitize_line_value(std::string value) -> std::string {
|
auto sanitize_line_value(std::string value) -> std::string {
|
||||||
for (auto& ch : value) {
|
for (auto &ch : value) {
|
||||||
if (ch == '\r' || ch == '\n') {
|
if (ch == '\r' || ch == '\n') {
|
||||||
ch = ' ';
|
ch = ' ';
|
||||||
}
|
}
|
||||||
@@ -330,7 +330,6 @@ auto vamp_zero::apply_app_command(vamp_zero::app_state state, vamp_protocol::pag
|
|||||||
++state.select_count;
|
++state.select_count;
|
||||||
}
|
}
|
||||||
state.last_command = vamp_protocol::page_command_name(command);
|
state.last_command = vamp_protocol::page_command_name(command);
|
||||||
|
|
||||||
switch (state.mode) {
|
switch (state.mode) {
|
||||||
case vamp_zero::app_mode::MAIN_MENU:
|
case vamp_zero::app_mode::MAIN_MENU:
|
||||||
if (state.audio.tempo_editing) {
|
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;
|
state.audio.tempo_editing = true;
|
||||||
}
|
}
|
||||||
return clamp_state(std::move(state));
|
return clamp_state(std::move(state));
|
||||||
|
|
||||||
case vamp_zero::app_mode::PERFORMANCE_SONG_MENU:
|
case vamp_zero::app_mode::PERFORMANCE_SONG_MENU:
|
||||||
if (command == vamp_protocol::page_command::NEXT) {
|
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_song_index = wrap_next(state.selected_song_index, menu_row_count(state.performance.groups.size()));
|
||||||
state.selected_part_index = 0;
|
state.selected_part_index = 0;
|
||||||
} else if (command == vamp_protocol::page_command::PREVIOUS) {
|
} 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;
|
state.selected_part_index = 0;
|
||||||
} else if (state.selected_song_index == 0U) {
|
} else if (state.selected_song_index == 0U) {
|
||||||
state.mode = vamp_zero::app_mode::MAIN_MENU;
|
state.mode = vamp_zero::app_mode::MAIN_MENU;
|
||||||
@@ -382,9 +381,8 @@ auto vamp_zero::apply_app_command(vamp_zero::app_state state, vamp_protocol::pag
|
|||||||
state.selected_part_index = 0;
|
state.selected_part_index = 0;
|
||||||
}
|
}
|
||||||
return clamp_state(std::move(state));
|
return clamp_state(std::move(state));
|
||||||
|
|
||||||
case vamp_zero::app_mode::PERFORMANCE_PART_MENU:
|
case vamp_zero::app_mode::PERFORMANCE_PART_MENU:
|
||||||
if (auto const* group = selected_group(state); group != nullptr) {
|
if (auto const *group = selected_group(state); group != nullptr) {
|
||||||
if (command == vamp_protocol::page_command::NEXT) {
|
if (command == vamp_protocol::page_command::NEXT) {
|
||||||
state.selected_part_index = wrap_next(state.selected_part_index, menu_row_count(group->parts.size()));
|
state.selected_part_index = wrap_next(state.selected_part_index, menu_row_count(group->parts.size()));
|
||||||
} else if (command == vamp_protocol::page_command::PREVIOUS) {
|
} else if (command == vamp_protocol::page_command::PREVIOUS) {
|
||||||
@@ -393,7 +391,7 @@ auto vamp_zero::apply_app_command(vamp_zero::app_state state, vamp_protocol::pag
|
|||||||
state.mode = vamp_zero::app_mode::PERFORMANCE_SONG_MENU;
|
state.mode = vamp_zero::app_mode::PERFORMANCE_SONG_MENU;
|
||||||
state.active_part_id.clear();
|
state.active_part_id.clear();
|
||||||
state.page_index = 0;
|
state.page_index = 0;
|
||||||
} else if (auto const* part = selected_part(state); part != nullptr) {
|
} else if (auto const *part = selected_part(state); part != nullptr) {
|
||||||
state.active_part_id = part->id;
|
state.active_part_id = part->id;
|
||||||
state.active_practice_id.clear();
|
state.active_practice_id.clear();
|
||||||
state.page_index = 0;
|
state.page_index = 0;
|
||||||
@@ -401,9 +399,8 @@ auto vamp_zero::apply_app_command(vamp_zero::app_state state, vamp_protocol::pag
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return clamp_state(std::move(state));
|
return clamp_state(std::move(state));
|
||||||
|
|
||||||
case vamp_zero::app_mode::PERFORMANCE_SCORE:
|
case vamp_zero::app_mode::PERFORMANCE_SCORE:
|
||||||
if (auto const* part = active_part(state); part != nullptr) {
|
if (auto const *part = active_part(state); part != nullptr) {
|
||||||
if (command == vamp_protocol::page_command::NEXT) {
|
if (command == vamp_protocol::page_command::NEXT) {
|
||||||
state.page_index = std::min(state.page_index + 1U, part->total_pages == 0U ? 0U : part->total_pages - 1U);
|
state.page_index = std::min(state.page_index + 1U, part->total_pages == 0U ? 0U : part->total_pages - 1U);
|
||||||
} else if (command == vamp_protocol::page_command::PREVIOUS) {
|
} else if (command == vamp_protocol::page_command::PREVIOUS) {
|
||||||
@@ -415,15 +412,16 @@ auto vamp_zero::apply_app_command(vamp_zero::app_state state, vamp_protocol::pag
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return clamp_state(std::move(state));
|
return clamp_state(std::move(state));
|
||||||
|
|
||||||
case vamp_zero::app_mode::PRACTICE_TYPE_MENU:
|
case vamp_zero::app_mode::PRACTICE_TYPE_MENU:
|
||||||
if (command == vamp_protocol::page_command::NEXT) {
|
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_lesson_index = 0;
|
||||||
state.selected_practice_exercise_index = 0;
|
state.selected_practice_exercise_index = 0;
|
||||||
state.selected_practice_key_index = 0;
|
state.selected_practice_key_index = 0;
|
||||||
} else if (command == vamp_protocol::page_command::PREVIOUS) {
|
} 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_lesson_index = 0;
|
||||||
state.selected_practice_exercise_index = 0;
|
state.selected_practice_exercise_index = 0;
|
||||||
state.selected_practice_key_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;
|
state.selected_practice_key_index = 0;
|
||||||
}
|
}
|
||||||
return clamp_state(std::move(state));
|
return clamp_state(std::move(state));
|
||||||
|
|
||||||
case vamp_zero::app_mode::PRACTICE_LESSON_MENU:
|
case vamp_zero::app_mode::PRACTICE_LESSON_MENU:
|
||||||
if (auto const* type = selected_practice_type(state); type != nullptr) {
|
if (auto const *type = selected_practice_type(state); type != nullptr) {
|
||||||
if (command == vamp_protocol::page_command::NEXT) {
|
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_exercise_index = 0;
|
||||||
state.selected_practice_key_index = 0;
|
state.selected_practice_key_index = 0;
|
||||||
} else if (command == vamp_protocol::page_command::PREVIOUS) {
|
} 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_exercise_index = 0;
|
||||||
state.selected_practice_key_index = 0;
|
state.selected_practice_key_index = 0;
|
||||||
} else if (state.selected_practice_lesson_index == 0U) {
|
} else if (state.selected_practice_lesson_index == 0U) {
|
||||||
@@ -458,20 +457,21 @@ auto vamp_zero::apply_app_command(vamp_zero::app_state state, vamp_protocol::pag
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return clamp_state(std::move(state));
|
return clamp_state(std::move(state));
|
||||||
|
|
||||||
case vamp_zero::app_mode::PRACTICE_EXERCISE_MENU:
|
case vamp_zero::app_mode::PRACTICE_EXERCISE_MENU:
|
||||||
if (auto const* lesson = selected_practice_lesson(state); lesson != nullptr) {
|
if (auto const *lesson = selected_practice_lesson(state); lesson != nullptr) {
|
||||||
if (command == vamp_protocol::page_command::NEXT) {
|
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;
|
state.selected_practice_key_index = 0;
|
||||||
} else if (command == vamp_protocol::page_command::PREVIOUS) {
|
} 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;
|
state.selected_practice_key_index = 0;
|
||||||
} else if (state.selected_practice_exercise_index == 0U) {
|
} else if (state.selected_practice_exercise_index == 0U) {
|
||||||
state.mode = vamp_zero::app_mode::PRACTICE_LESSON_MENU;
|
state.mode = vamp_zero::app_mode::PRACTICE_LESSON_MENU;
|
||||||
state.active_practice_id.clear();
|
state.active_practice_id.clear();
|
||||||
state.page_index = 0;
|
state.page_index = 0;
|
||||||
} else if (auto const* exercise = selected_practice_exercise(state); exercise != nullptr) {
|
} else if (auto const *exercise = selected_practice_exercise(state); exercise != nullptr) {
|
||||||
state.active_part_id.clear();
|
state.active_part_id.clear();
|
||||||
state.active_practice_id.clear();
|
state.active_practice_id.clear();
|
||||||
state.page_index = 0;
|
state.page_index = 0;
|
||||||
@@ -486,13 +486,15 @@ auto vamp_zero::apply_app_command(vamp_zero::app_state state, vamp_protocol::pag
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return clamp_state(std::move(state));
|
return clamp_state(std::move(state));
|
||||||
|
|
||||||
case vamp_zero::app_mode::PRACTICE_KEY_MENU:
|
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) {
|
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) {
|
} 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) {
|
} else if (state.selected_practice_key_index == 0U) {
|
||||||
state.mode = vamp_zero::app_mode::PRACTICE_EXERCISE_MENU;
|
state.mode = vamp_zero::app_mode::PRACTICE_EXERCISE_MENU;
|
||||||
state.active_practice_id.clear();
|
state.active_practice_id.clear();
|
||||||
@@ -505,17 +507,17 @@ auto vamp_zero::apply_app_command(vamp_zero::app_state state, vamp_protocol::pag
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return clamp_state(std::move(state));
|
return clamp_state(std::move(state));
|
||||||
|
|
||||||
case vamp_zero::app_mode::PRACTICE_SCORE:
|
case vamp_zero::app_mode::PRACTICE_SCORE:
|
||||||
if (auto const* exercise = active_practice(state); exercise != nullptr) {
|
if (auto const *exercise = active_practice(state); exercise != nullptr) {
|
||||||
auto const* variant = selected_practice_variant(state, *exercise);
|
auto const *variant = selected_practice_variant(state, *exercise);
|
||||||
if (command == vamp_protocol::page_command::NEXT) {
|
if (command == vamp_protocol::page_command::NEXT) {
|
||||||
auto pages = variant == nullptr ? 0U : variant->total_pages;
|
auto pages = variant == nullptr ? 0U : variant->total_pages;
|
||||||
state.page_index = std::min(state.page_index + 1U, pages == 0U ? 0U : pages - 1U);
|
state.page_index = std::min(state.page_index + 1U, pages == 0U ? 0U : pages - 1U);
|
||||||
} else if (command == vamp_protocol::page_command::PREVIOUS) {
|
} else if (command == vamp_protocol::page_command::PREVIOUS) {
|
||||||
state.page_index = state.page_index == 0U ? 0U : state.page_index - 1U;
|
state.page_index = state.page_index == 0U ? 0U : state.page_index - 1U;
|
||||||
} else {
|
} 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.active_practice_id.clear();
|
||||||
state.page_index = 0;
|
state.page_index = 0;
|
||||||
}
|
}
|
||||||
@@ -525,22 +527,21 @@ auto vamp_zero::apply_app_command(vamp_zero::app_state state, vamp_protocol::pag
|
|||||||
return clamp_state(std::move(state));
|
return clamp_state(std::move(state));
|
||||||
}
|
}
|
||||||
|
|
||||||
auto vamp_zero::app_status_json(vamp_zero::app_state const& state) -> boost::json::value {
|
auto vamp_zero::app_status_json(vamp_zero::app_state const &state) -> boost::json::value {
|
||||||
auto selection = boost::json::object{};
|
auto selection = boost::json::object{};
|
||||||
selection["main_menu_index"] = static_cast<std::uint64_t>(state.selected_main_menu_index);
|
selection["main_menu_index"] = static_cast<std::uint64_t>(state.selected_main_menu_index);
|
||||||
selection["song_row_index"] = static_cast<std::uint64_t>(state.selected_song_index);
|
selection["song_row_index"] = static_cast<std::uint64_t>(state.selected_song_index);
|
||||||
selection["part_row_index"] = static_cast<std::uint64_t>(state.selected_part_index);
|
selection["part_row_index"] = static_cast<std::uint64_t>(state.selected_part_index);
|
||||||
if (auto const* group = selected_group(state); group != nullptr) {
|
if (auto const *group = selected_group(state); group != nullptr) {
|
||||||
selection["title"] = group->title;
|
selection["title"] = group->title;
|
||||||
selection["composer"] = group->composer;
|
selection["composer"] = group->composer;
|
||||||
}
|
}
|
||||||
if (auto const* part = selected_part(state); part != nullptr) {
|
if (auto const *part = selected_part(state); part != nullptr) {
|
||||||
selection["part"] = part_json(*part);
|
selection["part"] = part_json(*part);
|
||||||
}
|
}
|
||||||
|
|
||||||
auto active = boost::json::object{};
|
auto active = boost::json::object{};
|
||||||
if (state.mode == vamp_zero::app_mode::PERFORMANCE_SCORE) {
|
if (state.mode == vamp_zero::app_mode::PERFORMANCE_SCORE) {
|
||||||
if (auto const* part = active_part(state); part != nullptr) {
|
if (auto const *part = active_part(state); part != nullptr) {
|
||||||
active["selected"] = true;
|
active["selected"] = true;
|
||||||
active["kind"] = "performance";
|
active["kind"] = "performance";
|
||||||
active["part"] = part_json(*part);
|
active["part"] = part_json(*part);
|
||||||
@@ -549,8 +550,8 @@ auto vamp_zero::app_status_json(vamp_zero::app_state const& state) -> boost::jso
|
|||||||
active["page_total"] = static_cast<std::uint64_t>(part->total_pages);
|
active["page_total"] = static_cast<std::uint64_t>(part->total_pages);
|
||||||
}
|
}
|
||||||
} else if (state.mode == vamp_zero::app_mode::PRACTICE_SCORE) {
|
} else if (state.mode == vamp_zero::app_mode::PRACTICE_SCORE) {
|
||||||
if (auto const* exercise = active_practice(state); exercise != nullptr) {
|
if (auto const *exercise = active_practice(state); exercise != nullptr) {
|
||||||
auto const* variant = selected_practice_variant(state, *exercise);
|
auto const *variant = selected_practice_variant(state, *exercise);
|
||||||
active["selected"] = true;
|
active["selected"] = true;
|
||||||
active["kind"] = "practice";
|
active["kind"] = "practice";
|
||||||
active["exercise"] = exercise_json(*exercise);
|
active["exercise"] = exercise_json(*exercise);
|
||||||
@@ -566,11 +567,9 @@ auto vamp_zero::app_status_json(vamp_zero::app_state const& state) -> boost::jso
|
|||||||
active["page_number"] = 0;
|
active["page_number"] = 0;
|
||||||
active["page_total"] = 0;
|
active["page_total"] = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto remote = boost::json::object{};
|
auto remote = boost::json::object{};
|
||||||
remote["pico_connected"] = state.pico_connected;
|
remote["pico_connected"] = state.pico_connected;
|
||||||
remote["heartbeat_count"] = state.heartbeat_count;
|
remote["heartbeat_count"] = state.heartbeat_count;
|
||||||
|
|
||||||
auto performance = performance_json(state);
|
auto performance = performance_json(state);
|
||||||
auto root = boost::json::object{};
|
auto root = boost::json::object{};
|
||||||
root["mode"] = vamp_zero::to_string(state.mode);
|
root["mode"] = vamp_zero::to_string(state.mode);
|
||||||
@@ -585,14 +584,14 @@ auto vamp_zero::app_status_json(vamp_zero::app_state const& state) -> boost::jso
|
|||||||
return root;
|
return root;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto vamp_zero::remote_display_text(vamp_zero::app_state const& state) -> std::string {
|
auto vamp_zero::remote_display_text(vamp_zero::app_state const &state) -> std::string {
|
||||||
auto title = std::string{};
|
auto title = std::string{};
|
||||||
auto part = std::string{};
|
auto part = std::string{};
|
||||||
auto page = std::size_t{};
|
auto page = std::size_t{};
|
||||||
auto pages = std::size_t{};
|
auto pages = std::size_t{};
|
||||||
auto active = std::bool_t{false};
|
auto active = std::bool_t{false};
|
||||||
if (state.mode == vamp_zero::app_mode::PERFORMANCE_SCORE) {
|
if (state.mode == vamp_zero::app_mode::PERFORMANCE_SCORE) {
|
||||||
if (auto const* selected = active_part(state); selected != nullptr) {
|
if (auto const *selected = active_part(state); selected != nullptr) {
|
||||||
active = true;
|
active = true;
|
||||||
title = selected->title;
|
title = selected->title;
|
||||||
part = selected->part;
|
part = selected->part;
|
||||||
@@ -600,8 +599,8 @@ auto vamp_zero::remote_display_text(vamp_zero::app_state const& state) -> std::s
|
|||||||
pages = selected->total_pages;
|
pages = selected->total_pages;
|
||||||
}
|
}
|
||||||
} else if (state.mode == vamp_zero::app_mode::PRACTICE_SCORE) {
|
} else if (state.mode == vamp_zero::app_mode::PRACTICE_SCORE) {
|
||||||
if (auto const* selected = active_practice(state); selected != nullptr) {
|
if (auto const *selected = active_practice(state); selected != nullptr) {
|
||||||
auto const* variant = selected_practice_variant(state, *selected);
|
auto const *variant = selected_practice_variant(state, *selected);
|
||||||
active = true;
|
active = true;
|
||||||
title = selected->title;
|
title = selected->title;
|
||||||
part = "Key: " + effective_practice_key(state, *selected);
|
part = "Key: " + effective_practice_key(state, *selected);
|
||||||
@@ -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;
|
pages = variant == nullptr ? 0U : variant->total_pages;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
auto output = std::ostringstream{};
|
auto output = std::ostringstream{};
|
||||||
output << "mode=" << vamp_zero::to_string(state.mode) << "\n";
|
output << "mode=" << vamp_zero::to_string(state.mode) << "\n";
|
||||||
output << "active=" << (active ? "1" : "0") << "\n";
|
output << "active=" << (active ? "1" : "0") << "\n";
|
||||||
@@ -620,7 +618,7 @@ auto vamp_zero::remote_display_text(vamp_zero::app_state const& state) -> std::s
|
|||||||
return output.str();
|
return output.str();
|
||||||
}
|
}
|
||||||
|
|
||||||
auto vamp_zero::validate_app_state(vamp_zero::app_state const& state) -> std::expected<void, modulus::error> {
|
auto vamp_zero::validate_app_state(vamp_zero::app_state const &state) -> std::expected<void, modulus::error> {
|
||||||
if (state.selected_main_menu_index >= main_menu_count) {
|
if (state.selected_main_menu_index >= main_menu_count) {
|
||||||
return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::INVALID_ARGUMENT, "selected main menu index is out of range"));
|
return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::INVALID_ARGUMENT, "selected main menu index is out of range"));
|
||||||
}
|
}
|
||||||
@@ -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) {
|
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"));
|
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"));
|
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) {
|
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())) {
|
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"));
|
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"));
|
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())) {
|
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"));
|
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"));
|
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"));
|
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)) {
|
if (state.selected_practice_key_index >= practice_key_row_count(state)) {
|
||||||
@@ -664,13 +666,13 @@ auto vamp_zero::validate_app_state(vamp_zero::app_state const& state) -> std::ex
|
|||||||
return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::INVALID_ARGUMENT, "practice exercise mode requires a selected lesson group"));
|
return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::INVALID_ARGUMENT, "practice exercise mode requires a selected lesson group"));
|
||||||
}
|
}
|
||||||
if (state.mode == vamp_zero::app_mode::PRACTICE_KEY_MENU) {
|
if (state.mode == vamp_zero::app_mode::PRACTICE_KEY_MENU) {
|
||||||
auto const* exercise = selected_practice_exercise(state);
|
auto const *exercise = selected_practice_exercise(state);
|
||||||
if (exercise == nullptr || !supports_key_selection(*exercise)) {
|
if (exercise == nullptr || !supports_key_selection(*exercise)) {
|
||||||
return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::INVALID_ARGUMENT, "practice key mode requires a melody exercise"));
|
return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::INVALID_ARGUMENT, "practice key mode requires a melody exercise"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (state.mode == vamp_zero::app_mode::PERFORMANCE_SCORE) {
|
if (state.mode == vamp_zero::app_mode::PERFORMANCE_SCORE) {
|
||||||
auto const* part = active_part(state);
|
auto const *part = active_part(state);
|
||||||
if (part == nullptr) {
|
if (part == nullptr) {
|
||||||
return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::INVALID_ARGUMENT, "performance score mode requires an active part"));
|
return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::INVALID_ARGUMENT, "performance score mode requires an active part"));
|
||||||
}
|
}
|
||||||
@@ -679,11 +681,11 @@ auto vamp_zero::validate_app_state(vamp_zero::app_state const& state) -> std::ex
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (state.mode == vamp_zero::app_mode::PRACTICE_SCORE) {
|
if (state.mode == vamp_zero::app_mode::PRACTICE_SCORE) {
|
||||||
auto const* exercise = active_practice(state);
|
auto const *exercise = active_practice(state);
|
||||||
if (exercise == nullptr) {
|
if (exercise == nullptr) {
|
||||||
return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::INVALID_ARGUMENT, "practice score mode requires an active exercise"));
|
return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::INVALID_ARGUMENT, "practice score mode requires an active exercise"));
|
||||||
}
|
}
|
||||||
auto const* variant = selected_practice_variant(state, *exercise);
|
auto const *variant = selected_practice_variant(state, *exercise);
|
||||||
if (variant == nullptr) {
|
if (variant == nullptr) {
|
||||||
return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::INVALID_ARGUMENT, "practice score mode requires a selected generated variant"));
|
return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::INVALID_ARGUMENT, "practice score mode requires a selected generated variant"));
|
||||||
}
|
}
|
||||||
|
|||||||
+17
-17
@@ -1,7 +1,6 @@
|
|||||||
#include <vamp_zero/portal.hpp>
|
#include <vamp_zero/portal.hpp>
|
||||||
|
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
#include <cstdint>
|
|
||||||
#include <expected>
|
#include <expected>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
@@ -15,19 +14,20 @@
|
|||||||
#include <vamp_protocol/page_control.hpp>
|
#include <vamp_protocol/page_control.hpp>
|
||||||
#include <vamp_zero/page_control.hpp>
|
#include <vamp_zero/page_control.hpp>
|
||||||
|
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
auto portal_html() -> std::string {
|
auto portal_html() -> std::string {
|
||||||
return R"html(<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Product Vamp</title></head><body><main><h1>Product Vamp</h1><pre id="status">Loading...</pre><p><button id="prev">Previous</button> <button id="select">Select</button> <button id="next">Next</button></p><script>
|
return R"html(<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Product Vamp</title></head><body><main><h1>Product Vamp</h1><pre id="status">Loading...</pre><p><button id="prev">Previous</button> <button id="select">Select</button> <button id="next">Next</button></p><script>
|
||||||
async function refresh(){const r=await fetch('/api/status');const j=await r.json();document.getElementById('status').textContent=JSON.stringify(j.payload||j,null,2);}
|
async function refresh(){const r=await fetch('/api/status');const j=await r.json();document.getElementById('status').textContent=JSON.stringify(j.payload||j,null,2);}
|
||||||
document.getElementById('prev').onclick=async()=>{await fetch('/api/remote/previous',{method:'POST'});refresh();};
|
document.getElementById('prev').onclick=async()=>{await fetch('/api/remote/previous',{method:'POST'});refresh();};
|
||||||
document.getElementById('select').onclick=async()=>{await fetch('/api/remote/select',{method:'POST'});refresh();};
|
document.getElementById('select').onclick=async()=>{await fetch('/api/remote/select',{method:'POST'});refresh();};
|
||||||
document.getElementById('next').onclick=async()=>{await fetch('/api/remote/next',{method:'POST'});refresh();};
|
document.getElementById('next').onclick=async()=>{await fetch('/api/remote/next',{method:'POST'});refresh();};
|
||||||
refresh();
|
refresh();
|
||||||
</script></main></body></html>)html";
|
</script></main></body></html>)html";
|
||||||
}
|
}
|
||||||
|
|
||||||
auto endpoint_port(std::string const& endpoint) -> std::string {
|
auto endpoint_port(std::string const &endpoint) -> std::string {
|
||||||
auto separator = endpoint.rfind(':');
|
auto separator = endpoint.rfind(':');
|
||||||
if (separator == std::string::npos || separator + 1U >= endpoint.size()) {
|
if (separator == std::string::npos || separator + 1U >= endpoint.size()) {
|
||||||
return "8080";
|
return "8080";
|
||||||
@@ -35,13 +35,14 @@ auto endpoint_port(std::string const& endpoint) -> std::string {
|
|||||||
return endpoint.substr(separator + 1U);
|
return endpoint.substr(separator + 1U);
|
||||||
}
|
}
|
||||||
|
|
||||||
auto remote_response(vamp_zero::app_state const& state) -> ligo::service_response {
|
auto remote_response(vamp_zero::app_state const &state) -> ligo::service_response {
|
||||||
return ligo::text_response("text/plain; charset=utf-8", vamp_zero::remote_display_text(state));
|
return ligo::text_response("text/plain; charset=utf-8", vamp_zero::remote_display_text(state));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 vamp_zero::control_state::config() const -> vamp_zero::config {
|
||||||
auto lock = std::scoped_lock{mutex_};
|
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 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 updated = vamp_zero::app_state{};
|
||||||
{
|
{
|
||||||
auto lock = std::scoped_lock{mutex_};
|
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 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 updated = vamp_zero::app_state{};
|
||||||
{
|
{
|
||||||
auto lock = std::scoped_lock{mutex_};
|
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 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 updated = vamp_zero::app_state{};
|
||||||
{
|
{
|
||||||
auto lock = std::scoped_lock{mutex_};
|
auto lock = std::scoped_lock{mutex_};
|
||||||
@@ -118,17 +119,16 @@ auto vamp_zero::control_state::refresh_connection() -> std::optional<vamp_zero::
|
|||||||
return updated;
|
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_};
|
auto lock = std::scoped_lock{mutex_};
|
||||||
observer_ = std::move(observer);
|
observer_ = std::move(observer);
|
||||||
}
|
}
|
||||||
|
|
||||||
auto vamp_zero::portal_status_json(vamp_zero::config const& config, vamp_zero::app_state const& state) -> boost::json::value {
|
auto vamp_zero::portal_status_json(vamp_zero::config const &config, vamp_zero::app_state const &state) -> boost::json::value {
|
||||||
auto control = boost::json::object{};
|
auto control = boost::json::object{};
|
||||||
control["ap_ssid"] = config.control.ap_ssid;
|
control["ap_ssid"] = config.control.ap_ssid;
|
||||||
control["zero_ip"] = config.control.zero_ip;
|
control["zero_ip"] = config.control.zero_ip;
|
||||||
control["endpoint"] = "http://" + config.control.zero_ip + ":" + endpoint_port(config.control.endpoint.bind);
|
control["endpoint"] = "http://" + config.control.zero_ip + ":" + endpoint_port(config.control.endpoint.bind);
|
||||||
|
|
||||||
auto root = boost::json::object{};
|
auto root = boost::json::object{};
|
||||||
root["device"] = "zero";
|
root["device"] = "zero";
|
||||||
root["version"] = VAMP_VERSION;
|
root["version"] = VAMP_VERSION;
|
||||||
@@ -138,7 +138,7 @@ auto vamp_zero::portal_status_json(vamp_zero::config const& config, vamp_zero::a
|
|||||||
return root;
|
return root;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto vamp_zero::make_router(vamp_zero::control_state& state) -> ligo::service_router {
|
auto vamp_zero::make_router(vamp_zero::control_state &state) -> ligo::service_router {
|
||||||
auto router = ligo::service_router{};
|
auto router = ligo::service_router{};
|
||||||
router.add_response_route("GET", vamp_protocol::root_path, [](ligo::request_context, boost::json::value) -> boost::asio::awaitable<std::expected<ligo::service_response, modulus::error>> {
|
router.add_response_route("GET", vamp_protocol::root_path, [](ligo::request_context, boost::json::value) -> boost::asio::awaitable<std::expected<ligo::service_response, modulus::error>> {
|
||||||
co_return ligo::text_response("text/html; charset=utf-8", portal_html());
|
co_return ligo::text_response("text/html; charset=utf-8", portal_html());
|
||||||
|
|||||||
+989
-895
File diff suppressed because it is too large
Load Diff
@@ -3,6 +3,7 @@
|
|||||||
#include <string_view>
|
#include <string_view>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
|
|
||||||
auto vamp_zero::to_string(vamp_zero::hardware_mode value) -> std::string {
|
auto vamp_zero::to_string(vamp_zero::hardware_mode value) -> std::string {
|
||||||
switch (value) {
|
switch (value) {
|
||||||
case vamp_zero::hardware_mode::FAKE:
|
case vamp_zero::hardware_mode::FAKE:
|
||||||
@@ -37,7 +38,7 @@ auto vamp_zero::to_string(vamp_zero::app_mode value) -> std::string {
|
|||||||
return "main_menu";
|
return "main_menu";
|
||||||
}
|
}
|
||||||
|
|
||||||
auto vamp_zero::practice_key_names() -> std::vector<std::string_view> const& {
|
auto vamp_zero::practice_key_names() -> std::vector<std::string_view> const & {
|
||||||
static auto const keys = std::vector<std::string_view>{
|
static auto const keys = std::vector<std::string_view>{
|
||||||
"C",
|
"C",
|
||||||
"Db",
|
"Db",
|
||||||
|
|||||||
+213
-102
@@ -22,14 +22,15 @@
|
|||||||
#include <vamp_zero/color.hpp>
|
#include <vamp_zero/color.hpp>
|
||||||
#include <vamp_zero/library.hpp>
|
#include <vamp_zero/library.hpp>
|
||||||
#include <vamp_zero/navigation.hpp>
|
#include <vamp_zero/navigation.hpp>
|
||||||
|
#include <vamp_zero/parse.hpp>
|
||||||
#include <vamp_zero/score.hpp>
|
#include <vamp_zero/score.hpp>
|
||||||
|
|
||||||
|
|
||||||
struct vamp_zero::score_draw_cache {
|
struct vamp_zero::score_draw_cache {
|
||||||
struct entry {
|
struct entry {
|
||||||
std::string source_json{};
|
std::string source_json{};
|
||||||
std::shared_ptr<vamp_zero::score_document const> document{};
|
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 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;
|
auto key = source_path;
|
||||||
key.push_back('\n');
|
key.push_back('\n');
|
||||||
@@ -38,12 +39,10 @@ struct vamp_zero::score_draw_cache {
|
|||||||
key += std::to_string(source_json.size());
|
key += std::to_string(source_json.size());
|
||||||
key.push_back('\n');
|
key.push_back('\n');
|
||||||
key += std::to_string(std::hash<std::string_view>{}(source_json));
|
key += std::to_string(std::hash<std::string_view>{}(source_json));
|
||||||
|
|
||||||
auto found = entries.find(key);
|
auto found = entries.find(key);
|
||||||
if (found != entries.end() && found->second.source_json == source_json && found->second.document) {
|
if (found != entries.end() && found->second.source_json == source_json && found->second.document) {
|
||||||
return found->second.document;
|
return found->second.document;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto parsed = vamp_zero::parse_score_document(source_json, source_path);
|
auto parsed = vamp_zero::parse_score_document(source_json, source_path);
|
||||||
if (!parsed) {
|
if (!parsed) {
|
||||||
return std::unexpected(parsed.error());
|
return std::unexpected(parsed.error());
|
||||||
@@ -58,15 +57,13 @@ struct vamp_zero::score_draw_cache {
|
|||||||
};
|
};
|
||||||
return document;
|
return document;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::unordered_map<std::string, entry> entries{};
|
std::unordered_map<std::string, entry> entries{};
|
||||||
std::shared_ptr<vamp_zero::score_document const> page_document{};
|
std::shared_ptr<vamp_zero::score_document const> page_document{};
|
||||||
std::size_t page_index{};
|
std::size_t page_index{};
|
||||||
std::optional<vamp_zero::score_page_layout> page{};
|
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 {
|
||||||
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 ||
|
||||||
if (!page || page_document != document || index != page_index
|
page->bounds.y != bounds.y || page->bounds.width != bounds.width || page->bounds.height != bounds.height) {
|
||||||
|| 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 = vamp_zero::layout_score_page(bounds, *document, index);
|
||||||
page_document = std::move(document);
|
page_document = std::move(document);
|
||||||
page_index = index;
|
page_index = index;
|
||||||
@@ -80,22 +77,16 @@ namespace {
|
|||||||
constexpr auto touch_hold_duration = std::chrono::milliseconds{700};
|
constexpr auto touch_hold_duration = std::chrono::milliseconds{700};
|
||||||
constexpr auto touch_double_tap_duration = std::chrono::milliseconds{220};
|
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 bind_port(vamp_zero::config const& config) -> std::uint16_t {
|
|
||||||
auto const& bind = config.control.endpoint.bind;
|
|
||||||
auto colon = bind.rfind(':');
|
auto colon = bind.rfind(':');
|
||||||
if (colon == std::string::npos) {
|
if (colon == std::string::npos) {
|
||||||
return 8080;
|
return 8080;
|
||||||
}
|
}
|
||||||
try {
|
return vamp_zero::parse_integer<std::uint16_t>(std::string_view{bind}.substr(colon + 1U), "port").value_or(8080);
|
||||||
return static_cast<std::uint16_t>(std::stoul(bind.substr(colon + 1U)));
|
|
||||||
} catch (...) {
|
|
||||||
return 8080;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
auto status_color(vamp_zero::app_state const& state) -> vamp_style::color {
|
auto status_color(vamp_zero::app_state const &state) -> vamp_style::color {
|
||||||
return state.pico_connected ? vamp_style::connected : vamp_style::retrying;
|
return state.pico_connected ? vamp_style::connected : vamp_style::retrying;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,7 +106,7 @@ auto text_style(std::int32_t scale) -> luma::text_style {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
auto draw_text(luma::draw_context& context, std::float32_t x, std::float32_t y, std::string const& text, std::int32_t scale, vamp_style::color color, std::float32_t max_width) -> void {
|
auto draw_text(luma::draw_context &context, std::float32_t x, std::float32_t y, std::string const &text, std::int32_t scale, vamp_style::color color, std::float32_t max_width) -> void {
|
||||||
if (text.empty() || max_width <= 0.0F) {
|
if (text.empty() || max_width <= 0.0F) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -125,23 +116,27 @@ auto draw_text(luma::draw_context& context, std::float32_t x, std::float32_t y,
|
|||||||
context.pop_clip();
|
context.pop_clip();
|
||||||
}
|
}
|
||||||
|
|
||||||
auto draw_header(luma::draw_context& context, luma::rect bounds, vamp_zero::ui_state const& state) -> void {
|
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(
|
||||||
context.fill_rect({.x = bounds.x, .y = bounds.y + 78.0F, .width = bounds.width, .height = 2.0F}, vamp_zero::to_luma(vamp_style::accent));
|
{.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 + 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);
|
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 indicator = status_color(state.app);
|
||||||
auto x = bounds.x + bounds.width - 148.0F;
|
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));
|
context.fill_rect({.x = x, .y = bounds.y + 22.0F, .width = 24.0F, .height = 24.0F}, vamp_zero::to_luma(indicator));
|
||||||
draw_text(context, x + 36.0F, bounds.y + 26.0F, "PICO", 2, vamp_style::ink, 88.0F);
|
draw_text(context, x + 36.0F, bounds.y + 26.0F, "PICO", 2, vamp_style::ink, 88.0F);
|
||||||
}
|
}
|
||||||
|
|
||||||
auto draw_row(luma::draw_context& context, luma::rect row, std::string const& primary, std::string const& secondary, std::bool_t selected) -> void {
|
auto draw_row(luma::draw_context &context, luma::rect row, std::string const &primary, std::string const &secondary, std::bool_t selected) -> void {
|
||||||
if (selected) {
|
if (selected) {
|
||||||
context.fill_rect(row, vamp_zero::to_luma(vamp_style::selection));
|
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);
|
draw_text(context, row.x + 16.0F, row.y + 10.0F, primary, 2, vamp_style::ink, row.width * 0.62F);
|
||||||
if (!secondary.empty()) {
|
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);
|
draw_text(context, row.x + row.width - 250.0F, row.y + 17.0F, secondary, 1, selected ? vamp_style::ink : vamp_style::muted, 230.0F);
|
||||||
@@ -155,27 +150,46 @@ auto first_visible_row(std::size_t selected, std::size_t total, std::size_t max_
|
|||||||
return selected - max_rows + 1U;
|
return selected - max_rows + 1U;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto draw_main_menu(luma::draw_context& context, luma::rect bounds, vamp_zero::app_state const& state) -> void {
|
auto draw_main_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, "MODE", 4, vamp_style::ink, 260.0F);
|
draw_text(context, bounds.x + 36.0F, bounds.y + 28.0F, "MODE", 4, vamp_style::ink, 260.0F);
|
||||||
draw_text(context, bounds.x + 38.0F, bounds.y + 66.0F, "SELECT WORKFLOW", 1, vamp_style::muted, 320.0F);
|
draw_text(context, bounds.x + 38.0F, bounds.y + 66.0F, "SELECT WORKFLOW", 1, vamp_style::muted, 320.0F);
|
||||||
auto list_y = bounds.y + 112.0F;
|
auto list_y = bounds.y + 112.0F;
|
||||||
auto row_height = std::float32_t{54.0F};
|
auto row_height = std::float32_t{54.0F};
|
||||||
auto row_width = bounds.width - 72.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,
|
||||||
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);
|
{.x = bounds.x + 36.0F, .y = list_y, .width = row_width, .height = row_height},
|
||||||
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);
|
"Performance Mode",
|
||||||
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);
|
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";
|
auto tempo = std::to_string(state.audio.metronome_bpm) + " BPM";
|
||||||
if (state.audio.tempo_editing) {
|
if (state.audio.tempo_editing) {
|
||||||
tempo = "EDIT " + tempo;
|
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 {
|
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 + 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);
|
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 list_y = bounds.y + 112.0F;
|
||||||
auto row_height = std::float32_t{54.0F};
|
auto row_height = std::float32_t{54.0F};
|
||||||
auto row_width = bounds.width - 72.0F;
|
auto row_width = bounds.width - 72.0F;
|
||||||
@@ -185,26 +199,45 @@ auto draw_performance_song_menu(luma::draw_context& context, luma::rect bounds,
|
|||||||
for (auto row = std::size_t{}; row < max_rows; ++row) {
|
for (auto row = std::size_t{}; row < max_rows; ++row) {
|
||||||
auto index = first + row;
|
auto index = first + row;
|
||||||
if (index == 0U) {
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
auto const& group = state.performance.groups[index - 1U];
|
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()) {
|
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);
|
draw_text(context, bounds.x + 38.0F, list_y + row_height + 28.0F, "NO SONGS FOUND", 3, vamp_style::retrying, 420.0F);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
auto draw_performance_part_menu(luma::draw_context& context, luma::rect bounds, vamp_zero::app_state const& state) -> void {
|
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);
|
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) {
|
if (group == nullptr) {
|
||||||
draw_text(context, bounds.x + 38.0F, bounds.y + 118.0F, "NO PARTS FOUND", 3, vamp_style::retrying, 420.0F);
|
draw_text(context, bounds.x + 38.0F, bounds.y + 118.0F, "NO PARTS FOUND", 3, vamp_style::retrying, 420.0F);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
draw_text(context, bounds.x + 38.0F, bounds.y + 66.0F, group->composer, 1, vamp_style::muted, bounds.width - 72.0F);
|
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 list_y = bounds.y + 112.0F;
|
||||||
auto row_height = std::float32_t{54.0F};
|
auto row_height = std::float32_t{54.0F};
|
||||||
auto row_width = bounds.width - 72.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) {
|
for (auto row = std::size_t{}; row < max_rows; ++row) {
|
||||||
auto index = first + row;
|
auto index = first + row;
|
||||||
if (index == 0U) {
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
auto const& part = group->parts[index - 1U];
|
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 {
|
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 + 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);
|
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 list_y = bounds.y + 112.0F;
|
||||||
auto row_height = std::float32_t{54.0F};
|
auto row_height = std::float32_t{54.0F};
|
||||||
auto row_width = bounds.width - 72.0F;
|
auto row_width = bounds.width - 72.0F;
|
||||||
@@ -235,30 +281,49 @@ auto draw_practice_type_menu(luma::draw_context& context, luma::rect bounds, vam
|
|||||||
for (auto row = std::size_t{}; row < max_rows; ++row) {
|
for (auto row = std::size_t{}; row < max_rows; ++row) {
|
||||||
auto index = first + row;
|
auto index = first + row;
|
||||||
if (index == 0U) {
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
auto const& type = state.practice.types[index - 1U];
|
auto const &type = state.practice.types[index - 1U];
|
||||||
auto exercise_count = std::size_t{};
|
auto exercise_count = std::size_t{};
|
||||||
for (auto const& lesson : type.lesson_groups) {
|
for (auto const &lesson : type.lesson_groups) {
|
||||||
exercise_count += lesson.exercises.size();
|
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()) {
|
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);
|
draw_text(context, bounds.x + 38.0F, list_y + row_height + 28.0F, "NO PRACTICE EXERCISES FOUND", 3, vamp_style::retrying, 560.0F);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
auto draw_practice_lesson_menu(luma::draw_context& context, luma::rect bounds, vamp_zero::app_state const& state) -> void {
|
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);
|
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);
|
draw_text(context, bounds.x + 38.0F, bounds.y + 66.0F, "SELECT LESSON GROUP", 1, vamp_style::muted, 360.0F);
|
||||||
if (type == nullptr) {
|
if (type == nullptr) {
|
||||||
draw_text(context, bounds.x + 38.0F, bounds.y + 118.0F, "NO LESSON GROUPS FOUND", 3, vamp_style::retrying, 520.0F);
|
draw_text(context, bounds.x + 38.0F, bounds.y + 118.0F, "NO LESSON GROUPS FOUND", 3, vamp_style::retrying, 520.0F);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto list_y = bounds.y + 112.0F;
|
auto list_y = bounds.y + 112.0F;
|
||||||
auto row_height = std::float32_t{54.0F};
|
auto row_height = std::float32_t{54.0F};
|
||||||
auto row_width = bounds.width - 72.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) {
|
for (auto row = std::size_t{}; row < max_rows; ++row) {
|
||||||
auto index = first + row;
|
auto index = first + row;
|
||||||
if (index == 0U) {
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
auto const& lesson = type->lesson_groups[index - 1U];
|
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 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);
|
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);
|
draw_text(context, bounds.x + 38.0F, bounds.y + 66.0F, "SELECT EXERCISE", 1, vamp_style::muted, 320.0F);
|
||||||
if (lesson == nullptr) {
|
if (lesson == nullptr) {
|
||||||
draw_text(context, bounds.x + 38.0F, bounds.y + 118.0F, "NO EXERCISES FOUND", 3, vamp_style::retrying, 440.0F);
|
draw_text(context, bounds.x + 38.0F, bounds.y + 118.0F, "NO EXERCISES FOUND", 3, vamp_style::retrying, 440.0F);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto list_y = bounds.y + 112.0F;
|
auto list_y = bounds.y + 112.0F;
|
||||||
auto row_height = std::float32_t{54.0F};
|
auto row_height = std::float32_t{54.0F};
|
||||||
auto row_width = bounds.width - 72.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) {
|
for (auto row = std::size_t{}; row < max_rows; ++row) {
|
||||||
auto index = first + row;
|
auto index = first + row;
|
||||||
if (index == 0U) {
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
auto const& exercise = lesson->exercises[index - 1U];
|
auto const &exercise = lesson->exercises[index - 1U];
|
||||||
auto secondary = supports_key_selection(exercise) ? std::string{"KEY SELECT"} : "KEY " + effective_practice_key(state, exercise);
|
auto secondary =
|
||||||
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);
|
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 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);
|
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);
|
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 list_y = bounds.y + 112.0F;
|
||||||
auto row_height = std::float32_t{54.0F};
|
auto row_height = std::float32_t{54.0F};
|
||||||
auto row_width = bounds.width - 72.0F;
|
auto row_width = bounds.width - 72.0F;
|
||||||
@@ -317,21 +421,36 @@ auto draw_practice_key_menu(luma::draw_context& context, luma::rect bounds, vamp
|
|||||||
for (auto row = std::size_t{}; row < max_rows; ++row) {
|
for (auto row = std::size_t{}; row < max_rows; ++row) {
|
||||||
auto index = first + row;
|
auto index = first + row;
|
||||||
if (index == 0U) {
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
auto key = exercise == nullptr ? std::string{"C"} : exercise->variants[index - 1U].key_signature;
|
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;
|
auto source = exercise == nullptr || exercise->key_signature.empty() ? std::string{"SOURCE C"}
|
||||||
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);
|
: "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 draw_score(luma::draw_context& context, luma::rect bounds, vamp_zero::app_state const& state, vamp_zero::score_draw_cache& score_cache) -> void {
|
auto draw_score(luma::draw_context &context, luma::rect bounds, vamp_zero::app_state const &state, vamp_zero::score_draw_cache &score_cache) -> void {
|
||||||
auto source_json = std::string_view{};
|
auto source_json = std::string_view{};
|
||||||
auto source_path = std::string{};
|
auto source_path = std::string{};
|
||||||
auto part_override = std::string{};
|
auto part_override = std::string{};
|
||||||
if (state.mode == vamp_zero::app_mode::PERFORMANCE_SCORE) {
|
if (state.mode == vamp_zero::app_mode::PERFORMANCE_SCORE) {
|
||||||
auto const* part = active_part(state);
|
auto const *part = active_part(state);
|
||||||
if (part == nullptr) {
|
if (part == nullptr) {
|
||||||
draw_performance_song_menu(context, bounds, state);
|
draw_performance_song_menu(context, bounds, state);
|
||||||
return;
|
return;
|
||||||
@@ -339,12 +458,12 @@ auto draw_score(luma::draw_context& context, luma::rect bounds, vamp_zero::app_s
|
|||||||
source_json = part->source_json;
|
source_json = part->source_json;
|
||||||
source_path = part->source_path;
|
source_path = part->source_path;
|
||||||
} else {
|
} else {
|
||||||
auto const* exercise = active_practice(state);
|
auto const *exercise = active_practice(state);
|
||||||
if (exercise == nullptr) {
|
if (exercise == nullptr) {
|
||||||
draw_practice_type_menu(context, bounds, state);
|
draw_practice_type_menu(context, bounds, state);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
auto const* variant = selected_practice_variant(state, *exercise);
|
auto const *variant = selected_practice_variant(state, *exercise);
|
||||||
if (variant == nullptr) {
|
if (variant == nullptr) {
|
||||||
draw_practice_key_menu(context, bounds, state);
|
draw_practice_key_menu(context, bounds, state);
|
||||||
return;
|
return;
|
||||||
@@ -353,7 +472,6 @@ auto draw_score(luma::draw_context& context, luma::rect bounds, vamp_zero::app_s
|
|||||||
source_path = variant->source_path;
|
source_path = variant->source_path;
|
||||||
part_override = "Key: " + effective_practice_key(state, *exercise);
|
part_override = "Key: " + effective_practice_key(state, *exercise);
|
||||||
}
|
}
|
||||||
|
|
||||||
auto parsed = score_cache.get(source_json, source_path, std::move(part_override));
|
auto parsed = score_cache.get(source_json, source_path, std::move(part_override));
|
||||||
if (!parsed) {
|
if (!parsed) {
|
||||||
draw_text(context, bounds.x + 36.0F, bounds.y + 38.0F, "SCORE ERROR", 4, vamp_style::danger, 360.0F);
|
draw_text(context, bounds.x + 36.0F, bounds.y + 38.0F, "SCORE ERROR", 4, vamp_style::danger, 360.0F);
|
||||||
@@ -371,12 +489,12 @@ auto draw_score(luma::draw_context& context, luma::rect bounds, vamp_zero::app_s
|
|||||||
|
|
||||||
class vamp_root_control final : public luma::control {
|
class vamp_root_control final : public luma::control {
|
||||||
public:
|
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)} {}
|
: 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 {
|
auto measure(luma::draw_context &, luma::theme const &, luma::constraints limits) -> luma::size override {
|
||||||
return luma::clamp({.width = 1024.0F, .height = 600.0F}, limits);
|
return luma::clamp({.width = 1024.0F, .height = 600.0F}, limits);
|
||||||
}
|
}
|
||||||
auto draw(luma::draw_context& context, luma::theme const&) const -> void override {
|
auto draw(luma::draw_context &context, luma::theme const &) const -> void override {
|
||||||
auto area = bounds();
|
auto area = bounds();
|
||||||
context.fill_rect(area, vamp_zero::to_luma(vamp_style::background));
|
context.fill_rect(area, vamp_zero::to_luma(vamp_style::background));
|
||||||
draw_header(context, area, state_);
|
draw_header(context, area, state_);
|
||||||
@@ -400,7 +518,8 @@ public:
|
|||||||
draw_practice_exercise_menu(context, content, state_.app);
|
draw_practice_exercise_menu(context, content, state_.app);
|
||||||
} else if (state_.app.mode == vamp_zero::app_mode::PRACTICE_KEY_MENU) {
|
} else if (state_.app.mode == vamp_zero::app_mode::PRACTICE_KEY_MENU) {
|
||||||
draw_practice_key_menu(context, content, state_.app);
|
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_) {
|
if (score_cache_) {
|
||||||
draw_score(context, content, state_.app, *score_cache_);
|
draw_score(context, content, state_.app, *score_cache_);
|
||||||
}
|
}
|
||||||
@@ -408,31 +527,26 @@ public:
|
|||||||
draw_main_menu(context, content, state_.app);
|
draw_main_menu(context, content, state_.app);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
auto handle_touch(luma::touch_event const& event) -> std::bool_t override {
|
auto handle_touch(luma::touch_event const &event) -> std::bool_t override {
|
||||||
return on_touch_ ? on_touch_(event) : false;
|
return on_touch_ ? on_touch_(event) : false;
|
||||||
}
|
}
|
||||||
private:
|
private:
|
||||||
vamp_zero::ui_state state_{};
|
vamp_zero::ui_state state_{};
|
||||||
std::shared_ptr<vamp_zero::score_draw_cache> score_cache_{};
|
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)
|
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)},
|
: executor_{std::move(executor)}, tap_timer_{executor_}, application_{std::move(config)},
|
||||||
tap_timer_{executor_},
|
command_handler_{std::move(command_handler)}, score_cache_{std::make_shared<vamp_zero::score_draw_cache>()} {}
|
||||||
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 {
|
auto vamp_zero::zero_ui::send_touch_command(vamp_protocol::page_command command) -> void {
|
||||||
if (!command_handler_) {
|
if (!command_handler_) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
boost::asio::post(executor_, [handler = command_handler_, command] {
|
boost::asio::post(executor_, [handler = command_handler_, command] { handler(command); });
|
||||||
handler(command);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
auto vamp_zero::zero_ui::cancel_pending_tap_locked() -> void {
|
auto vamp_zero::zero_ui::cancel_pending_tap_locked() -> void {
|
||||||
@@ -441,7 +555,7 @@ auto vamp_zero::zero_ui::cancel_pending_tap_locked() -> void {
|
|||||||
|
|
||||||
auto vamp_zero::zero_ui::arm_pending_tap() -> void {
|
auto vamp_zero::zero_ui::arm_pending_tap() -> void {
|
||||||
tap_timer_.expires_after(touch_double_tap_duration);
|
tap_timer_.expires_after(touch_double_tap_duration);
|
||||||
tap_timer_.async_wait([this](boost::system::error_code const& error) {
|
tap_timer_.async_wait([this](boost::system::error_code const &error) {
|
||||||
if (error) {
|
if (error) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -457,17 +571,16 @@ auto vamp_zero::zero_ui::arm_pending_tap() -> void {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
auto vamp_zero::zero_ui::handle_touch(luma::touch_event const& event) -> std::bool_t {
|
auto vamp_zero::zero_ui::handle_touch(luma::touch_event const &event) -> std::bool_t {
|
||||||
if (!command_handler_ || event.points.empty()) {
|
if (!command_handler_ || event.points.empty()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto consumed = std::bool_t{false};
|
auto consumed = std::bool_t{false};
|
||||||
auto commands = std::vector<vamp_protocol::page_command>{};
|
auto commands = std::vector<vamp_protocol::page_command>{};
|
||||||
auto arm_tap = std::bool_t{false};
|
auto arm_tap = std::bool_t{false};
|
||||||
{
|
{
|
||||||
auto lock = std::scoped_lock{touch_mutex_};
|
auto lock = std::scoped_lock{touch_mutex_};
|
||||||
for (auto const& point : event.points) {
|
for (auto const &point : event.points) {
|
||||||
if (point.phase == luma::touch_phase::DOWN && !active_touch_id_) {
|
if (point.phase == luma::touch_phase::DOWN && !active_touch_id_) {
|
||||||
active_touch_id_ = point.id;
|
active_touch_id_ = point.id;
|
||||||
touch_down_time_ = event.time;
|
touch_down_time_ = event.time;
|
||||||
@@ -515,7 +628,6 @@ auto vamp_zero::zero_ui::handle_touch(luma::touch_event const& event) -> std::bo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (auto command : commands) {
|
for (auto command : commands) {
|
||||||
send_touch_command(command);
|
send_touch_command(command);
|
||||||
}
|
}
|
||||||
@@ -561,13 +673,12 @@ auto vamp_zero::zero_ui::snapshot() const -> vamp_zero::ui_state {
|
|||||||
return state_;
|
return state_;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto vamp_zero::zero_ui::make_root(vamp_zero::ui_state const& state) -> std::expected<std::unique_ptr<luma::control>, modulus::error> {
|
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 std::make_unique<vamp_root_control>(
|
||||||
return handle_touch(event);
|
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 {
|
auto vamp_zero::make_ui_state(vamp_zero::config const &config, vamp_zero::app_state app) -> vamp_zero::ui_state {
|
||||||
return vamp_zero::ui_state{
|
return vamp_zero::ui_state{
|
||||||
.app = std::move(app),
|
.app = std::move(app),
|
||||||
.ap_ssid = config.control.ap_ssid,
|
.ap_ssid = config.control.ap_ssid,
|
||||||
|
|||||||
+38
-14
@@ -7,15 +7,20 @@
|
|||||||
#include <rlgl.h>
|
#include <rlgl.h>
|
||||||
#include <luma.hpp>
|
#include <luma.hpp>
|
||||||
#include <vamp_zero/library.hpp>
|
#include <vamp_zero/library.hpp>
|
||||||
|
#include <vamp_zero/parse.hpp>
|
||||||
#include <vamp_zero/score.hpp>
|
#include <vamp_zero/score.hpp>
|
||||||
|
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
class score_view final : public luma::control {
|
class score_view final : public luma::control {
|
||||||
public:
|
public:
|
||||||
score_view(luma::application& app, vamp_zero::score_document score, std::string output, std::size_t page, std::bool_t& 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} {}
|
: 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 {
|
||||||
auto draw(luma::draw_context& context, luma::theme const&) const -> void 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_);
|
vamp_zero::draw_score_page(context, bounds(), score_, page_);
|
||||||
rlDrawRenderBatchActive();
|
rlDrawRenderBatchActive();
|
||||||
auto image = LoadImageFromScreen();
|
auto image = LoadImageFromScreen();
|
||||||
@@ -24,24 +29,43 @@ public:
|
|||||||
app_.request_stop();
|
app_.request_stop();
|
||||||
}
|
}
|
||||||
private:
|
private:
|
||||||
luma::application& app_;
|
luma::application &app_;
|
||||||
vamp_zero::score_document score_;
|
vamp_zero::score_document score_;
|
||||||
std::string output_;
|
std::string output_;
|
||||||
std::size_t page_;
|
std::size_t page_;
|
||||||
std::bool_t& saved_;
|
std::bool_t &saved_;
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int main(int argc, std::char_t** argv) {
|
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; }
|
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 input = std::ifstream{argv[1]};
|
||||||
auto json = std::string{std::istreambuf_iterator<std::char_t>{input},std::istreambuf_iterator<std::char_t>{}};
|
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]);
|
auto score = vamp_zero::parse_score_document(json, argv[1]);
|
||||||
if (!score) { std::cerr << score.error().describe() << '\n'; return 1; }
|
if (!score) {
|
||||||
auto app = luma::application{{.title="Notation Regression",.display={.width=976,.height=484,.target_fps=30,.fullscreen=false}}};
|
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 saved = std::bool_t{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));
|
auto root = app.set_root(std::make_unique<score_view>(app, std::move(*score), argv[2], page, saved));
|
||||||
if (!root) return 1;
|
if (!root) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
auto ran = app.run();
|
auto ran = app.run();
|
||||||
return ran && saved ? 0 : 1;
|
return ran && saved ? 0 : 1;
|
||||||
}
|
}
|
||||||
|
|||||||
+507
-243
File diff suppressed because it is too large
Load Diff
@@ -6,29 +6,29 @@ import types
|
|||||||
import unittest
|
import unittest
|
||||||
from unittest.mock import Mock, patch
|
from unittest.mock import Mock, patch
|
||||||
|
|
||||||
|
|
||||||
class BuildBootstrapTests(unittest.TestCase):
|
class BuildBootstrapTests(unittest.TestCase):
|
||||||
def test_requirements_fingerprint_invalidates_cached_build_image(self):
|
def test_requirements_fingerprint_invalidates_cached_build_image(self):
|
||||||
root = Path(__file__).resolve().parents[1]
|
root = Path(__file__).resolve().parents[1]
|
||||||
spec = importlib.util.spec_from_file_location('vamp_bootstrap', root / 'modulus.py')
|
spec = importlib.util.spec_from_file_location('vamp_bootstrap', root / 'modulus.py')
|
||||||
bootstrap = importlib.util.module_from_spec(spec)
|
bootstrap = importlib.util.module_from_spec(spec)
|
||||||
spec.loader.exec_module(bootstrap)
|
spec.loader.exec_module(bootstrap)
|
||||||
digest = hashlib.sha256((root / 'tools/requirements.txt').read_bytes()).hexdigest()
|
digest = hashlib.sha256((root / 'tools/requirements.txt').read_bytes()).hexdigest()
|
||||||
self.assertEqual(bootstrap.build_requirements_digest(), digest)
|
self.assertEqual(bootstrap.build_requirements_digest(), digest)
|
||||||
self.assertIn(f'build-requirements="{digest}"', bootstrap.product_build_dockerfile())
|
self.assertIn(f'build-requirements="{digest}"', bootstrap.product_build_dockerfile())
|
||||||
|
package = types.ModuleType('modulus')
|
||||||
package = types.ModuleType('modulus')
|
cli = types.ModuleType('modulus.cli')
|
||||||
cli = types.ModuleType('modulus.cli')
|
package.cli = cli
|
||||||
package.cli = cli
|
docker_run = Mock()
|
||||||
docker_run = Mock()
|
cli.docker_run = docker_run
|
||||||
cli.docker_run = docker_run
|
cli.main = Mock(return_value=0)
|
||||||
cli.main = Mock(return_value=0)
|
for command in ('clangd', 'format'):
|
||||||
with patch.dict(sys.modules, {'modulus':package, 'modulus.cli':cli}), \
|
with patch.dict(sys.modules, {'modulus': package, 'modulus.cli': cli}), \
|
||||||
patch.object(bootstrap, 'ensure_runtime') as prepare, \
|
patch.object(bootstrap, 'ensure_runtime') as prepare, \
|
||||||
patch.object(bootstrap, 'runtime_ready', return_value=True), \
|
patch.object(bootstrap, 'runtime_ready', return_value=True), \
|
||||||
patch.object(sys, 'path', list(sys.path)):
|
patch.object(sys, 'path', list(sys.path)):
|
||||||
self.assertEqual(bootstrap.delegate('clangd', []), 0)
|
self.assertEqual(bootstrap.delegate(command, []), 0)
|
||||||
prepare.assert_not_called()
|
prepare.assert_not_called()
|
||||||
docker_run.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)
|
self.assertIs(cli.docker_run, docker_run)
|
||||||
|
cli.main.reset_mock()
|
||||||
|
|||||||
+109
-121
@@ -15,125 +15,113 @@ from generator.styles import BUILT_IN_STYLES, canonical_style
|
|||||||
from generator.transposition import concert_document, shift_pitch, transpose_document
|
from generator.transposition import concert_document, shift_pitch, transpose_document
|
||||||
from tests.test_musicxml import parse, notes, xml_note, xml_score
|
from tests.test_musicxml import parse, notes, xml_note, xml_score
|
||||||
|
|
||||||
|
|
||||||
class MusicModelTests(unittest.TestCase):
|
class MusicModelTests(unittest.TestCase):
|
||||||
def test_chord_semantics_are_not_voicing_heuristics(self):
|
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},
|
cases = {'C': {0, 4, 7}, 'Cdim': {0, 3, 6}, 'Cdim7': {0, 3, 6, 9}, 'Csus2': {0, 2, 7},
|
||||||
'C7b5':{0,4,6,10},'G7b9':{7,11,2,5,8},'Fmaj7#11':{5,9,0,4,11}}
|
'C7b5': {0, 4, 6, 10}, 'G7b9': {7, 11, 2, 5, 8}, 'Fmaj7#11': {5, 9, 0, 4, 11}}
|
||||||
for label, expected in cases.items():
|
for label, expected in cases.items():
|
||||||
self.assertEqual(set(harmony.pitch_classes(harmony.parse_symbol(label))),expected,label)
|
self.assertEqual(set(harmony.pitch_classes(harmony.parse_symbol(label))), expected, label)
|
||||||
for label, essentials in [('G7b9',{11,5,8}),('C13',{4,10,9})]:
|
for label, essentials in [('G7b9', {11, 5, 8}), ('C13', {4, 10, 9})]:
|
||||||
pitches = pitches_for_voicing(label)
|
pitches = pitches_for_voicing(label)
|
||||||
self.assertTrue(essentials.issubset({pitch_number(p)%12 for p in pitches}),label)
|
self.assertTrue(essentials.issubset({pitch_number(p) % 12 for p in pitches}), label)
|
||||||
with self.assertRaises(ValueError):
|
with self.assertRaises(ValueError):
|
||||||
harmony.parse_symbol('Cunrecognised')
|
harmony.parse_symbol('Cunrecognised')
|
||||||
|
def test_transposition_preserves_spelling_octaves_modes(self):
|
||||||
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}):
|
||||||
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)
|
||||||
self.assertEqual(shift_pitch(p,0,0),p)
|
self.assertEqual(pitch_number(shift_pitch(p, 1, 2)), pitch_number(p) + 2)
|
||||||
self.assertEqual(pitch_number(shift_pitch(p,1,2)),pitch_number(p)+2)
|
source = parse(xml_score(xml_note(), attributes='<key><fifths>-3</fifths><mode>minor</mode></key>'))
|
||||||
source = parse(xml_score(xml_note(),attributes='<key><fifths>-3</fifths><mode>minor</mode></key>'))
|
result = transpose_document(source, 'D')
|
||||||
result = transpose_document(source,'D')
|
self.assertEqual(result['metadata']['key_signature'], 'D minor')
|
||||||
self.assertEqual(result['metadata']['key_signature'],'D minor')
|
self.assertEqual(result['metadata']['key']['alterations'], key_from_fifths(-1, 'minor')['alterations'])
|
||||||
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)])
|
||||||
def test_additive_and_compound_grouping(self):
|
self.assertEqual(beat_lengths({'groups': [{'beats': [6], 'unit': 8}]}), [Fraction(3, 2)] * 2)
|
||||||
self.assertEqual(beat_lengths({'groups':[{'beats':[3,2],'unit':8}]}),[Fraction(3,2),Fraction(1)])
|
self.assertEqual(beat_lengths({'groups': [{'beats': [3], 'unit': 4}]}), [Fraction(1)] * 3)
|
||||||
self.assertEqual(beat_lengths({'groups':[{'beats':[6],'unit':8}]}),[Fraction(3,2)]*2)
|
def test_generated_rhythm_follows_mid_measure_meter(self):
|
||||||
self.assertEqual(beat_lengths({'groups':[{'beats':[3],'unit':4}]}),[Fraction(1)]*3)
|
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/>')))
|
||||||
def test_generated_rhythm_follows_mid_measure_meter(self):
|
measure = source['staves'][0]['measures'][0]
|
||||||
change = '<attributes><time><beats>6</beats><beat-type>8</beat-type></time></attributes>'
|
events = generated_voice(measure, 'slash', None)
|
||||||
source = parse(xml_score(xml_note(duration=1,kind='quarter')+change+xml_note(duration=3,kind='half',extra='<dot/>')))
|
self.assertEqual([fraction(n['onset']) for n in events], [0, 1, Fraction(5, 2)])
|
||||||
measure = source['staves'][0]['measures'][0]
|
self.assertEqual([fraction(n['duration']) for n in events], [1, Fraction(3, 2), Fraction(3, 2)])
|
||||||
events = generated_voice(measure,'slash',None)
|
def test_harmony_carry_uses_last_event_and_never_future_event(self):
|
||||||
self.assertEqual([fraction(n['onset']) for n in events],[0,1,Fraction(5,2)])
|
c = harmony.parse_symbol('C7') | {'onset': rational(0)}
|
||||||
self.assertEqual([fraction(n['duration']) for n in events],[1,Fraction(3,2),Fraction(3,2)])
|
f = harmony.parse_symbol('F7') | {'onset': rational(2)}
|
||||||
|
measures = [{'harmonies': [c, f]}, {'harmonies': []}]
|
||||||
def test_harmony_carry_uses_last_event_and_never_future_event(self):
|
self.assertEqual(propagated_chords(measures), [None, f])
|
||||||
c = harmony.parse_symbol('C7') | {'onset':rational(0)}
|
self.assertIsNone(active_chord({'harmonies': [f]}, Fraction(0), None))
|
||||||
f = harmony.parse_symbol('F7') | {'onset':rational(2)}
|
self.assertEqual(active_chord(measures[0], Fraction(1), None), c)
|
||||||
measures = [{'harmonies':[c,f]},{'harmonies':[]}]
|
def test_tuplet_ratios_and_tie_links_validated(self):
|
||||||
self.assertEqual(propagated_chords(measures),[None,f])
|
document = parse(xml_score(xml_note()))
|
||||||
self.assertIsNone(active_chord({'harmonies':[f]},Fraction(0),None))
|
notes(document)[0]['pitches'][0]['tie_to'] = 'missing'
|
||||||
self.assertEqual(active_chord(measures[0],Fraction(1),None),c)
|
with self.assertRaisesRegex(ValueError, 'dangling tie'):
|
||||||
|
validate_document(document)
|
||||||
def test_tuplet_ratios_and_tie_links_validated(self):
|
document = parse(xml_score(xml_note()))
|
||||||
document = parse(xml_score(xml_note()))
|
notes(document)[0]['duration'] = {'numerator': 8, 'denominator': 2}
|
||||||
notes(document)[0]['pitches'][0]['tie_to'] = 'missing'
|
with self.assertRaisesRegex(ValueError, 'reduced'):
|
||||||
with self.assertRaisesRegex(ValueError,'dangling tie'):
|
validate_document(document)
|
||||||
validate_document(document)
|
def test_optional_turnaround_and_form(self):
|
||||||
document = parse(xml_score(xml_note()))
|
source = load_musicxml(Path('data/songs/it-could-happen-to-you.xml'))
|
||||||
notes(document)[0]['duration'] = {'numerator':8,'denominator':2}
|
for part in ('acoustic', 'electric', 'vocal'):
|
||||||
with self.assertRaisesRegex(ValueError,'reduced'):
|
result = arrange_document(source, part)
|
||||||
validate_document(document)
|
validate_document(result)
|
||||||
|
measures = result['staves'][0]['measures']
|
||||||
def test_optional_turnaround_and_form(self):
|
self.assertEqual(sum(bool(m.get('section_label')) for m in measures), 7)
|
||||||
source = load_musicxml(Path('data/songs/it-could-happen-to-you.musicxml'))
|
self.assertFalse(measures[-1]['harmonies'])
|
||||||
for part in ('acoustic','electric','vocal'):
|
self.assertTrue(any(h for m in measures[:-1] for h in m['harmonies']))
|
||||||
result = arrange_document(source,part)
|
self.assertFalse(any('chord_optional' in h for m in measures for h in m['harmonies']))
|
||||||
validate_document(result)
|
def test_player_styles_preserved(self):
|
||||||
measures = result['staves'][0]['measures']
|
self.assertEqual(len(BUILT_IN_STYLES), 50)
|
||||||
self.assertEqual(sum(bool(m.get('section_label')) for m in measures),7)
|
for text in ('Med.', 'Med. Swing', 'Medium', 'Medium Swing'):
|
||||||
self.assertFalse(measures[-1]['harmonies'])
|
self.assertEqual(canonical_style(text), 'Medium Swing')
|
||||||
self.assertTrue(any(h for m in measures[:-1] for h in m['harmonies']))
|
with self.assertRaises(ValueError):
|
||||||
self.assertFalse(any('chord_optional' in h for m in measures for h in m['harmonies']))
|
canonical_style('Unrecognised style')
|
||||||
|
def test_failure_preserves_generated_assets(self):
|
||||||
def test_player_styles_preserved(self):
|
with tempfile.TemporaryDirectory() as temp:
|
||||||
self.assertEqual(len(BUILT_IN_STYLES),50)
|
root = Path(temp)
|
||||||
for text in ('Med.','Med. Swing','Medium','Medium Swing'):
|
data = root / 'data'
|
||||||
self.assertEqual(canonical_style(text),'Medium Swing')
|
(data / 'songs').mkdir(parents=True)
|
||||||
with self.assertRaises(ValueError):
|
(data / 'songs' / 'bad.xml').write_text(xml_score(xml_note(duration=5)), encoding='utf-8')
|
||||||
canonical_style('Unrecognised style')
|
output = root / 'output'
|
||||||
|
output.mkdir()
|
||||||
def test_failure_preserves_generated_assets(self):
|
(output / 'last-good').write_text('preserved')
|
||||||
with tempfile.TemporaryDirectory() as temp:
|
args = argparse.Namespace(data_dir=data, output_dir=output, static_assets_dir=Path('package/assets'), clean=True, check=False)
|
||||||
root = Path(temp)
|
with self.assertRaises(ValueError):
|
||||||
data = root/'data'
|
generate_assets(args)
|
||||||
(data/'songs').mkdir(parents=True)
|
self.assertEqual((output / 'last-good').read_text(), 'preserved')
|
||||||
(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()), encoding='utf-8')
|
||||||
output = root/'output'
|
args.check = True
|
||||||
output.mkdir()
|
with self.assertRaisesRegex(ValueError, 'summary'):
|
||||||
(output/'last-good').write_text('preserved')
|
generate_assets(args)
|
||||||
args = argparse.Namespace(data_dir=data,output_dir=output,static_assets_dir=Path('package/assets'),clean=True,check=False)
|
self.assertEqual((output / 'last-good').read_text(), 'preserved')
|
||||||
with self.assertRaises(ValueError):
|
def test_instrument_transposition_is_converted_once(self):
|
||||||
generate_assets(args)
|
source = parse(xml_score(xml_note(), attributes='<transpose><diatonic>-1</diatonic><chromatic>-2</chromatic></transpose>'))
|
||||||
self.assertEqual((output/'last-good').read_text(),'preserved')
|
concert = concert_document(source)
|
||||||
(data/'songs'/'bad.musicxml').write_text(xml_score(xml_note()),encoding='utf-8')
|
p = notes(concert)[0]['pitches'][0]
|
||||||
args.check = True
|
self.assertEqual((p['letter'], p['alter'], p['octave']), ('b', -1, 3))
|
||||||
with self.assertRaisesRegex(ValueError,'summary'):
|
self.assertEqual(concert_document(concert), concert)
|
||||||
generate_assets(args)
|
self.assertEqual(concert['metadata']['key_signature'], 'Bb')
|
||||||
self.assertEqual((output/'last-good').read_text(),'preserved')
|
def test_deterministic_generation_and_removal(self):
|
||||||
|
with tempfile.TemporaryDirectory() as temp:
|
||||||
def test_instrument_transposition_is_converted_once(self):
|
data, output = Path(temp) / 'data', Path(temp) / 'output'
|
||||||
source = parse(xml_score(xml_note(),attributes='<transpose><diatonic>-1</diatonic><chromatic>-2</chromatic></transpose>'))
|
(data / 'songs').mkdir(parents=True)
|
||||||
concert = concert_document(source)
|
source = data / 'songs' / 'fixture.xml'
|
||||||
p = notes(concert)[0]['pitches'][0]
|
source.write_text(xml_score(xml_note()), encoding='utf-8')
|
||||||
self.assertEqual((p['letter'],p['alter'],p['octave']),('b',-1,3))
|
args = argparse.Namespace(data_dir=data, output_dir=output, static_assets_dir=Path('package/assets'), clean=True, check=False)
|
||||||
self.assertEqual(concert_document(concert),concert)
|
generate_assets(args)
|
||||||
self.assertEqual(concert['metadata']['key_signature'],'Bb')
|
first = {p.relative_to(output): p.read_bytes() for p in output.rglob('*') if p.is_file()}
|
||||||
|
generate_assets(args)
|
||||||
def test_deterministic_generation_and_removal(self):
|
self.assertEqual(first, {p.relative_to(output): p.read_bytes() for p in output.rglob('*') if p.is_file()})
|
||||||
with tempfile.TemporaryDirectory() as temp:
|
source.unlink()
|
||||||
data,output = Path(temp)/'data',Path(temp)/'output'
|
generate_assets(args)
|
||||||
(data/'songs').mkdir(parents=True)
|
self.assertFalse(list(output.rglob('*.json')))
|
||||||
source = data/'songs'/'fixture.musicxml'
|
def test_pickup_tie_survives_vocal_restoration(self):
|
||||||
source.write_text(xml_score(xml_note()),encoding='utf-8')
|
source = xml_score(xml_note(duration=1, kind='quarter', extra='<tie type="start"/>'), implicit=True)
|
||||||
args = argparse.Namespace(data_dir=data,output_dir=output,static_assets_dir=Path('package/assets'),clean=True,check=False)
|
source = source.replace('</measure></part>', '</measure><measure number="2">' + xml_note(extra='<tie type="stop"/>') + '</measure></part>')
|
||||||
generate_assets(args)
|
result = arrange_document(parse(source), 'vocal')
|
||||||
first = {p.relative_to(output):p.read_bytes() for p in output.rglob('*') if p.is_file()}
|
validate_document(result)
|
||||||
generate_assets(args)
|
measures = result['staves'][0]['measures']
|
||||||
self.assertEqual(first,{p.relative_to(output):p.read_bytes() for p in output.rglob('*') if p.is_file()})
|
start = measures[0]['voices'][0]['notes'][-1]['pitches'][0]
|
||||||
source.unlink()
|
end = measures[1]['voices'][0]['notes'][0]['pitches'][0]
|
||||||
generate_assets(args)
|
self.assertEqual(start['tie_to'], end['id'])
|
||||||
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>')
|
|
||||||
result = arrange_document(parse(source),'vocal')
|
|
||||||
validate_document(result)
|
|
||||||
measures = result['staves'][0]['measures']
|
|
||||||
start = measures[0]['voices'][0]['notes'][-1]['pitches'][0]
|
|
||||||
end = measures[1]['voices'][0]['notes'][0]['pitches'][0]
|
|
||||||
self.assertEqual(start['tie_to'],end['id'])
|
|
||||||
|
|||||||
+158
-171
@@ -6,183 +6,170 @@ import unittest
|
|||||||
from generator.musicxml import MusicXMLError, load_musicxml
|
from generator.musicxml import MusicXMLError, load_musicxml
|
||||||
from generator.model import NOTE_TYPES, TYPE_LENGTHS, fraction, pitch_number
|
from generator.model import NOTE_TYPES, TYPE_LENGTHS, fraction, pitch_number
|
||||||
|
|
||||||
|
|
||||||
def xml_score(content, attributes='', *, divisions=1, implicit=False):
|
def xml_score(content, attributes='', *, divisions=1, implicit=False):
|
||||||
return ('<?xml version="1.0"?>'
|
return ('<?xml version="1.0"?>'
|
||||||
'<score-partwise version="4.0"><work><work-title>Fixture</work-title></work>'
|
'<score-partwise version="4.0"><work><work-title>Fixture</work-title></work>'
|
||||||
'<part-list><score-part id="P1"><part-name>Lead</part-name></score-part></part-list>'
|
'<part-list><score-part id="P1"><part-name>Lead</part-name></score-part></part-list>'
|
||||||
f'<part id="P1"><measure number="1" implicit="{"yes" if implicit else "no"}">'
|
f'<part id="P1"><measure number="1" implicit="{"yes" if implicit else "no"}">'
|
||||||
f'<attributes><divisions>{divisions}</divisions><key><fifths>0</fifths></key>'
|
f'<attributes><divisions>{divisions}</divisions><key><fifths>0</fifths></key>'
|
||||||
'<time><beats>4</beats><beat-type>4</beat-type></time>'
|
'<time><beats>4</beats><beat-type>4</beat-type></time>'
|
||||||
f'<clef><sign>G</sign><line>2</line></clef>{attributes}</attributes>'
|
f'<clef><sign>G</sign><line>2</line></clef>{attributes}</attributes>'
|
||||||
f'{content}</measure></part></score-partwise>')
|
f'{content}</measure></part></score-partwise>')
|
||||||
|
|
||||||
|
|
||||||
def xml_note(step='C', duration=4, kind='whole', extra='', prefix='', alter=0, octave=4):
|
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>'
|
return (f'<note>{prefix}<pitch><step>{step}</step><alter>{alter}</alter><octave>{octave}</octave></pitch>'
|
||||||
f'<duration>{duration}</duration><type>{kind}</type>{extra}</note>')
|
f'<duration>{duration}</duration><type>{kind}</type>{extra}</note>')
|
||||||
|
|
||||||
|
|
||||||
def parse(text):
|
def parse(text):
|
||||||
with tempfile.TemporaryDirectory() as directory:
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
path = Path(directory) / 'fixture.musicxml'
|
path = Path(directory) / 'fixture.xml'
|
||||||
path.write_text(text, encoding='utf-8')
|
path.write_text(text, encoding='utf-8')
|
||||||
return load_musicxml(path)
|
return load_musicxml(path)
|
||||||
|
|
||||||
|
|
||||||
def notes(document, measure=0):
|
def notes(document, measure=0):
|
||||||
return document['staves'][0]['measures'][measure]['voices'][0]['notes']
|
return document['staves'][0]['measures'][measure]['voices'][0]['notes']
|
||||||
|
|
||||||
|
|
||||||
class MusicXMLTests(unittest.TestCase):
|
class MusicXMLTests(unittest.TestCase):
|
||||||
def test_regression_fixture(self):
|
def test_measure_sound_tempo_matches_direction_sound(self):
|
||||||
score = load_musicxml(Path('tests/fixtures/notation.musicxml'))
|
sound = '<sound tempo="120"/>'
|
||||||
self.assertEqual(len(score['staves'][0]['measures']),4)
|
direct = parse(xml_score(sound + xml_note()))
|
||||||
self.assertEqual(len(score['staves'][0]['octave_shifts']),1)
|
direction = parse(xml_score('<direction>' + sound + '</direction>' + xml_note()))
|
||||||
|
self.assertEqual(direct, direction)
|
||||||
def test_catalogue_imports(self):
|
with self.assertRaises(MusicXMLError):
|
||||||
sources = sorted(Path('data').rglob('*.musicxml'))
|
parse(xml_score('<sound tempo="0"/>' + xml_note()))
|
||||||
self.assertEqual(len(sources), 6)
|
with self.assertRaises(MusicXMLError):
|
||||||
for path in sources:
|
parse(xml_score('<sound dacapo="yes"/>' + xml_note()))
|
||||||
with self.subTest(path=path):
|
def test_regression_fixture(self):
|
||||||
self.assertEqual(load_musicxml(path)['schema_version'], 2)
|
score = load_musicxml(Path('tests/fixtures/notation.xml'))
|
||||||
|
self.assertEqual(len(score['staves'][0]['measures']), 4)
|
||||||
def test_all_written_values_and_dots(self):
|
self.assertEqual(len(score['staves'][0]['octave_shifts']), 1)
|
||||||
for kind in NOTE_TYPES:
|
def test_catalogue_imports(self):
|
||||||
for dots in (0, 1, 2, 3):
|
sources = sorted(Path('data').rglob('*.xml'))
|
||||||
value = TYPE_LENGTHS[kind] * (2-Fraction(1, 2**dots))
|
self.assertEqual(len(sources), 6)
|
||||||
score = parse(xml_score(xml_note(duration=value.numerator, kind=kind, extra='<dot/>'*dots), divisions=value.denominator, implicit=True))
|
for path in sources:
|
||||||
self.assertEqual(fraction(notes(score)[0]['duration']), value)
|
with self.subTest(path=path):
|
||||||
self.assertEqual(notes(score)[0]['written'], {'type':kind, 'dots':dots})
|
self.assertEqual(load_musicxml(path)['schema_version'], 2)
|
||||||
|
def test_all_written_values_and_dots(self):
|
||||||
def test_divisions_do_not_change_music(self):
|
for kind in NOTE_TYPES:
|
||||||
a = parse(xml_score(xml_note(duration=4)))
|
for dots in (0, 1, 2, 3):
|
||||||
b = parse(xml_score(xml_note(duration=1920), divisions=480))
|
value = TYPE_LENGTHS[kind] * (2 - Fraction(1, 2**dots))
|
||||||
self.assertEqual(a, b)
|
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)
|
||||||
def test_doctype_allowed_but_entities_rejected(self):
|
self.assertEqual(notes(score)[0]['written'], {'type': kind, 'dots': dots})
|
||||||
source = xml_score(xml_note())
|
def test_divisions_do_not_change_music(self):
|
||||||
source = source.replace('<score-partwise', '<!DOCTYPE score-partwise SYSTEM "https://invalid.test/partwise.dtd"><score-partwise', 1)
|
a = parse(xml_score(xml_note(duration=4)))
|
||||||
self.assertEqual(parse(source)['schema_version'], 2)
|
b = parse(xml_score(xml_note(duration=1920), divisions=480))
|
||||||
malicious = xml_score(xml_note()).replace('<score-partwise', '<!DOCTYPE score-partwise [<!ENTITY secret SYSTEM "file:///etc/passwd">]><score-partwise',1)
|
self.assertEqual(a, b)
|
||||||
with self.assertRaises(MusicXMLError):
|
def test_doctype_allowed_but_entities_rejected(self):
|
||||||
parse(malicious)
|
source = xml_score(xml_note())
|
||||||
with self.assertRaises(MusicXMLError):
|
source = source.replace('<score-partwise', '<!DOCTYPE score-partwise SYSTEM "https://invalid.test/partwise.dtd"><score-partwise', 1)
|
||||||
parse('<score-partwise')
|
self.assertEqual(parse(source)['schema_version'], 2)
|
||||||
|
malicious = xml_score(xml_note()).replace('<score-partwise', '<!DOCTYPE score-partwise [<!ENTITY secret SYSTEM "file:///etc/passwd">]><score-partwise', 1)
|
||||||
def test_harmony_inside_sustained_note_remains_independent(self):
|
with self.assertRaises(MusicXMLError):
|
||||||
h = '<harmony><root><root-step>F</root-step></root><kind>major</kind><offset>-2</offset></harmony>'
|
parse(malicious)
|
||||||
score = parse(xml_score(xml_note()+h))
|
with self.assertRaises(MusicXMLError):
|
||||||
self.assertEqual(len(notes(score)), 1)
|
parse('<score-partwise')
|
||||||
self.assertEqual(fraction(score['staves'][0]['measures'][0]['harmonies'][0]['onset']), 2)
|
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>'
|
||||||
def test_partial_chord_ties_and_unequal_tones(self):
|
score = parse(xml_score(xml_note() + h))
|
||||||
source = xml_note(duration=2,kind='half',extra='<tie type="start"/>')
|
self.assertEqual(len(notes(score)), 1)
|
||||||
source += xml_note('E',1,'quarter',prefix='<chord/>')
|
self.assertEqual(fraction(score['staves'][0]['measures'][0]['harmonies'][0]['onset']), 2)
|
||||||
source += xml_note(duration=2,kind='half',extra='<tie type="stop"/>')
|
def test_partial_chord_ties_and_unequal_tones(self):
|
||||||
source += xml_note('G',2,'half',prefix='<chord/>')
|
source = xml_note(duration=2, kind='half', extra='<tie type="start"/>')
|
||||||
result = notes(parse(xml_score(source)))
|
source += xml_note('E', 1, 'quarter', prefix='<chord/>')
|
||||||
self.assertEqual(fraction(result[0]['pitches'][1]['duration']), 1)
|
source += xml_note(duration=2, kind='half', extra='<tie type="stop"/>')
|
||||||
self.assertEqual(result[0]['pitches'][0]['tie_to'], result[1]['pitches'][0]['id'])
|
source += xml_note('G', 2, 'half', prefix='<chord/>')
|
||||||
self.assertNotIn('tie_to', result[0]['pitches'][1])
|
result = notes(parse(xml_score(source)))
|
||||||
|
self.assertEqual(fraction(result[0]['pitches'][1]['duration']), 1)
|
||||||
def test_grace_cue_and_measured_tremolo(self):
|
self.assertEqual(result[0]['pitches'][0]['tie_to'], result[1]['pitches'][0]['id'])
|
||||||
grace = '<note><grace slash="yes"/><pitch><step>D</step><octave>4</octave></pitch><type>16th</type></note>'
|
self.assertNotIn('tie_to', result[0]['pitches'][1])
|
||||||
regular = xml_note(extra='<notations><ornaments><tremolo type="single">3</tremolo></ornaments></notations>', prefix='<cue/>')
|
def test_grace_cue_and_measured_tremolo(self):
|
||||||
result = notes(parse(xml_score(grace+regular)))
|
grace = '<note><grace slash="yes"/><pitch><step>D</step><octave>4</octave></pitch><type>16th</type></note>'
|
||||||
self.assertEqual(fraction(result[0]['duration']), 0)
|
regular = xml_note(extra='<notations><ornaments><tremolo type="single">3</tremolo></ornaments></notations>', prefix='<cue/>')
|
||||||
self.assertTrue(result[0]['grace']['slash'])
|
result = notes(parse(xml_score(grace + regular)))
|
||||||
self.assertTrue(result[1]['cue'])
|
self.assertEqual(fraction(result[0]['duration']), 0)
|
||||||
self.assertEqual(result[1]['tremolo']['marks'], 3)
|
self.assertTrue(result[0]['grace']['slash'])
|
||||||
|
self.assertTrue(result[1]['cue'])
|
||||||
def test_exact_quintuplets(self):
|
self.assertEqual(result[1]['tremolo']['marks'], 3)
|
||||||
content = ''
|
def test_exact_quintuplets(self):
|
||||||
for i in range(5):
|
content = ''
|
||||||
tuplet = '<tuplet type="start"/>' if i==0 else '<tuplet type="stop"/>' if i==4 else ''
|
for i in range(5):
|
||||||
extra = '<time-modification><actual-notes>5</actual-notes><normal-notes>4</normal-notes></time-modification>'
|
tuplet = '<tuplet type="start"/>' if i == 0 else '<tuplet type="stop"/>' if i == 4 else ''
|
||||||
extra += '<notations>'+tuplet+'</notations>'
|
extra = '<time-modification><actual-notes>5</actual-notes><normal-notes>4</normal-notes></time-modification>'
|
||||||
content += xml_note(duration=4, kind='quarter',extra=extra)
|
extra += '<notations>' + tuplet + '</notations>'
|
||||||
result = parse(xml_score(content,divisions=5))
|
content += xml_note(duration=4, kind='quarter', extra=extra)
|
||||||
self.assertEqual(sum(fraction(n['duration']) for n in notes(result)),4)
|
result = parse(xml_score(content, divisions=5))
|
||||||
self.assertTrue(all(fraction(n['duration'])==Fraction(4,5) for n in notes(result)))
|
self.assertEqual(sum(fraction(n['duration']) for n in notes(result)), 4)
|
||||||
self.assertEqual(result['staves'][0]['tuplets'][0]['note_count'],5)
|
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):
|
def test_inconsistent_written_timing_is_rejected(self):
|
||||||
modulation = '<time-modification><actual-notes>3</actual-notes><normal-notes>2</normal-notes></time-modification>'
|
modulation = '<time-modification><actual-notes>3</actual-notes><normal-notes>2</normal-notes></time-modification>'
|
||||||
with self.assertRaisesRegex(MusicXMLError,'disagrees with written duration'):
|
with self.assertRaisesRegex(MusicXMLError, 'disagrees with written duration'):
|
||||||
parse(xml_score(xml_note(extra=modulation)))
|
parse(xml_score(xml_note(extra=modulation)))
|
||||||
with self.assertRaisesRegex(MusicXMLError,'disagrees with written duration'):
|
with self.assertRaisesRegex(MusicXMLError, 'disagrees with written duration'):
|
||||||
parse(xml_score(xml_note(kind='half')))
|
parse(xml_score(xml_note(kind='half')))
|
||||||
|
def test_paired_tremolo_timing(self):
|
||||||
def test_paired_tremolo_timing(self):
|
modulation = '<time-modification><actual-notes>2</actual-notes><normal-notes>1</normal-notes></time-modification>'
|
||||||
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>')
|
||||||
content = ''.join(xml_note(step,2,'whole',extra=modulation+f'<notations><ornaments><tremolo type="{action}">3</tremolo></ornaments></notations>')
|
for step, action in [('C', 'start'), ('G', 'stop')])
|
||||||
for step,action in [('C','start'),('G','stop')])
|
result = notes(parse(xml_score(content)))
|
||||||
result = notes(parse(xml_score(content)))
|
self.assertEqual(result[0]['tremolo']['partner'], result[1]['id'])
|
||||||
self.assertEqual(result[0]['tremolo']['partner'],result[1]['id'])
|
self.assertEqual(fraction(result[0]['duration']), 2)
|
||||||
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>'
|
||||||
def test_mid_measure_clef_and_key_changes(self):
|
result = parse(xml_score(xml_note(duration=2, kind='half') + change + xml_note(duration=2, kind='half')))
|
||||||
change = '<attributes><key><fifths>-3</fifths><mode>minor</mode></key><clef><sign>C</sign><line>3</line></clef></attributes>'
|
attr = result['staves'][0]['measures'][0]['attributes'][0]
|
||||||
result = parse(xml_score(xml_note(duration=2,kind='half')+change+xml_note(duration=2,kind='half')))
|
self.assertEqual(fraction(attr['onset']), 2)
|
||||||
attr = result['staves'][0]['measures'][0]['attributes'][0]
|
self.assertEqual(attr['key']['mode'], 'minor')
|
||||||
self.assertEqual(fraction(attr['onset']),2)
|
self.assertEqual(attr['clef']['sign'], 'C')
|
||||||
self.assertEqual(attr['key']['mode'],'minor')
|
def test_optional_marker_contract(self):
|
||||||
self.assertEqual(attr['clef']['sign'],'C')
|
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>'
|
||||||
def test_optional_marker_contract(self):
|
score = parse(xml_score(marker + h + xml_note()))
|
||||||
marker = '<direction><direction-type><words>vamp:optional-chord</words></direction-type></direction>'
|
self.assertTrue(score['staves'][0]['measures'][0]['harmonies'][0]['chord_optional'])
|
||||||
h = '<harmony><root><root-step>C</root-step></root><kind>major</kind></harmony>'
|
for content in (marker + xml_note(), marker + marker + h + xml_note(), marker.replace('optional-chord', 'unknown') + xml_note()):
|
||||||
score = parse(xml_score(marker+h+xml_note()))
|
with self.assertRaisesRegex(MusicXMLError, 'measure 1'):
|
||||||
self.assertTrue(score['staves'][0]['measures'][0]['harmonies'][0]['chord_optional'])
|
parse(xml_score(content))
|
||||||
for content in (marker+xml_note(),marker+marker+h+xml_note(),marker.replace('optional-chord','unknown')+xml_note()):
|
def test_rejects_musical_content_outside_contract(self):
|
||||||
with self.assertRaisesRegex(MusicXMLError,'measure 1'):
|
extras = ['<notations><slur type="start"/></notations>', '<notations><articulations><accent/></articulations></notations>',
|
||||||
parse(xml_score(content))
|
'<voice>2</voice><staff>2</staff>', '<pitch><unpitched/></pitch>',
|
||||||
|
'<notations><ornaments><trill-mark/></ornaments></notations>']
|
||||||
def test_rejects_musical_content_outside_contract(self):
|
for extra in extras:
|
||||||
extras = ['<notations><slur type="start"/></notations>', '<notations><articulations><accent/></articulations></notations>',
|
with self.subTest(extra=extra), self.assertRaises(MusicXMLError):
|
||||||
'<voice>2</voice><staff>2</staff>', '<pitch><unpitched/></pitch>',
|
parse(xml_score(xml_note(extra=extra)))
|
||||||
'<notations><ornaments><trill-mark/></ornaments></notations>']
|
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 extras:
|
with self.subTest(extra=extra), self.assertRaises(MusicXMLError):
|
||||||
with self.subTest(extra=extra), self.assertRaises(MusicXMLError):
|
parse(xml_score(xml_note() + extra))
|
||||||
parse(xml_score(xml_note(extra=extra)))
|
with self.assertRaises(MusicXMLError):
|
||||||
for extra in ('<barline><repeat direction="backward"/></barline>', '<backup><duration>4</duration></backup>',
|
parse(xml_score(xml_note(alter='0.5')))
|
||||||
'<direction><direction-type><dynamics><ff/></dynamics></direction-type></direction>'):
|
with self.assertRaises(MusicXMLError):
|
||||||
with self.subTest(extra=extra), self.assertRaises(MusicXMLError):
|
parse(xml_score(xml_note(extra='<tie type="stop"/>')))
|
||||||
parse(xml_score(xml_note()+extra))
|
with self.assertRaises(MusicXMLError):
|
||||||
with self.assertRaises(MusicXMLError):
|
parse(xml_score(xml_note(duration=5)))
|
||||||
parse(xml_score(xml_note(alter='0.5')))
|
def test_b_sharp_c_flat_preserve_octave(self):
|
||||||
with self.assertRaises(MusicXMLError):
|
b = notes(parse(xml_score(xml_note('B', alter=1))))[0]['pitches'][0]
|
||||||
parse(xml_score(xml_note(extra='<tie type="stop"/>')))
|
c = notes(parse(xml_score(xml_note('C', alter=-1))))[0]['pitches'][0]
|
||||||
with self.assertRaises(MusicXMLError):
|
self.assertEqual(pitch_number(b), 72)
|
||||||
parse(xml_score(xml_note(duration=5)))
|
self.assertEqual(pitch_number(c), 59)
|
||||||
|
def test_musicxml_degree_alterations_are_relative(self):
|
||||||
def test_b_sharp_c_flat_preserve_octave(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>'
|
||||||
b = notes(parse(xml_score(xml_note('B',alter=1))))[0]['pitches'][0]
|
from generator.harmony import degrees
|
||||||
c = notes(parse(xml_score(xml_note('C',alter=-1))))[0]['pitches'][0]
|
score = parse(xml_score(h + xml_note()))
|
||||||
self.assertEqual(pitch_number(b),72)
|
self.assertEqual(degrees(score['staves'][0]['measures'][0]['harmonies'][0])[3], 0)
|
||||||
self.assertEqual(pitch_number(c),59)
|
h = h.replace('<kind>minor</kind>', '<kind text="maj7">minor</kind>')
|
||||||
|
with self.assertRaisesRegex(MusicXMLError, 'contradictory'):
|
||||||
def test_musicxml_degree_alterations_are_relative(self):
|
parse(xml_score(h + xml_note()))
|
||||||
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>'
|
def test_nested_and_cross_bar_tuplets(self):
|
||||||
from generator.harmony import degrees
|
modulation = '<time-modification><actual-notes>9</actual-notes><normal-notes>4</normal-notes></time-modification>'
|
||||||
score = parse(xml_score(h+xml_note()))
|
def tuplet(number, action):
|
||||||
self.assertEqual(degrees(score['staves'][0]['measures'][0]['harmonies'][0])[3],0)
|
details = '<tuplet-actual><tuplet-number>3</tuplet-number></tuplet-actual><tuplet-normal><tuplet-number>2</tuplet-number></tuplet-normal>' if action == 'start' else ''
|
||||||
h = h.replace('<kind>minor</kind>','<kind text="maj7">minor</kind>')
|
return f'<tuplet number="{number}" type="{action}">{details}</tuplet>'
|
||||||
with self.assertRaisesRegex(MusicXMLError,'contradictory'):
|
first = xml_note(duration=4, kind='quarter', extra=modulation + '<notations>' + tuplet(1, 'start') + tuplet(2, 'start') + '</notations>')
|
||||||
parse(xml_score(h+xml_note()))
|
last = xml_note(duration=4, kind='quarter', extra=modulation + '<notations>' + tuplet(2, 'stop') + tuplet(1, 'stop') + '</notations>')
|
||||||
|
text = xml_score(first, divisions=9, implicit=True)
|
||||||
def test_nested_and_cross_bar_tuplets(self):
|
text = text.replace('</measure></part>', '</measure><measure number="2" implicit="yes">' + last + '</measure></part>')
|
||||||
modulation = '<time-modification><actual-notes>9</actual-notes><normal-notes>4</normal-notes></time-modification>'
|
result = parse(text)
|
||||||
def tuplet(number,action):
|
spans = result['staves'][0]['tuplets']
|
||||||
details = '<tuplet-actual><tuplet-number>3</tuplet-number></tuplet-actual><tuplet-normal><tuplet-number>2</tuplet-number></tuplet-normal>' if action=='start' else ''
|
self.assertEqual([t['depth'] for t in spans], [0, 1])
|
||||||
return f'<tuplet number="{number}" type="{action}">{details}</tuplet>'
|
self.assertEqual(fraction(notes(result, 1)[0]['duration']), Fraction(4, 9))
|
||||||
first = xml_note(duration=4,kind='quarter',extra=modulation+'<notations>'+tuplet(1,'start')+tuplet(2,'start')+'</notations>')
|
self.assertTrue(all(t['end'] == notes(result, 1)[0]['id'] for t in spans))
|
||||||
last = xml_note(duration=4,kind='quarter',extra=modulation+'<notations>'+tuplet(2,'stop')+tuplet(1,'stop')+'</notations>')
|
|
||||||
text = xml_score(first,divisions=9,implicit=True)
|
|
||||||
text = text.replace('</measure></part>', '</measure><measure number="2" implicit="yes">'+last+'</measure></part>')
|
|
||||||
result = parse(text)
|
|
||||||
spans = result['staves'][0]['tuplets']
|
|
||||||
self.assertEqual([t['depth'] for t in spans],[0,1])
|
|
||||||
self.assertEqual(fraction(notes(result,1)[0]['duration']),Fraction(4,9))
|
|
||||||
self.assertTrue(all(t['end']==notes(result,1)[0]['id'] for t in spans))
|
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ from generator.__main__ import copy_runtime_fonts
|
|||||||
from tools import generate_pico_font
|
from tools import generate_pico_font
|
||||||
from tools import generate_score_metrics
|
from tools import generate_score_metrics
|
||||||
|
|
||||||
|
|
||||||
class FontAssetsTests(unittest.TestCase):
|
class FontAssetsTests(unittest.TestCase):
|
||||||
def test_score_metrics_use_fixed_width_codepoints(self):
|
def test_score_metrics_use_fixed_width_codepoints(self):
|
||||||
root = Path(__file__).resolve().parents[1]
|
root = Path(__file__).resolve().parents[1]
|
||||||
@@ -21,12 +20,10 @@ class FontAssetsTests(unittest.TestCase):
|
|||||||
content = output.read_text(encoding="ascii")
|
content = output.read_text(encoding="ascii")
|
||||||
self.assertIn("#include <cstdint>", content)
|
self.assertIn("#include <cstdint>", content)
|
||||||
self.assertIn("std::int32_t codepoint", content)
|
self.assertIn("std::int32_t codepoint", content)
|
||||||
|
|
||||||
def test_font_generator_requires_kerning_backend(self):
|
def test_font_generator_requires_kerning_backend(self):
|
||||||
with unittest.mock.patch.object(generate_pico_font.features, "check_feature", return_value=False):
|
with unittest.mock.patch.object(generate_pico_font.features, "check_feature", return_value=False):
|
||||||
with self.assertRaisesRegex(RuntimeError, "Raqm"):
|
with self.assertRaisesRegex(RuntimeError, "Raqm"):
|
||||||
generate_pico_font.generate()
|
generate_pico_font.generate()
|
||||||
|
|
||||||
def test_runtime_fonts_exclude_unused_assets(self):
|
def test_runtime_fonts_exclude_unused_assets(self):
|
||||||
root = Path(__file__).resolve().parents[1]
|
root = Path(__file__).resolve().parents[1]
|
||||||
with tempfile.TemporaryDirectory() as directory:
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
@@ -34,7 +31,6 @@ class FontAssetsTests(unittest.TestCase):
|
|||||||
copy_runtime_fonts(root / "package/assets", output)
|
copy_runtime_fonts(root / "package/assets", output)
|
||||||
files = {path.name for path in (output / "fonts/petaluma").iterdir()}
|
files = {path.name for path in (output / "fonts/petaluma").iterdir()}
|
||||||
self.assertEqual(files, {"Petaluma.otf", "PetalumaScript.otf", "OFL.txt"})
|
self.assertEqual(files, {"Petaluma.otf", "PetalumaScript.otf", "OFL.txt"})
|
||||||
|
|
||||||
def test_font_generator_uses_explicit_build_output(self):
|
def test_font_generator_uses_explicit_build_output(self):
|
||||||
root = Path(__file__).resolve().parents[1]
|
root = Path(__file__).resolve().parents[1]
|
||||||
with tempfile.TemporaryDirectory() as directory:
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ from pathlib import Path
|
|||||||
|
|
||||||
from PIL import Image, ImageDraw, ImageFont, features
|
from PIL import Image, ImageDraw, ImageFont, features
|
||||||
|
|
||||||
|
|
||||||
REPO = Path(__file__).resolve().parents[1]
|
REPO = Path(__file__).resolve().parents[1]
|
||||||
FONT_PATH = REPO / "package/assets/fonts/petaluma/PetalumaScript.otf"
|
FONT_PATH = REPO / "package/assets/fonts/petaluma/PetalumaScript.otf"
|
||||||
FONT_SIZE = 32
|
FONT_SIZE = 32
|
||||||
@@ -15,7 +14,6 @@ FONT_MAX_WIDTH = 28
|
|||||||
FIRST_CODEPOINT = 32
|
FIRST_CODEPOINT = 32
|
||||||
LAST_CODEPOINT = 126
|
LAST_CODEPOINT = 126
|
||||||
|
|
||||||
|
|
||||||
def cpp_char_literal(value: str) -> str:
|
def cpp_char_literal(value: str) -> str:
|
||||||
if value == "'":
|
if value == "'":
|
||||||
return "'\\''"
|
return "'\\''"
|
||||||
@@ -23,30 +21,26 @@ def cpp_char_literal(value: str) -> str:
|
|||||||
return "'\\\\'"
|
return "'\\\\'"
|
||||||
return f"'{value}'"
|
return f"'{value}'"
|
||||||
|
|
||||||
|
|
||||||
def render_glyph(font: ImageFont.FreeTypeFont, value: str, y_offset: int) -> tuple[list[int], int, int]:
|
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)
|
image = Image.new("L", (FONT_MAX_WIDTH, FONT_HEIGHT), 0)
|
||||||
draw = ImageDraw.Draw(image)
|
draw = ImageDraw.Draw(image)
|
||||||
bbox = font.getbbox(value)
|
bbox = font.getbbox(value)
|
||||||
x = max(0, -bbox[0])
|
x = max(0, -bbox[0])
|
||||||
draw.text((x, y_offset), value, font=font, fill=255)
|
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()]
|
pixels = [min(15, max(0, round(pixel / 17))) for pixel in image.get_flattened_data()]
|
||||||
visible = image.getbbox()
|
visible = image.getbbox()
|
||||||
width = 0 if visible is None else min(FONT_MAX_WIDTH, visible[2])
|
width = 0 if visible is None else min(FONT_MAX_WIDTH, visible[2])
|
||||||
advance = min(255, max(0, round(font.getlength(value))))
|
advance = min(255, max(0, round(font.getlength(value))))
|
||||||
return pixels, width, advance
|
return pixels, width, advance
|
||||||
|
|
||||||
|
|
||||||
def render_alpha_rows(values: list[int]) -> list[str]:
|
def render_alpha_rows(values: list[int]) -> list[str]:
|
||||||
lines = []
|
lines = []
|
||||||
for row in range(FONT_HEIGHT):
|
for row in range(FONT_HEIGHT):
|
||||||
start = row * FONT_MAX_WIDTH
|
start = row * FONT_MAX_WIDTH
|
||||||
row_values = values[start : start + FONT_MAX_WIDTH]
|
row_values = values[start: start + FONT_MAX_WIDTH]
|
||||||
lines.append(" " + ", ".join(f"0x{value:02x}" for value in row_values) + ",")
|
lines.append(" " + ", ".join(f"0x{value:02x}" for value in row_values) + ",")
|
||||||
return lines
|
return lines
|
||||||
|
|
||||||
|
|
||||||
def pair_adjustments(font: ImageFont.FreeTypeFont) -> list[tuple[int, int]]:
|
def pair_adjustments(font: ImageFont.FreeTypeFont) -> list[tuple[int, int]]:
|
||||||
chars = [chr(value) for value in range(FIRST_CODEPOINT, LAST_CODEPOINT + 1)]
|
chars = [chr(value) for value in range(FIRST_CODEPOINT, LAST_CODEPOINT + 1)]
|
||||||
adjustments = []
|
adjustments = []
|
||||||
@@ -60,7 +54,6 @@ def pair_adjustments(font: ImageFont.FreeTypeFont) -> list[tuple[int, int]]:
|
|||||||
adjustments.append((key, int(adjustment)))
|
adjustments.append((key, int(adjustment)))
|
||||||
return sorted(adjustments)
|
return sorted(adjustments)
|
||||||
|
|
||||||
|
|
||||||
def generate() -> str:
|
def generate() -> str:
|
||||||
if not features.check_feature("raqm"):
|
if not features.check_feature("raqm"):
|
||||||
raise RuntimeError("Font generation requires Pillow with Raqm support; install libfribidi0 on Debian/Ubuntu.")
|
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))
|
min_top = min(font.getbbox(chr(value))[1] for value in range(FIRST_CODEPOINT, LAST_CODEPOINT + 1))
|
||||||
y_offset = max(0, -min_top)
|
y_offset = max(0, -min_top)
|
||||||
adjustments = pair_adjustments(font)
|
adjustments = pair_adjustments(font)
|
||||||
|
|
||||||
lines = [
|
lines = [
|
||||||
"#include <vamp_style/font.hpp>",
|
"#include <vamp_style/font.hpp>",
|
||||||
"",
|
"",
|
||||||
@@ -112,7 +104,6 @@ def generate() -> str:
|
|||||||
" switch (value) {",
|
" switch (value) {",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
for codepoint in range(FIRST_CODEPOINT, LAST_CODEPOINT + 1):
|
for codepoint in range(FIRST_CODEPOINT, LAST_CODEPOINT + 1):
|
||||||
value = chr(codepoint)
|
value = chr(codepoint)
|
||||||
alpha, width, advance = render_glyph(font, value, y_offset)
|
alpha, width, advance = render_glyph(font, value, y_offset)
|
||||||
@@ -132,7 +123,6 @@ def generate() -> str:
|
|||||||
" };",
|
" };",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
lines.extend(
|
lines.extend(
|
||||||
[
|
[
|
||||||
" default:",
|
" default:",
|
||||||
@@ -171,13 +161,11 @@ def generate() -> str:
|
|||||||
)
|
)
|
||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
def main() -> int:
|
||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
parser.add_argument("--output", type=Path, required=True)
|
parser.add_argument("--output", type=Path, required=True)
|
||||||
parser.add_argument("--check", action="store_true")
|
parser.add_argument("--check", action="store_true")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
generated = generate()
|
generated = generate()
|
||||||
if args.check:
|
if args.check:
|
||||||
if not args.output.is_file():
|
if not args.output.is_file():
|
||||||
@@ -195,12 +183,10 @@ def main() -> int:
|
|||||||
)
|
)
|
||||||
print("\n".join(diff))
|
print("\n".join(diff))
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
args.output.parent.mkdir(parents=True, exist_ok=True)
|
args.output.parent.mkdir(parents=True, exist_ok=True)
|
||||||
args.output.write_text(generated, encoding="utf-8")
|
args.output.write_text(generated, encoding="utf-8")
|
||||||
print(f"generated {args.output}")
|
print(f"generated {args.output}")
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
raise SystemExit(main())
|
raise SystemExit(main())
|
||||||
|
|||||||
@@ -5,34 +5,32 @@ from pathlib import Path
|
|||||||
from fontTools.pens.boundsPen import BoundsPen
|
from fontTools.pens.boundsPen import BoundsPen
|
||||||
from fontTools.ttLib import TTFont
|
from fontTools.ttLib import TTFont
|
||||||
|
|
||||||
|
|
||||||
def generate(font_path, output):
|
def generate(font_path, output):
|
||||||
with TTFont(font_path) as font:
|
with TTFont(font_path) as font:
|
||||||
glyphs, cmap = font.getGlyphSet(), font.getBestCmap()
|
glyphs, cmap = font.getGlyphSet(), font.getBestCmap()
|
||||||
units = font['head'].unitsPerEm / 4
|
units = font['head'].unitsPerEm / 4
|
||||||
ranges = [(0xE050, 0xE06F), (0xE080, 0xE09F), (0xE0A0, 0xE0A5),
|
ranges = [(0xE050, 0xE06F), (0xE080, 0xE09F), (0xE0A0, 0xE0A5),
|
||||||
(0xE101, 0xE102), (0xE1E7, 0xE1E8), (0xE240, 0xE250),
|
(0xE101, 0xE102), (0xE1E7, 0xE1E8), (0xE240, 0xE250),
|
||||||
(0xE260, 0xE270), (0xE4E0, 0xE4EE)]
|
(0xE260, 0xE270), (0xE4E0, 0xE4EE)]
|
||||||
records = []
|
records = []
|
||||||
for start, end in ranges:
|
for start, end in ranges:
|
||||||
for codepoint in range(start, end):
|
for codepoint in range(start, end):
|
||||||
if codepoint not in cmap:
|
if codepoint not in cmap:
|
||||||
continue
|
continue
|
||||||
pen = BoundsPen(glyphs)
|
pen = BoundsPen(glyphs)
|
||||||
glyphs[cmap[codepoint]].draw(pen)
|
glyphs[cmap[codepoint]].draw(pen)
|
||||||
if pen.bounds:
|
if pen.bounds:
|
||||||
box = ', '.join(f'{v / units:.8f}F' for v in pen.bounds)
|
box = ', '.join(f'{v / units:.8f}F' for v in pen.bounds)
|
||||||
records.append(f' {{{codepoint}, {{{box}}}}},')
|
records.append(f' {{{codepoint}, {{{box}}}}},')
|
||||||
output.parent.mkdir(parents=True, exist_ok=True)
|
output.parent.mkdir(parents=True, exist_ok=True)
|
||||||
output.write_text('#pragma once\n#include <array>\n#include <cstdint>\nnamespace vamp_zero::font_data {\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'
|
'struct glyph { std::int32_t codepoint; std::array<float, 4> bounds; };\n'
|
||||||
f'inline constexpr std::array<glyph, {len(records)}> glyphs = {{{{\n'
|
f'inline constexpr std::array<glyph, {len(records)}> glyphs = {{{{\n'
|
||||||
+ '\n'.join(records) + '\n}};\n}\n', encoding='ascii')
|
+ '\n'.join(records) + '\n}};\n}\n', encoding='ascii')
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
parser = argparse.ArgumentParser(description=__doc__)
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
parser.add_argument('--font', type=Path, required=True)
|
parser.add_argument('--font', type=Path, required=True)
|
||||||
parser.add_argument('--output', type=Path, required=True)
|
parser.add_argument('--output', type=Path, required=True)
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
generate(args.font, args.output)
|
generate(args.font, args.output)
|
||||||
|
|||||||
Reference in New Issue
Block a user