From 1be98e14aa7a27da5f3c8be228398f0835026147 Mon Sep 17 00:00:00 2001 From: khodowany Date: Thu, 24 Sep 2026 06:20:37 -0500 Subject: [PATCH] Update --- README.md | 14 ++- SETUP.md | 2 +- TECHNICAL_GUIDE.md | 30 ++--- cmake/runtime_assets.cmake | 4 +- cmake/zero.cmake | 2 +- generator/__main__.py | 6 +- generator/{catalogue.py => catalog.py} | 14 +-- generator/pitch.py | 6 +- include/vamp_zero/audio.hpp | 2 +- include/vamp_zero/config.hpp | 2 +- include/vamp_zero/portal.hpp | 2 +- include/vamp_zero/score_loader.hpp | 2 +- include/vamp_zero/types.hpp | 6 +- package/config/vamp.development.json | 2 +- package/config/vamp.example.json | 2 +- src/vamp_zero/app.cpp | 8 +- src/vamp_zero/database.cpp | 26 ++--- src/vamp_zero/page_control.cpp | 4 +- src/vamp_zero/portal.cpp | 2 +- src/vamp_zero/score_loader.cpp | 4 +- src/vamp_zero/types.cpp | 4 +- src/vamp_zero/ui.cpp | 2 +- tests/CMakeLists.txt | 2 +- ...alogue_fixture.hpp => catalog_fixture.hpp} | 4 +- tests/test.cpp | 23 ++-- tests/{test_catalogue.py => test_catalog.py} | 2 +- ...e_database.py => test_catalog_database.py} | 44 ++++---- ...e_runtime.cpp => test_catalog_runtime.cpp} | 106 +++++++++--------- tests/test_health.cpp | 14 +-- tests/test_music_model.py | 10 +- tests/test_musicxml.py | 2 +- tests/test_parts.py | 2 +- tests/test_playback.cpp | 6 +- tests/test_score_bundle.py | 2 +- tests/test_score_loader.cpp | 12 +- 35 files changed, 191 insertions(+), 184 deletions(-) rename generator/{catalogue.py => catalog.py} (91%) rename tests/{catalogue_fixture.hpp => catalog_fixture.hpp} (82%) rename tests/{test_catalogue.py => test_catalog.py} (98%) rename tests/{test_catalogue_database.py => test_catalog_database.py} (82%) rename tests/{test_catalogue_runtime.cpp => test_catalog_runtime.cpp} (84%) diff --git a/README.md b/README.md index 031ae29..8d06bb9 100644 --- a/README.md +++ b/README.md @@ -5,9 +5,9 @@ Product Vamp is a two-device sheet-music appliance. - Raspberry Pi Zero 2 W hosts the control Wi-Fi AP, runs the display/control service in Docker, and drives a Waveshare Zero-DISP-7A 1024x600 touch display. - Raspberry Pi Pico 2 W runs UF2 firmware for a Waveshare 2-inch capacitive touch LCD and sends menu/page commands over the Zero-hosted AP. -Python builds the complete SQLite catalogue with the score assets. The Zero opens that packaged catalogue read-only through Noto, without creating a database, migrating schemas, or importing JSON at startup. Runtime navigation and rendering use Ligo for the HTTP API, Luma for touch UI rendering, and Modulus for shared build/runtime utilities. +Python builds the complete SQLite catalog with the score assets. The Zero opens that packaged catalog read-only through Noto, without creating a database, migrating schemas, or importing JSON at startup. Runtime navigation and rendering use Ligo for the HTTP API, Luma for touch UI rendering, and Modulus for shared build/runtime utilities. -The catalogue stores shared, immutable menu metadata. Complete score bundles stay in SQLite and are loaded individually when selected; snapshots and Pico heartbeats do not copy the catalogue's musical JSON. +The catalog stores shared, immutable menu metadata. Complete score bundles stay in SQLite and are loaded individually when selected; snapshots and Pico heartbeats do not copy the catalog's musical JSON. Pico controls: @@ -31,7 +31,7 @@ python3 modulus.py configure --target x86_64_gcc12_release python3 modulus.py build --target x86_64_gcc12_release ``` -`python3 modulus.py run` uses `package/config/vamp.development.json` for a headless readiness check against native build assets. `vamp-zero --once` checks the packaged database and referenced page/audio assets, checks runtime writes and the server bind, then exits without opening audio or display hardware. Missing or incompatible catalogues require rebuilding or reinstalling; they are never repaired at runtime. `--check-config` only validates configuration; `--health-check` probes the running service's `/api/health` endpoint without modifying files. +`python3 modulus.py run` uses `package/config/vamp.development.json` for a headless readiness check against native build assets. `vamp-zero --once` checks the packaged database and referenced page/audio assets, checks runtime writes and the server bind, then exits without opening audio or display hardware. Missing or incompatible catalogs require rebuilding or reinstalling; they are never repaired at runtime. `--check-config` only validates configuration; `--health-check` probes the running service's `/api/health` endpoint without modifying files. Configure/build can read public packages anonymously from the khodo Conan registry. @@ -64,7 +64,7 @@ build/packages/product-vamp-latest-arm_64_gcc12_release.tar.gz build/packages/product-vamp-latest-arm_64_gcc12_release.tar.gz.sha256 ``` -The tarball contains the Zero Docker archive, Pi install scripts, generated catalogue database, pages, audio, fonts, and Pico UF2 firmware. Complete score JSON is stored inside `catalogue.db`; individual JSON files remain build intermediates and are not deployed. +The tarball contains the Zero Docker archive, Pi install scripts, generated catalog database, pages, audio, fonts, and Pico UF2 firmware. Complete score JSON is stored inside `catalog.db`; individual JSON files remain build intermediates and are not deployed. Run `sudo zero/install-zero.sh` from an extracted bundle on the Zero. Every installation resets all Product Vamp data and settings, including the control Wi-Fi profile, to the new bundle defaults. No previous database rows or configuration are migrated. Unrelated OS data and networks are untouched. Installing through the control Wi-Fi can disconnect your session; use a local console or an independent connection. @@ -91,9 +91,9 @@ python3 -m generator --data-dir data --output-dir /tmp/product-vamp-generated -- The `--check` path compares generation invariants against `data/expected/performance_summary.json`. The pinned build-only dependencies are in `generator/requirements.txt`. Configure/build automatically refresh the project build image when this file changes; direct builds must install it in their Python environment. -CMake builds engrave with two Python workers and share a disposable page cache under `build/build_common/engraving-cache`. Native and ARM packaging reuse unchanged pages; CI also retains this cache between runs. Music, playback, and catalogue validation still run for every generation. Set `VAMP_ENGRAVING_JOBS` (1-16) to adjust concurrency, or set `VAMP_ENGRAVING_CACHE_DIR` to an empty CMake value to disable caching. Direct generator runs can opt in with `--jobs 2 --engraving-cache build/build_common/engraving-cache --progress`. +CMake builds engrave with two Python workers and share a disposable page cache under `build/build_common/engraving-cache`. Native and ARM packaging reuse unchanged pages; CI also retains this cache between runs. Music, playback, and catalog validation still run for every generation. Set `VAMP_ENGRAVING_JOBS` (1-16) to adjust concurrency, or set `VAMP_ENGRAVING_CACHE_DIR` to an empty CMake value to disable caching. Direct generator runs can opt in with `--jobs 2 --engraving-cache build/build_common/engraving-cache --progress`. -The build-time pipeline is Soundslice/MuseScore -> `.xml` -> musical model -> part generation -> schema-v3 JSON, engraved PNG pages, generated-tone audio, and a read-only catalogue database. Sources in `data/songs/` retain the seven-section performance form. Numbered sources in `data/practice/` (`000.xml` through `366.xml`, as authored) produce one pass each of **acoustic accompaniment**, **electric melody**, and **vocal melody**. Both categories use the authored musical key, with no key variants or key-selection menu. Duplicate titles never merge exercises. +The build-time pipeline is Soundslice/MuseScore -> `.xml` -> musical model -> part generation -> schema-v3 JSON, engraved PNG pages, generated-tone audio, and a read-only catalog database. Sources in `data/songs/` retain the seven-section performance form. Numbered sources in `data/practice/` (`000.xml` through `366.xml`, as authored) produce one pass each of **acoustic accompaniment**, **electric melody**, and **vocal melody**. Both categories use the authored musical key, with no key variants or key-selection menu. Duplicate titles never merge exercises. Guitar parts share a theory-based voicing and proximity search over standard tuning, frets 0-20, and a maximum four-fret span excluding open strings. They render standard notation plus generated TAB. Practice sources provide a monophonic melody and optional chord symbols; the vocal part adds deterministic scat syllables without changing rhythm or written register. Acoustic, electric, and vocal parts all sound one octave below their notation, for both songs and practice. Explicit MusicXML instrument transposition is honored once; an unplayable passage fails rather than being shifted to fit. @@ -103,6 +103,8 @@ The main menu's **Playback** toggle plays the selected part once at the menu BPM 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. +Use American English in project-owned identifiers, filenames, messages, and documentation, such as `catalog`, `color`, and `canceled`. Preserve authored music and external API names. + 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. Build-time engraving is implemented in Python in `generator/engraving`, using the bundled Petaluma OTF fonts and SMuFL metadata. MuseScore is an optional authoring tool, not a build dependency or rendering backend. The managed image and CI install the pinned Python requirements automatically; direct host builds use `pip install -r generator/requirements.txt`. Exact musical timing is retained through layout, and dense measures can continue across systems without shrinking the notation. Missing glyphs, unrepresentable page geometry, and failed notation audits stop generation before replacing existing assets. diff --git a/SETUP.md b/SETUP.md index 5eb8b57..801d634 100644 --- a/SETUP.md +++ b/SETUP.md @@ -31,7 +31,7 @@ Defaults: For API-only quick checks, set `runtime.headless` to `true` or run `vamp-zero --once` from a development build. -Score inputs are tracked under `data/songs/` and `data/practice/`. The build/package step generates acoustic, electric, and vocal parts, engraved pages, audio, and the read-only catalogue database. Installation replaces these assets together; no database migration or runtime score import is needed. +Score inputs are tracked under `data/songs/` and `data/practice/`. The build/package step generates acoustic, electric, and vocal parts, engraved pages, audio, and the read-only catalog database. Installation replaces these assets together; no database migration or runtime score import is needed. ## Raspberry Pi Pico 2 W diff --git a/TECHNICAL_GUIDE.md b/TECHNICAL_GUIDE.md index d1c715e..77bc51c 100644 --- a/TECHNICAL_GUIDE.md +++ b/TECHNICAL_GUIDE.md @@ -10,7 +10,7 @@ Product Vamp follows the Modulus product layout used by the khodo repos: - `cmake/pico.cmake` and `cmake/zero.cmake`: device-specific build definitions selected by the root `CMakeLists.txt`. - `include/vamp_style.hpp`, `include/vamp_style/font.hpp`, and `src/vamp_style.cpp`: shared color palette and Petaluma font interfaces. Bitmap font data is generated in the build directory. - `data`: authored MusicXML for performance songs and practice exercises, plus the generator regression summary. -- `generator`: build-time Python part generation, guitar placement, scat lyrics, engraving, audio compilation, and catalogue construction. +- `generator`: build-time Python part generation, guitar placement, scat lyrics, engraving, audio compilation, and catalog construction. - `package`: generic bundle inputs, scripts, config, static font assets, and device install files. ## Zero Runtime @@ -35,17 +35,17 @@ The AP setup pins NetworkManager to 2.4 GHz channel 6 with WPA2/RSN and CCMP cip On Raspberry Pi OS Lite, the installed display service owns `tty1`, conflicts with `getty@tty1.service`, and starts the Zero container through `product-vamp-start-zero`. The wrapper switches to the display VT when `chvt` is available and runs `vamp-zero` inside the container through `setsid --wait --ctty` with `/dev/tty1` as stdin, giving raylib's DRM backend a real virtual terminal while keeping Docker attached to the long-running display process. SSH remains the normal admin path and `tty2` is enabled as the fallback local console. -At startup the Zero validates the packaged `catalogue.db` application identifier and schema version, opens it read-only through Noto, and loads menu metadata. It never creates or migrates a database, scans score JSON, or repairs metadata. Performance navigation is Song -> Part -> Score; practice is Exercise -> Part -> Score. Both offer Acoustic, Electric, and Vocal, without key selection. Musical interpretation, spacing, beaming, ties, accidentals, and pagination happen on the build host. The Zero displays engraved PNGs and retains at most two page textures. +At startup the Zero validates the packaged `catalog.db` application identifier and schema version, opens it read-only through Noto, and loads menu metadata. It never creates or migrates a database, scans score JSON, or repairs metadata. Performance navigation is Song -> Part -> Score; practice is Exercise -> Part -> Score. Both offer Acoustic, Electric, and Vocal, without key selection. Musical interpretation, spacing, beaming, ties, accidentals, and pagination happen on the build host. The Zero displays engraved PNGs and retains at most two page textures. -Runtime catalogues contain immutable metadata shared by navigation, the HTTP service, and the UI. Song groups reference canonical records by index; practice is a flat, numerically ordered exercise list. SQLite retains complete bundles, while cached measure/page counts let menu queries avoid reading musical JSON. Python constructs the complete database before deployment; metadata and embedded JSON are published together. There are no custom rows, runtime imports, metadata-repair triggers, or persisted page-state tables. Selecting a score loads only that bundle asynchronously, discarding stale results after navigation; the view keeps at most two page textures. +Runtime catalogs contain immutable metadata shared by navigation, the HTTP service, and the UI. Song groups reference canonical records by index; practice is a flat, numerically ordered exercise list. SQLite retains complete bundles, while cached measure/page counts let menu queries avoid reading musical JSON. Python constructs the complete database before deployment; metadata and embedded JSON are published together. There are no custom rows, runtime imports, metadata-repair triggers, or persisted page-state tables. Selecting a score loads only that bundle asynchronously, discarding stale results after navigation; the view keeps at most two page textures. -Practice exercise IDs are the three-digit source stems, including `000` when present. Part IDs append the part name, such as `001-electric`. Duplicate titles and identical placeholder music remain independent exercises. Catalogue schema 3 stores exercise identity and part metadata without practice categories, lesson groups, or key variants. Every exercise must have all three parts. Older catalogues must be rebuilt or reinstalled, not migrated. +Practice exercise IDs are the three-digit source stems, including `000` when present. Part IDs append the part name, such as `001-electric`. Duplicate titles and identical placeholder music remain independent exercises. Catalog schema 3 stores exercise identity and part metadata without practice categories, lesson groups, or key variants. Every exercise must have all three parts. Older catalogs must be rebuilt or reinstalled, not migrated. -`--once` (also `runtime.exit_after_startup`) is a headless readiness check: it checks database integrity and validates the catalogue and referenced page assets, checks runtime-directory writes and the configured server bind, and exits without opening audio/display devices or publishing a readiness heartbeat. Page checks include containment, checksums, CPU-only PNG decoding, and exact 976x484 dimensions. The configured heartbeat destination must be absent or a regular file, never a directory, symlink, or special file. Runtime writes and readiness probes use exclusive sibling temporary files; readiness leaves an existing heartbeat unchanged and removes its probes. `--health-check` instead probes the running service's read-only `/api/health` endpoint using the configured timeout. A stopped/unresponsive service or an invalid response fails the check; it does not create or repair files. `--check-config` remains configuration-only. +`--once` (also `runtime.exit_after_startup`) is a headless readiness check: it checks database integrity and validates the catalog and referenced page assets, checks runtime-directory writes and the configured server bind, and exits without opening audio/display devices or publishing a readiness heartbeat. Page checks include containment, checksums, CPU-only PNG decoding, and exact 976x484 dimensions. The configured heartbeat destination must be absent or a regular file, never a directory, symlink, or special file. Runtime writes and readiness probes use exclusive sibling temporary files; readiness leaves an existing heartbeat unchanged and removes its probes. `--health-check` instead probes the running service's read-only `/api/health` endpoint using the configured timeout. A stopped/unresponsive service or an invalid response fails the check; it does not create or repair files. `--check-config` remains configuration-only. Pico networking progresses without blocking touch sampling. Navigation commands use a bounded 16-entry FIFO ahead of background status polls; a full queue reports busy. Commands are not automatically retried because a timed-out request may already have executed. Background polls wait one second after completion before trying again. -Zero and Pico share one gesture state machine: a second release within 220 ms of the first release produces previous, a hold of at least 700 ms produces select once, and other taps produce next. A late second release preserves both single taps; holding consumes a pending first tap. Zero schedules pending-tap deadlines without polling and discards cancelled or obsolete timer callbacks. +Zero and Pico share one gesture state machine: a second release within 220 ms of the first release produces previous, a hold of at least 700 ms produces select once, and other taps produce next. A late second release preserves both single taps; holding consumes a pending first tap. Zero schedules pending-tap deadlines without polling and discards canceled or obsolete timer callbacks. ## Score Generation @@ -53,7 +53,7 @@ The build runs `python3 -m generator` against `data/` and writes build-only asse Practice sources live directly in `data/practice/` with three-digit filenames, currently `001.xml` through `366.xml`; `000.xml` is also valid. For example, `data/practice/001.xml` generates `practice/001/{acoustic,electric,vocal}.json` and corresponding page directories. Songs stay in `data/songs/`. The filename number determines exercise identity and ordering; MusicXML titles determine labels, falling back to the number. Root-level XML, other practice filenames, and nested practice directories are rejected. Adding, renaming, or removing a source requires no registry edit. The current 366 exercises produce 1,098 practice parts, plus six performance parts. -The pipeline is Soundslice/MuseScore -> uncompressed `.xml` -> musical model -> part generation -> semantic JSON, audio, and engraved pages -> packaged SQLite catalogue. Export MusicXML 3.1 or 4.0, `score-partwise`, with one part and one staff. The musical model retains independent voices and per-tone durations/ties; automatic practice generation additionally requires a single monophonic melody and optional structured harmonies. Packaged music describes each generated part, not an archival copy of the XML. +The pipeline is Soundslice/MuseScore -> uncompressed `.xml` -> musical model -> part generation -> semantic JSON, audio, and engraved pages -> packaged SQLite catalog. Export MusicXML 3.1 or 4.0, `score-partwise`, with one part and one staff. The musical model retains independent voices and per-tone durations/ties; automatic practice generation additionally requires a single monophonic melody and optional structured harmonies. Packaged music describes each generated part, not an archival copy of the XML. ### MusicXML Contract @@ -120,7 +120,7 @@ Measure durations, event onsets/durations, harmony offsets, span times, and pitc Practice acoustic accompaniment is derived from structured harmony roots, kinds, degrees, inversions, and slash basses using the shared accompaniment rhythm. A missing harmony or N.C. produces rests, not guessed chords. Electric retains the exact melody, spelling, rhythm, ties, and tuplets; vocal retains those same authored notes and receives scat lyrics. Song vocal lyrics are unchanged. Practice optional-turnaround harmonies are played once, without creating extra sections. -Accompaniment attacks at each bar start and authored harmony onset, sustaining until the next harmony onset or bar end. Harmony carries into subsequent bars until changed or explicitly cancelled by N.C.; melody attacks never suppress accompaniment. This same rule applies to electric accompaniment sections in songs. Song acoustic rhythm and solo-slash patterns are unchanged. +Accompaniment attacks at each bar start and authored harmony onset, sustaining until the next harmony onset or bar end. Harmony carries into subsequent bars until changed or explicitly canceled by N.C.; melody attacks never suppress accompaniment. This same rule applies to electric accompaniment sections in songs. Song acoustic rhythm and solo-slash patterns are unchanged. Each source is parsed, validated, and normalized once for all three parts. Part builders treat the prepared source as read-only and own their generated notes and spans. Source-path checks reject colliding song slugs before writing parts: for example, `one-two.xml` and `one_two.xml` would both produce `one-two-acoustic.json` and cannot coexist. @@ -150,7 +150,7 @@ At runtime the shared on-demand score loader serves both display and audio. A ba `audio.playback_enabled` defaults to false and replaces the old inert `track_enabled` setting. The old config name is accepted for migration, but conflicting values fail. Playback suppresses the metronome without changing its stored toggle. Authored tempo marks remain notation; menu BPM is constant throughout a session. Selecting a practice or performance part starts a new session. Manual page changes never restart or seek. Natural completion, including trailing silence/release, returns to the corresponding part menu with its selection preserved. Leaving the score cancels playback; stale results cannot close a newer score. Errors remain visible rather than being mistaken for completion. -`--once` additionally validates referenced audio without opening display/audio hardware. Runtime packages contain WAVs alongside pages and the SQLite catalogue; the source XML and Python dependencies remain build-only. +`--once` additionally validates referenced audio without opening display/audio hardware. Runtime packages contain WAVs alongside pages and the SQLite catalog; the source XML and Python dependencies remain build-only. ### Engraving Boundary @@ -166,19 +166,19 @@ Generated guitar tones carry `tablature: {string, fret}`; their staff is marked `audit.py` checks the final layout against the musical model: timeline and fragment coverage, exact fragment durations, actual glyph bounds, pitch positions, visible note primitives, required accidentals, ties, lyrics, harmonies, tempos and spans. It rejects out-of-page geometry and collisions among unrelated noteheads, accidentals and same-lane text. Staff, ledger, stem, beam and tie contacts are intentional. This is not a proof of publication-quality engraving for every possible MusicXML document; notation changes need targeted semantic, geometry and reviewed image tests. The writer's XML round-trip audit is retained separately in the test suite. -Bundles keep schema version 3 and an unchanged `music` object. Presentation identifies `Vamp Python` version `1.0.0`; its style fingerprint includes the style settings, both font files, font-specific metadata and the glyph-name mapping. The musical bundle version is separate from the catalogue database version; runtime accepts only the supported packaged catalogue schema. +Bundles keep schema version 3 and an unchanged `music` object. Presentation identifies `Vamp Python` version `1.0.0`; its style fingerprint includes the style settings, both font files, font-specific metadata and the glyph-name mapping. The musical bundle version is separate from the catalog database version; runtime accepts only the supported packaged catalog schema. Pages are 976x484 pixels with 12-pixel staff spacing, 28-pixel horizontal margins and 18-pixel vertical margins. Dense measures continue across systems/pages; sustained notes become exact, tied presentation fragments and rests become exact rest fragments. These fragments never change the semantic JSON. Irreducible simultaneous content that cannot fit a fixed page fails contextually rather than being clipped or shrunk. Blank pages, incorrect dimensions, and ink reaching the page edge are rejected. Runtime letterboxing preserves aspect ratio. Luma's `load_texture_uncached()` lets Product Vamp retain at most two textures without a hidden strong asset-cache reference. The UI reports missing/corrupt assets instead of falling back to the removed renderer. Runtime validates bundle metadata, paths, dimensions, and page checksums; the build validates musical semantics. ### Validation And Migration -Generation validates sources and complete staged output before replacing the previous asset directory. Replacement is always complete; deleted sources cannot leave stale JSON, pages, or audio. CMake tracks the input list as well as file contents, so additions and removals regenerate the catalogue. Runtime packages contain `catalogue.db` with embedded semantic JSON, PNG pages, generated PCM WAVs, and the OTF UI text font, not MusicXML, MuseScore, or Python dependencies. Python test discovery runs model/import/export tests; the normal asset build additionally qualifies and audits every catalogue score. +Generation validates sources and complete staged output before replacing the previous asset directory. Replacement is always complete; deleted sources cannot leave stale JSON, pages, or audio. CMake tracks the input list as well as file contents, so additions and removals regenerate the catalog. Runtime packages contain `catalog.db` with embedded semantic JSON, PNG pages, generated PCM WAVs, and the OTF UI text font, not MusicXML, MuseScore, or Python dependencies. Python test discovery runs model/import/export tests; the normal asset build additionally qualifies and audits every catalog score. CI page-cache snapshot keys include every generator Python module and XML source, while retaining a renderer/font-compatible restore prefix. Individual cached pages remain validated by their semantic-content and renderer fingerprints. Diagnostics identify the file, part, measure, and unsupported element or invalid relationship. Examples include `part P1, measure 4: unsupported musical element `, `tie stop has no contiguous matching start`, and `optional-chord marker must identify exactly one harmony`. -The six compact source files were replaced, not retained as a second authoring format. Installation replaces the entire catalogue, rather than retaining or migrating rows from older releases. Legacy data must be authored as supported MusicXML and regenerated; the runtime never upgrades old databases. +The six compact source files were replaced, not retained as a second authoring format. Installation replaces the entire catalog, rather than retaining or migrating rows from older releases. Legacy data must be authored as supported MusicXML and regenerated; the runtime never upgrades old databases. References: [MusicXML durations](https://www.w3.org/2021/06/musicxml40/musicxml-reference/elements/duration/), [tuplets](https://www.w3.org/2021/06/musicxml40/musicxml-reference/elements/tuplet/), [degree alterations](https://www.w3.org/2021/06/musicxml40/musicxml-reference/elements/degree-alter/), and [SMuFL accidentals](https://smufl.formats.music/latest/tables/standard-accidentals-12-edo.html). @@ -220,13 +220,13 @@ Building the full package requires Docker/buildx, the ARM embedded GCC toolchain Generated score assets are created during CMake build/package and copied into the Zero image context. Static font assets remain tracked under `package/assets/fonts`. -`generator/catalogue.py` uses Python's standard-library SQLite module to construct `catalogue.db` after musical, playback, and engraving validation. The database carries application ID `0x56414D50` and catalogue schema version 2, with complete score bundles and menu metadata. Sorted insertion and the absence of wall-clock timestamps make repeated builds reproducible with the same toolchain. It is closed and integrity-checked before publication, without journal/WAL sidecars. Individual JSON files remain available in the build tree for inspection, but both CMake installation and Docker image assembly exclude them. +`generator/catalog.py` uses Python's standard-library SQLite module to construct `catalog.db` after musical, playback, and engraving validation. The database carries application ID `0x56414D50` and catalog schema version 3, with complete score bundles and menu metadata. Sorted insertion and the absence of wall-clock timestamps make repeated builds reproducible with the same toolchain. It is closed and integrity-checked before publication, without journal/WAL sidecars. Individual JSON files remain available in the build tree for inspection, but both CMake installation and Docker image assembly exclude them. -The default `database.path` is `/usr/share/product-vamp/catalogue.db`; development configuration points at the native generated-assets database. Stored source paths are logical, asset-relative locations, not build-host filenames. Runtime resolves them against `runtime.assets_path` and rejects traversal and escaping symlinks. The JSON file itself need not exist: its parent identifies the score's page directory. Normal startup checks catalogue compatibility; `--once` additionally runs SQLite's integrity probe and checks all page/audio files. Missing, corrupt, or incompatible catalogues fail rather than creating an empty library. `--ensure-database` is no longer a supported command. +The default `database.path` is `/usr/share/product-vamp/catalog.db`; development configuration points at the native generated-assets database. Stored source paths are logical, asset-relative locations, not build-host filenames. Runtime resolves them against `runtime.assets_path` and rejects traversal and escaping symlinks. The JSON file itself need not exist: its parent identifies the score's page directory. Normal startup checks catalog compatibility; `--once` additionally runs SQLite's integrity probe and checks all page/audio files. Missing, corrupt, or incompatible catalogs fail rather than creating an empty library. `--ensure-database` is no longer a supported command. Every run of `zero/install-zero.sh` is a clean installation. It stages configuration/scripts, verifies the manifest's Docker tag against the incoming archive's image digest, and probes that image before stopping Product Vamp. It then clears the owned `/etc/product-vamp`, `/var/lib/product-vamp`, `/run/product-vamp`, and obsolete host `/usr/share/product-vamp` trees, including old SQLite sidecars. The installed container uses assets from its image and no longer mounts a writable state database. Startup reads the verified image ID from `/etc/product-vamp/image-id`, rather than depending on a mutable Docker tag. The installer replaces its scripts and systemd units, removes their old overrides, recreates only the `product-vamp-control` NetworkManager profile, and requires a successful service health check before announcing success. -Configuration, AP credentials, and all prior catalogue data are intentionally reset to bundle defaults. There is no automatic backup or migration. Installation through the control AP can disconnect the session; use the local console or independent networking. Preflight failure leaves existing application files untouched and restores the previous image tag; replacement failure stops Product Vamp and reports the error. Rerunning the installer is the recovery path. Ordinary boots/restarts do not run cleanup. Unrelated networks, OS data, containers, and system logs remain untouched. Cleanup rejects symlink/mount targets, never follows paths from old configuration, and removes only identified obsolete Product Vamp images not used by another container. +Configuration, AP credentials, and all prior catalog data are intentionally reset to bundle defaults. There is no automatic backup or migration. Installation through the control AP can disconnect the session; use the local console or independent networking. Preflight failure leaves existing application files untouched and restores the previous image tag; replacement failure stops Product Vamp and reports the error. Rerunning the installer is the recovery path. Ordinary boots/restarts do not run cleanup. Unrelated networks, OS data, containers, and system logs remain untouched. Cleanup rejects symlink/mount targets, never follows paths from old configuration, and removes only identified obsolete Product Vamp images not used by another container. CI builds the generic artifact with `python3 modulus.py package --version latest`, then `package/scripts/publish_latest_generic_package.py` replaces the Gitea generic package version `latest` and deletes older Product Vamp generic package versions. diff --git a/cmake/runtime_assets.cmake b/cmake/runtime_assets.cmake index 53a812e..34e4910 100644 --- a/cmake/runtime_assets.cmake +++ b/cmake/runtime_assets.cmake @@ -1,4 +1,4 @@ -if(NOT EXISTS "${SOURCE}/catalogue.db") - message(FATAL_ERROR "Missing generated catalogue: ${SOURCE}/catalogue.db") +if(NOT EXISTS "${SOURCE}/catalog.db") + message(FATAL_ERROR "Missing generated catalog: ${SOURCE}/catalog.db") endif() file(COPY "${SOURCE}/" DESTINATION "${DESTINATION}" PATTERN "*.json" EXCLUDE) diff --git a/cmake/zero.cmake b/cmake/zero.cmake index 92da2db..b2394fa 100644 --- a/cmake/zero.cmake +++ b/cmake/zero.cmake @@ -132,7 +132,7 @@ file(CONFIGURE OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/generated-assets.inputs CONTENT "@VAMP_GENERATOR_INPUT_MANIFEST@\n" @ONLY) add_custom_command( OUTPUT ${VAMP_GENERATED_ASSETS_STAMP} - BYPRODUCTS ${VAMP_GENERATED_ASSETS_DIR}/catalogue.db + BYPRODUCTS ${VAMP_GENERATED_ASSETS_DIR}/catalog.db COMMAND ${Python3_EXECUTABLE} -u -m generator diff --git a/generator/__main__.py b/generator/__main__.py index d6437fe..bb657d4 100644 --- a/generator/__main__.py +++ b/generator/__main__.py @@ -13,7 +13,7 @@ from generator.summary import validate_expected_performance_summary, validate_pr from generator.parts import source_paths from generator.engraving import engrave_tree from generator.playback import compile_tree -from generator.catalogue import build_catalogue +from generator.catalog import build_catalog RUNTIME_FONT_FILES = ("PetalumaScript.otf", "OFL.txt") @@ -74,8 +74,8 @@ def generate_assets(args: argparse.Namespace) -> None: engrave_tree(staged, static_assets_dir / 'fonts' / 'petaluma', cache_dir=cache_dir, jobs=getattr(args, 'jobs', 1), progress=progress) report('validating score, page, and audio assets') validate_generated_tree(staged) - report('building catalogue database') - build_catalogue(staged) + report('building catalog database') + build_catalog(staged) backup = Path(temporary) / 'previous' if output_dir.exists(): output_dir.rename(backup) diff --git a/generator/catalogue.py b/generator/catalog.py similarity index 91% rename from generator/catalogue.py rename to generator/catalog.py index 8587fda..8f4c714 100644 --- a/generator/catalogue.py +++ b/generator/catalog.py @@ -81,15 +81,15 @@ def populate(connection: sqlite3.Connection, root: Path) -> None: raise ValueError(f'practice exercise requires all three parts: {identity}') connection.commit() if connection.execute('PRAGMA integrity_check').fetchall() != [('ok',)]: - raise ValueError('catalogue integrity check failed') + raise ValueError('catalog integrity check failed') -def build_catalogue(root: Path, destination: Path | None = None) -> Path: +def build_catalog(root: Path, destination: Path | None = None) -> Path: if not root.is_dir(): raise ValueError(f'{root}: score asset directory not found') - destination = destination or root / 'catalogue.db' + destination = destination or root / 'catalog.db' destination.parent.mkdir(parents=True, exist_ok=True) - with tempfile.TemporaryDirectory(prefix='.vamp-catalogue-', dir=destination.parent) as temporary: - staged = Path(temporary) / 'catalogue.db' + with tempfile.TemporaryDirectory(prefix='.vamp-catalog-', dir=destination.parent) as temporary: + staged = Path(temporary) / 'catalog.db' connection = sqlite3.connect(staged) try: populate(connection, root) @@ -99,8 +99,8 @@ def build_catalogue(root: Path, destination: Path | None = None) -> Path: return destination if __name__ == '__main__': - parser = argparse.ArgumentParser(description='Compile generated score bundles into a read-only runtime catalogue.') + parser = argparse.ArgumentParser(description='Compile generated score bundles into a read-only runtime catalog.') parser.add_argument('assets', type=Path) parser.add_argument('destination', type=Path, nargs='?') arguments = parser.parse_args() - build_catalogue(arguments.assets, arguments.destination) + build_catalog(arguments.assets, arguments.destination) diff --git a/generator/pitch.py b/generator/pitch.py index 7d2e947..d08ebc5 100644 --- a/generator/pitch.py +++ b/generator/pitch.py @@ -41,10 +41,10 @@ def shift_key(key: dict, steps: int, semitones: int) -> dict: result['custom'] = True if 'cancel' in key: # A cancellation references the old signature, not a count of naturals. - cancelled = shift_key(key_from_fifths(key['cancel']), steps, semitones) - if cancelled.get('custom'): + canceled = shift_key(key_from_fifths(key['cancel']), steps, semitones) + if canceled.get('custom'): raise ValueError('transposed key cancellation cannot be represented by fifths') - result['cancel'] = cancelled['fifths'] + result['cancel'] = canceled['fifths'] return result def concert_document(document: dict, *, octave_transposing: bool = False) -> dict: diff --git a/include/vamp_zero/audio.hpp b/include/vamp_zero/audio.hpp index fa5e456..8e65bfc 100644 --- a/include/vamp_zero/audio.hpp +++ b/include/vamp_zero/audio.hpp @@ -39,7 +39,7 @@ private: std::future> preparation_{}; std::uint64_t preparing_session_{}; std::optional attempted_session_{}; - std::shared_ptr catalog_{}; + std::shared_ptr catalog_{}; sonus::playback_session playback_{}; std::bool_t reported_{true}; }; diff --git a/include/vamp_zero/config.hpp b/include/vamp_zero/config.hpp index 5a884db..f4a1ca5 100644 --- a/include/vamp_zero/config.hpp +++ b/include/vamp_zero/config.hpp @@ -24,7 +24,7 @@ struct runtime_config { }; struct database_config { - std::string path{"/usr/share/product-vamp/catalogue.db"}; + std::string path{"/usr/share/product-vamp/catalog.db"}; }; struct control_config { diff --git a/include/vamp_zero/portal.hpp b/include/vamp_zero/portal.hpp index 3c7974b..d5b5def 100644 --- a/include/vamp_zero/portal.hpp +++ b/include/vamp_zero/portal.hpp @@ -24,7 +24,7 @@ public: auto heartbeat() -> vamp_zero::app_state; auto refresh_connection() -> std::optional; auto set_observer(std::function observer) -> void; - auto finish_playback(std::uint64_t session, std::string error = {}, std::shared_ptr catalog = {}) -> void; + auto finish_playback(std::uint64_t session, std::string error = {}, std::shared_ptr catalog = {}) -> void; private: auto pico_connected_locked() const -> std::bool_t; auto snapshot_locked() const -> vamp_zero::app_state; diff --git a/include/vamp_zero/score_loader.hpp b/include/vamp_zero/score_loader.hpp index 17c1239..0aabe7d 100644 --- a/include/vamp_zero/score_loader.hpp +++ b/include/vamp_zero/score_loader.hpp @@ -28,7 +28,7 @@ public: ~score_loader(); score_loader(score_loader const &) = delete; auto operator=(score_loader const &) -> score_loader & = delete; - auto select(std::shared_ptr catalog, std::optional reference) -> void; + auto select(std::shared_ptr catalog, std::optional reference) -> void; auto snapshot() const -> vamp_zero::score_load_state; auto stop() -> void; private: diff --git a/include/vamp_zero/types.hpp b/include/vamp_zero/types.hpp index 16b3192..2d0ae7f 100644 --- a/include/vamp_zero/types.hpp +++ b/include/vamp_zero/types.hpp @@ -64,12 +64,12 @@ struct practice_library { std::vector exercises{}; }; -struct catalogue { +struct catalog { vamp_zero::library_state performance{}; vamp_zero::practice_library practice{}; }; -auto empty_catalogue() -> std::shared_ptr const &; +auto empty_catalog() -> std::shared_ptr const &; enum class score_category { PERFORMANCE, @@ -104,7 +104,7 @@ struct audio_state { }; struct app_state { - std::shared_ptr catalog{vamp_zero::empty_catalogue()}; + std::shared_ptr catalog{vamp_zero::empty_catalog()}; vamp_zero::app_mode mode{vamp_zero::app_mode::MAIN_MENU}; vamp_zero::audio_state audio{}; std::size_t selected_main_menu_index{}; diff --git a/package/config/vamp.development.json b/package/config/vamp.development.json index e2c29e9..9378219 100644 --- a/package/config/vamp.development.json +++ b/package/config/vamp.development.json @@ -5,7 +5,7 @@ "heartbeat_path": "build/build_x86_debug/runtime/heartbeat.json" }, "database": { - "path": "build/build_x86_debug/generated-assets/catalogue.db" + "path": "build/build_x86_debug/generated-assets/catalog.db" }, "control": { "endpoint": { diff --git a/package/config/vamp.example.json b/package/config/vamp.example.json index cabbe28..dc3005f 100644 --- a/package/config/vamp.example.json +++ b/package/config/vamp.example.json @@ -17,7 +17,7 @@ "heartbeat_path": "/run/product-vamp/heartbeat.json" }, "database": { - "path": "/usr/share/product-vamp/catalogue.db" + "path": "/usr/share/product-vamp/catalog.db" }, "control": { "endpoint": { diff --git a/src/vamp_zero/app.cpp b/src/vamp_zero/app.cpp index 9e4ad0d..6d918aa 100644 --- a/src/vamp_zero/app.cpp +++ b/src/vamp_zero/app.cpp @@ -166,7 +166,7 @@ auto load_initial_state(vamp_zero::config const &config) -> std::expected(vamp_zero::catalogue{ + .catalog = std::make_shared(vamp_zero::catalog{ .performance = std::move(*performance), .practice = std::move(*practice), }), @@ -204,7 +204,7 @@ auto validate_score_pages(boost::asio::any_io_executor executor, vamp_zero::conf co_return std::expected{}; } -auto validate_catalogue(boost::asio::any_io_executor executor, vamp_zero::config const &config, vamp_zero::catalogue const &catalog) -> boost::asio::awaitable> { +auto validate_catalog(boost::asio::any_io_executor executor, vamp_zero::config const &config, vamp_zero::catalog const &catalog) -> boost::asio::awaitable> { for (auto const &part : catalog.performance.parts) { auto reference = vamp_zero::score_reference{.category = vamp_zero::score_category::PERFORMANCE, .id = part.id}; auto valid = co_await validate_score_pages(executor, config, std::move(reference)); @@ -261,9 +261,9 @@ auto probe_runtime_directory(vamp_zero::config const &config) -> std::expected std::expected { +auto check_readiness(vamp_zero::config const &config, vamp_zero::catalog const &catalog) -> std::expected { auto io = boost::asio::io_context{}; - auto future = boost::asio::co_spawn(io, validate_catalogue(io.get_executor(), config, catalog), boost::asio::use_future); + auto future = boost::asio::co_spawn(io, validate_catalog(io.get_executor(), config, catalog), boost::asio::use_future); io.run(); auto valid = future.get(); if (!valid) { diff --git a/src/vamp_zero/database.cpp b/src/vamp_zero/database.cpp index 758af2f..857f50d 100644 --- a/src/vamp_zero/database.cpp +++ b/src/vamp_zero/database.cpp @@ -25,7 +25,7 @@ namespace { constexpr auto application_id = std::int64_t{0x56414D50}; -constexpr auto catalogue_schema_version = std::int64_t{3}; +constexpr auto catalog_schema_version = std::int64_t{3}; auto connection_config(vamp_zero::database_config const &config) -> noto::connection_config { auto result = noto::file_connection_config(config.path, "product-vamp"); @@ -40,17 +40,17 @@ auto text_value(noto::sqlite_value const &value) -> std::string { return text == nullptr ? std::string{} : *text; } -auto close_catalogue_query(noto::connection &connection, noto::statement &statement, std::expected status) -> boost::asio::awaitable> { +auto close_catalog_query(noto::connection &connection, noto::statement &statement, std::expected status) -> boost::asio::awaitable> { auto finalized = co_await statement.async_finalize(); if (!finalized) { - modulus::log_error("catalogue query finalization failed: " + finalized.error().describe()); + modulus::log_error("catalog query finalization failed: " + finalized.error().describe()); if (status) { status = std::unexpected(finalized.error()); } } auto closed = co_await connection.async_close(); if (!closed) { - modulus::log_error("catalogue connection close failed: " + closed.error().describe()); + modulus::log_error("catalog connection close failed: " + closed.error().describe()); if (status) { status = std::unexpected(closed.error()); } @@ -60,7 +60,7 @@ auto close_catalogue_query(noto::connection &connection, noto::statement &statem auto resolve_source_path(std::string const &assets_path, std::string const &source) -> std::expected { auto fail = [] { - return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::PERSISTENCE, "unsafe catalogue asset path; rebuild or reinstall Product Vamp")); + return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::PERSISTENCE, "unsafe catalog asset path; rebuild or reinstall Product Vamp")); }; auto relative = std::filesystem::path{source}; if (source.empty() || source.find('\0') != std::string::npos || source.find('\\') != std::string::npos || @@ -94,23 +94,23 @@ auto count_value(noto::sqlite_value const &value) -> std::size_t { auto vamp_zero::async_validate_database(boost::asio::any_io_executor executor, vamp_zero::database_config config, std::bool_t integrity) -> boost::asio::awaitable> { auto error = std::error_code{}; if (!std::filesystem::is_regular_file(config.path, error) || error) { - co_return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::PERSISTENCE, "catalogue is missing or unreadable: " + config.path + "; rebuild or reinstall Product Vamp")); + co_return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::PERSISTENCE, "catalog is missing or unreadable: " + config.path + "; rebuild or reinstall Product Vamp")); } auto connection = noto::connection{executor, connection_config(config)}; auto connected = co_await connection.async_connect(); if (!connected) { - co_return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::PERSISTENCE, "cannot open catalogue: " + config.path + "; rebuild or reinstall Product Vamp: " + connected.error().describe())); + co_return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::PERSISTENCE, "cannot open catalog: " + config.path + "; rebuild or reinstall Product Vamp: " + connected.error().describe())); } auto application = co_await connection.async_application_id(); auto version = co_await connection.async_user_version(); auto status = std::expected{}; - if (!application || !version || *application != application_id || *version != catalogue_schema_version) { - status = std::unexpected(vamp_zero::make_error(vamp_zero::error_code::PERSISTENCE, "invalid or incompatible catalogue: " + config.path + "; rebuild or reinstall Product Vamp")); + if (!application || !version || *application != application_id || *version != catalog_schema_version) { + status = std::unexpected(vamp_zero::make_error(vamp_zero::error_code::PERSISTENCE, "invalid or incompatible catalog: " + config.path + "; rebuild or reinstall Product Vamp")); } if (status && integrity) { auto checked = co_await connection.async_execute("PRAGMA quick_check(1)"); if (!checked || checked->rows.size() != 1U || checked->rows[0].size() != 1U || text_value(checked->rows[0][0]) != "ok") { - status = std::unexpected(vamp_zero::make_error(vamp_zero::error_code::PERSISTENCE, "corrupt catalogue: " + config.path + "; rebuild or reinstall Product Vamp")); + status = std::unexpected(vamp_zero::make_error(vamp_zero::error_code::PERSISTENCE, "corrupt catalog: " + config.path + "; rebuild or reinstall Product Vamp")); } } auto closed = co_await connection.async_close(); @@ -169,7 +169,7 @@ auto vamp_zero::async_load_performance_library(boost::asio::any_io_executor exec .total_pages = count_value(row[7]), }); } - auto closed = co_await close_catalogue_query(connection, *selected, std::move(status)); + auto closed = co_await close_catalog_query(connection, *selected, std::move(status)); if (!closed) { co_return std::unexpected(closed.error()); } @@ -230,7 +230,7 @@ auto vamp_zero::async_load_practice_library(boost::asio::any_io_executor executo .parts = {std::move(practice_part)}, }); } - auto closed = co_await close_catalogue_query(connection, *selected, std::move(status)); + auto closed = co_await close_catalog_query(connection, *selected, std::move(status)); if (!closed) { co_return std::unexpected(closed.error()); } @@ -272,7 +272,7 @@ auto vamp_zero::async_load_score(boost::asio::any_io_executor executor, vamp_zer if (!result) { status = std::unexpected(result.error()); } - auto closed = co_await close_catalogue_query(connection, *selected, std::move(status)); + auto closed = co_await close_catalog_query(connection, *selected, std::move(status)); if (!closed) { co_return std::unexpected(closed.error()); } diff --git a/src/vamp_zero/page_control.cpp b/src/vamp_zero/page_control.cpp index a17560a..8114922 100644 --- a/src/vamp_zero/page_control.cpp +++ b/src/vamp_zero/page_control.cpp @@ -67,7 +67,7 @@ auto decrease_bpm(vamp_zero::audio_state &audio) -> void { auto clamp_state(vamp_zero::app_state state) -> vamp_zero::app_state { if (!state.catalog) { - state.catalog = vamp_zero::empty_catalogue(); + state.catalog = vamp_zero::empty_catalog(); } state.audio = clamp_audio_state(state.audio); state.selected_main_menu_index = std::min(state.selected_main_menu_index, main_menu_count - 1U); @@ -469,7 +469,7 @@ auto vamp_zero::remote_display_text(vamp_zero::app_state const &state) -> std::s auto vamp_zero::validate_app_state(vamp_zero::app_state const &state) -> std::expected { if (!state.catalog) { - return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::INVALID_ARGUMENT, "catalogue is missing")); + return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::INVALID_ARGUMENT, "catalog is missing")); } 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")); diff --git a/src/vamp_zero/portal.cpp b/src/vamp_zero/portal.cpp index 1353138..ac907a7 100644 --- a/src/vamp_zero/portal.cpp +++ b/src/vamp_zero/portal.cpp @@ -124,7 +124,7 @@ auto vamp_zero::control_state::set_observer(std::function catalog) -> void { +auto vamp_zero::control_state::finish_playback(std::uint64_t session, std::string error, std::shared_ptr catalog) -> void { auto observer = std::function{}; auto updated = vamp_zero::app_state{}; { diff --git a/src/vamp_zero/score_loader.cpp b/src/vamp_zero/score_loader.cpp index bfb21c1..e587a0d 100644 --- a/src/vamp_zero/score_loader.cpp +++ b/src/vamp_zero/score_loader.cpp @@ -75,7 +75,7 @@ struct vamp_zero::score_loader::state : std::enable_shared_from_this { std::function>(vamp_zero::score_reference)> load; std::function completed; std::mutex mutex{}; - std::shared_ptr catalog{}; + std::shared_ptr catalog{}; std::optional reference{}; vamp_zero::score_load_state value{}; std::uint64_t generation{}; @@ -90,7 +90,7 @@ vamp_zero::score_loader::~score_loader() { stop(); } -auto vamp_zero::score_loader::select(std::shared_ptr catalog, std::optional reference) -> void { +auto vamp_zero::score_loader::select(std::shared_ptr catalog, std::optional reference) -> void { auto lock = std::scoped_lock{state_->mutex}; if (!catalog) { reference.reset(); diff --git a/src/vamp_zero/types.cpp b/src/vamp_zero/types.cpp index e2867b1..2aea6aa 100644 --- a/src/vamp_zero/types.cpp +++ b/src/vamp_zero/types.cpp @@ -4,8 +4,8 @@ #include -auto vamp_zero::empty_catalogue() -> std::shared_ptr const & { - static auto const empty = std::make_shared(); +auto vamp_zero::empty_catalog() -> std::shared_ptr const & { + static auto const empty = std::make_shared(); return empty; } diff --git a/src/vamp_zero/ui.cpp b/src/vamp_zero/ui.cpp index 11e81d0..86a686d 100644 --- a/src/vamp_zero/ui.cpp +++ b/src/vamp_zero/ui.cpp @@ -402,7 +402,7 @@ auto vamp_zero::zero_ui::publish(vamp_zero::ui_state state) -> std::expectedmutex_}; if (!state.app.catalog) { - state.app.catalog = vamp_zero::empty_catalogue(); + state.app.catalog = vamp_zero::empty_catalog(); } score_loader_->select(state.app.catalog, vamp_zero::selected_score_reference(state.app)); runtime_->state_ = std::move(state); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index d165200..825df0e 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -3,7 +3,7 @@ add_executable(product-vamp-test ../src/vamp_pico/input.cpp ../src/vamp_pico/remote_display.cpp test.cpp - test_catalogue_runtime.cpp + test_catalog_runtime.cpp test_health.cpp test_pico.cpp test_playback.cpp diff --git a/tests/catalogue_fixture.hpp b/tests/catalog_fixture.hpp similarity index 82% rename from tests/catalogue_fixture.hpp rename to tests/catalog_fixture.hpp index 91bb0c8..2f44503 100644 --- a/tests/catalogue_fixture.hpp +++ b/tests/catalog_fixture.hpp @@ -21,13 +21,13 @@ inline auto part_filename(std::string_view part) -> std::string { return result + ".json"; } -inline auto build_catalogue(std::filesystem::path const &assets, std::filesystem::path const &database) -> std::bool_t { +inline auto build_catalog(std::filesystem::path const &assets, std::filesystem::path const &database) -> std::bool_t { auto source = assets.string(); auto destination = database.string(); auto child = ::fork(); BOOST_REQUIRE(child >= 0); if (child == 0) { - ::execl(VAMP_TEST_PYTHON, VAMP_TEST_PYTHON, VAMP_TEST_SOURCE_DIR "/generator/catalogue.py", source.c_str(), destination.c_str(), nullptr); + ::execl(VAMP_TEST_PYTHON, VAMP_TEST_PYTHON, VAMP_TEST_SOURCE_DIR "/generator/catalog.py", source.c_str(), destination.c_str(), nullptr); ::_exit(127); } auto status = std::int32_t{}; diff --git a/tests/test.cpp b/tests/test.cpp index f3a7973..0d9e770 100644 --- a/tests/test.cpp +++ b/tests/test.cpp @@ -41,7 +41,7 @@ #include #include -#include "catalogue_fixture.hpp" +#include "catalog_fixture.hpp" namespace { @@ -137,7 +137,7 @@ auto navigation_state() -> vamp_zero::app_state { auto practice = vamp_zero::group_practice_exercises({exercise}); BOOST_REQUIRE(practice); return vamp_zero::app_state{ - .catalog = std::make_shared(vamp_zero::catalogue{ + .catalog = std::make_shared(vamp_zero::catalog{ .performance = vamp_zero::group_library_parts({part}), .practice = std::move(*practice), }), @@ -157,6 +157,7 @@ BOOST_AUTO_TEST_CASE(runtime_config_uses_headless_and_ignores_unknown_fields) { BOOST_REQUIRE(parsed); BOOST_TEST(parsed->runtime.headless); BOOST_TEST(!parsed->audio.enabled); + BOOST_TEST(parsed->database.path == "/usr/share/product-vamp/catalog.db"); auto invalid = vamp_zero::parse_config(boost::json::parse(R"json({"runtime":{"headless":"yes"}})json")); BOOST_TEST(!invalid); } @@ -274,7 +275,7 @@ BOOST_AUTO_TEST_CASE(main_menu_metronome_setting_survives_score_selection) { BOOST_TEST(!state.audio.tempo_editing); } -BOOST_AUTO_TEST_CASE(score_parser_loads_the_generated_catalogue) { +BOOST_AUTO_TEST_CASE(score_parser_loads_the_generated_catalog) { auto count = std::size_t{}; for (auto const &entry : std::filesystem::recursive_directory_iterator{VAMP_TEST_ASSETS_DIR}) { if (entry.path().extension() != ".json") { @@ -294,7 +295,7 @@ BOOST_AUTO_TEST_CASE(score_parser_loads_the_generated_catalogue) { BOOST_AUTO_TEST_CASE(practice_part_navigation_and_remote_display) { auto state = navigation_state(); - auto catalog = std::make_shared(*state.catalog); + auto catalog = std::make_shared(*state.catalog); auto &practice_part = catalog->practice.exercises.front().parts[2]; practice_part.source_path = "exercise-vocal.json"; practice_part.total_pages = 3U; @@ -350,9 +351,9 @@ BOOST_AUTO_TEST_CASE(practice_part_navigation_and_remote_display) { BOOST_REQUIRE(vamp_zero::validate_app_state(state)); } -BOOST_AUTO_TEST_CASE(flat_practice_wraps_and_empty_catalogue_returns_to_main) { +BOOST_AUTO_TEST_CASE(flat_practice_wraps_and_empty_catalog_returns_to_main) { auto state = navigation_state(); - auto catalog = std::make_shared(*state.catalog); + auto catalog = std::make_shared(*state.catalog); auto first = catalog->practice.exercises.front(); for (auto index = std::uint32_t{2}; index <= 366U; ++index) { auto exercise = first; @@ -369,7 +370,7 @@ BOOST_AUTO_TEST_CASE(flat_practice_wraps_and_empty_catalogue_returns_to_main) { state = vamp_zero::apply_app_command(std::move(state), vamp_protocol::page_command::PREVIOUS); state = vamp_zero::apply_app_command(std::move(state), vamp_protocol::page_command::SELECT); BOOST_CHECK(state.mode == vamp_zero::app_mode::PRACTICE_PART_MENU); - state.catalog = vamp_zero::empty_catalogue(); + state.catalog = vamp_zero::empty_catalog(); state = vamp_zero::apply_app_command(std::move(state), vamp_protocol::page_command::NEXT); BOOST_CHECK(state.mode == vamp_zero::app_mode::PRACTICE_EXERCISE_MENU); BOOST_TEST(state.selected_practice_exercise_index == 0U); @@ -389,7 +390,7 @@ BOOST_AUTO_TEST_CASE(database_reload_preserves_all_generated_practice_parts) { } BOOST_REQUIRE(expected_exercises > 0U); for (auto iteration = std::uint32_t{}; iteration < 2U; ++iteration) { - config.path = (std::filesystem::path{VAMP_TEST_ASSETS_DIR} / "catalogue.db").string(); + config.path = (std::filesystem::path{VAMP_TEST_ASSETS_DIR} / "catalog.db").string(); auto io = boost::asio::io_context{}; auto future = boost::asio::co_spawn(io, vamp_zero::async_load_practice_library(io.get_executor(), config, VAMP_TEST_ASSETS_DIR), boost::asio::use_future); io.run(); @@ -434,7 +435,7 @@ BOOST_AUTO_TEST_CASE(numbered_practice_paths_keep_duplicate_titles_separate_and_ write_text(assets / "practice" / name / vamp_test::part_filename(part_name), score_asset_json("Shared Title", "C", std::string{part_name})); } } - BOOST_REQUIRE(vamp_test::build_catalogue(assets, config.path)); + BOOST_REQUIRE(vamp_test::build_catalog(assets, config.path)); auto io = boost::asio::io_context{}; auto future = boost::asio::co_spawn(io, vamp_zero::async_load_practice_library(io.get_executor(), config, assets.string()), boost::asio::use_future); io.run(); @@ -448,9 +449,9 @@ BOOST_AUTO_TEST_CASE(numbered_practice_paths_keep_duplicate_titles_separate_and_ for (auto const &exercise : library->exercises) { BOOST_TEST(exercise.title == "Shared Title"); } - BOOST_REQUIRE(vamp_test::build_catalogue(assets, config.path)); + BOOST_REQUIRE(vamp_test::build_catalog(assets, config.path)); std::filesystem::rename(assets / "practice/002", assets / "practice/099"); - BOOST_REQUIRE(vamp_test::build_catalogue(assets, config.path)); + BOOST_REQUIRE(vamp_test::build_catalog(assets, config.path)); auto rows = query_database(root / "vamp.db", "SELECT id FROM practice_exercises ORDER BY id"); BOOST_REQUIRE(rows); BOOST_REQUIRE_EQUAL(rows->rows.size(), 12U); diff --git a/tests/test_catalogue.py b/tests/test_catalog.py similarity index 98% rename from tests/test_catalogue.py rename to tests/test_catalog.py index 7ecaae2..870f615 100644 --- a/tests/test_catalogue.py +++ b/tests/test_catalog.py @@ -11,7 +11,7 @@ from generator.parts import PART_TYPES from unittest.mock import patch from tests.test_musicxml import xml_note, xml_score -class CatalogueTests(unittest.TestCase): +class CatalogTests(unittest.TestCase): def test_every_numbered_source_is_wired_to_three_part_paths(self): source = Path('data/practice') sources = sorted(source.glob('*.xml')) diff --git a/tests/test_catalogue_database.py b/tests/test_catalog_database.py similarity index 82% rename from tests/test_catalogue_database.py rename to tests/test_catalog_database.py index 916627c..33873e1 100644 --- a/tests/test_catalogue_database.py +++ b/tests/test_catalog_database.py @@ -5,7 +5,7 @@ import tempfile import unittest from pathlib import Path -from generator.catalogue import APPLICATION_ID, SCHEMA_VERSION, build_catalogue +from generator.catalog import APPLICATION_ID, SCHEMA_VERSION, build_catalog def bundle(title='Fixture', key='C', part='Lead'): return { @@ -21,14 +21,14 @@ def write_score(root, relative, document=None): path.write_text(json.dumps(document or bundle()), encoding='utf-8') return path -class CatalogueTests(unittest.TestCase): +class CatalogTests(unittest.TestCase): def setUp(self): self.temporary = tempfile.TemporaryDirectory() self.addCleanup(self.temporary.cleanup) self.root = Path(self.temporary.name) (self.root / 'songs').mkdir() def rows(self, table): - with closing(sqlite3.connect(self.root / 'catalogue.db')) as connection: + with closing(sqlite3.connect(self.root / 'catalog.db')) as connection: connection.row_factory = sqlite3.Row return [dict(row) for row in connection.execute(f'SELECT * FROM {table} ORDER BY id')] def test_metadata_relative_paths_and_complete_documents(self): @@ -36,7 +36,8 @@ class CatalogueTests(unittest.TestCase): write_score(self.root, 'songs/tune.json', document) for part in ('Acoustic', 'Electric', 'Vocal'): write_score(self.root, f'practice/001/{part.lower()}.json', bundle('Exercise', 'Eb', part)) - database = build_catalogue(self.root) + database = build_catalog(self.root) + self.assertEqual(database, self.root / 'catalog.db') song = self.rows('songs')[0] self.assertEqual(json.loads(song['source_json']), document) self.assertEqual((song['id'], song['part'], song['source_path']), ('tune', 'Lead', 'songs/tune.json')) @@ -57,62 +58,65 @@ class CatalogueTests(unittest.TestCase): self.assertFalse(Path(str(database) + '-journal').exists()) def test_rebuild_is_deterministic_relocatable_and_replaces_deleted_rows(self): score = write_score(self.root, 'songs/tune.json') - database = build_catalogue(self.root) + database = build_catalog(self.root) previous = database.read_bytes() - build_catalogue(self.root) + build_catalog(self.root) self.assertEqual(database.read_bytes(), previous) with tempfile.TemporaryDirectory() as other: root = Path(other) write_score(root, 'songs/tune.json') - self.assertEqual(build_catalogue(root).read_bytes(), previous) + self.assertEqual(build_catalog(root).read_bytes(), previous) score.rename(score.with_name('renamed.json')) - build_catalogue(self.root) + build_catalog(self.root) self.assertEqual([row['id'] for row in self.rows('songs')], ['renamed']) score.with_name('renamed.json').unlink() - build_catalogue(self.root) + build_catalog(self.root) self.assertEqual(self.rows('songs'), []) def test_invalid_input_preserves_existing_database(self): score = write_score(self.root, 'songs/tune.json') - database = build_catalogue(self.root) + database = build_catalog(self.root) previous = database.read_bytes() for document in ({}, bundle(title=''), bundle(key=None)): score.write_text(json.dumps(document)) with self.assertRaises(ValueError): - build_catalogue(self.root) + build_catalog(self.root) self.assertEqual(database.read_bytes(), previous) - self.assertFalse(list(self.root.glob('.vamp-catalogue-*'))) + self.assertFalse(list(self.root.glob('.vamp-catalog-*'))) def test_missing_parts_and_metadata_disagreement_fail(self): for part in ('Acoustic', 'Electric'): write_score(self.root, f'practice/001/{part.lower()}.json', bundle(part=part)) with self.assertRaisesRegex(ValueError, 'all three parts'): - build_catalogue(self.root) + build_catalog(self.root) write_score(self.root, 'practice/001/vocal.json', bundle(key='F', part='Vocal')) with self.assertRaisesRegex(ValueError, 'disagree'): - build_catalogue(self.root) + build_catalog(self.root) def test_keyless_parts_preserve_their_original_key(self): for part in ('Acoustic', 'Electric', 'Vocal'): write_score(self.root, f'practice/001/{part.lower()}.json', bundle(key='No key', part=part)) - build_catalogue(self.root) + build_catalog(self.root) self.assertTrue(all(row['key_signature'] == 'No key' for row in self.rows('practice_exercises'))) def test_invalid_layout_and_escaping_source_fail(self): with self.assertRaisesRegex(ValueError, 'directory not found'): - build_catalogue(self.root / 'missing') + build_catalog(self.root / 'missing') self.assertFalse((self.root / 'missing').exists()) score = write_score(self.root, 'songs/nested/tune.json') with self.assertRaisesRegex(ValueError, 'unexpected songs'): - build_catalogue(self.root) + build_catalog(self.root) score.unlink() (self.root / 'songs/escape.json').symlink_to('/etc/passwd') with self.assertRaises(ValueError): - build_catalogue(self.root) + build_catalog(self.root) def test_runtime_asset_packaging_excludes_score_json(self): import subprocess write_score(self.root, 'songs/tune.json') - build_catalogue(self.root) + build_catalog(self.root) (self.root / 'songs/tune.png').write_bytes(b'page') with tempfile.TemporaryDirectory() as temporary: destination = Path(temporary) subprocess.run(['cmake', f'-DSOURCE={self.root}', f'-DDESTINATION={destination}', '-P', 'cmake/runtime_assets.cmake'], check=True, capture_output=True) - self.assertTrue((destination / 'catalogue.db').exists()) + self.assertTrue((destination / 'catalog.db').exists()) + for name in ('vamp.example.json', 'vamp.development.json'): + config = json.loads((Path('package/config') / name).read_text()) + self.assertEqual(Path(config['database']['path']).name, 'catalog.db') self.assertTrue((destination / 'songs/tune.png').exists()) self.assertFalse(list(destination.rglob('*.json'))) diff --git a/tests/test_catalogue_runtime.cpp b/tests/test_catalog_runtime.cpp similarity index 84% rename from tests/test_catalogue_runtime.cpp rename to tests/test_catalog_runtime.cpp index 84f72fd..a078da8 100644 --- a/tests/test_catalogue_runtime.cpp +++ b/tests/test_catalog_runtime.cpp @@ -29,7 +29,7 @@ #include #include -#include "catalogue_fixture.hpp" +#include "catalog_fixture.hpp" namespace { @@ -40,30 +40,30 @@ static_assert(!retains_score_json); static_assert(!retains_score_json); static_assert(!retains_score_json); -struct catalogue_directory { +struct catalog_directory { std::filesystem::path path; - catalogue_directory() { - auto pattern = (std::filesystem::temp_directory_path() / "vamp-catalogue-XXXXXX").string(); + catalog_directory() { + auto pattern = (std::filesystem::temp_directory_path() / "vamp-catalog-XXXXXX").string(); BOOST_REQUIRE(::mkdtemp(pattern.data()) != nullptr); path = pattern; } - ~catalogue_directory() { + ~catalog_directory() { auto ignored = std::error_code{}; std::filesystem::remove_all(path, ignored); } }; template -auto run_catalogue_operation(Function function, Arguments... arguments) { +auto run_catalog_operation(Function function, Arguments... arguments) { auto io = boost::asio::io_context{}; auto future = boost::asio::co_spawn(io, function(io.get_executor(), std::move(arguments)...), boost::asio::use_future); io.run(); return future.get(); } -auto catalogue_query(boost::asio::any_io_executor executor, vamp_zero::database_config config, std::string sql, std::vector params) +auto catalog_query(boost::asio::any_io_executor executor, vamp_zero::database_config config, std::string sql, std::vector params) -> boost::asio::awaitable> { - auto connection = noto::connection{executor, noto::file_connection_config(config.path, "vamp-catalogue-test")}; + auto connection = noto::connection{executor, noto::file_connection_config(config.path, "vamp-catalog-test")}; auto connected = co_await connection.async_connect(); if (!connected) { co_return std::unexpected(connected.error()); @@ -79,7 +79,7 @@ auto catalogue_query(boost::asio::any_io_executor executor, vamp_zero::database_ co_return result; } -auto catalogue_bundle(std::string_view title, std::string_view key = "C", std::string_view part = "Lead") -> std::string { +auto catalog_bundle(std::string_view title, std::string_view key = "C", std::string_view part = "Lead") -> std::string { auto value = boost::json::parse(R"json({ "schema_version":3, "music":{"metadata":{"title":"Fixture","composer":"Composer","key_signature":"C","time_signature":"4/4"}, @@ -95,15 +95,15 @@ auto catalogue_bundle(std::string_view title, std::string_view key = "C", std::s return boost::json::serialize(value); } -struct catalogue_database { - catalogue_directory directory; +struct catalog_database { + catalog_directory directory; std::filesystem::path assets{directory.path / "assets"}; - vamp_zero::database_config config{.path = (directory.path / "catalogue.db").string()}; - catalogue_database() { + vamp_zero::database_config config{.path = (directory.path / "catalog.db").string()}; + catalog_database() { std::filesystem::create_directories(assets / "songs"); } auto query(std::string sql, std::vector params = {}) const -> noto::query_result { - auto result = run_catalogue_operation(catalogue_query, config, std::move(sql), std::move(params)); + auto result = run_catalog_operation(catalog_query, config, std::move(sql), std::move(params)); BOOST_REQUIRE_MESSAGE(result, (result ? "" : result.error().describe())); return std::move(*result); } @@ -126,20 +126,20 @@ struct catalogue_database { BOOST_REQUIRE(output.good()); } auto build() const -> std::bool_t { - return vamp_test::build_catalogue(assets, config.path); + return vamp_test::build_catalog(assets, config.path); } auto performance() const -> std::expected { - return run_catalogue_operation(vamp_zero::async_load_performance_library, config, assets.string()); + return run_catalog_operation(vamp_zero::async_load_performance_library, config, assets.string()); } auto practice() const -> std::expected { - return run_catalogue_operation(vamp_zero::async_load_practice_library, config, assets.string()); + return run_catalog_operation(vamp_zero::async_load_practice_library, config, assets.string()); } auto score(vamp_zero::score_category category, std::string id) const -> std::expected { - return run_catalogue_operation(vamp_zero::async_load_score, config, vamp_zero::score_reference{category, std::move(id)}, assets.string()); + return run_catalog_operation(vamp_zero::async_load_score, config, vamp_zero::score_reference{category, std::move(id)}, assets.string()); } }; -auto catalogue_exercise(std::string authored = "Eb", std::string id = "001") -> vamp_zero::practice_exercise { +auto catalog_exercise(std::string authored = "Eb", std::string id = "001") -> vamp_zero::practice_exercise { auto result = vamp_zero::practice_exercise{ .id = std::move(id), .title = "Exercise", @@ -153,14 +153,14 @@ auto catalogue_exercise(std::string authored = "Eb", std::string id = "001") -> } -BOOST_AUTO_TEST_CASE(catalogue_snapshots_heartbeat_and_navigation_share_immutable_metadata) { +BOOST_AUTO_TEST_CASE(catalog_snapshots_heartbeat_and_navigation_share_immutable_metadata) { auto performance = vamp_zero::group_library_parts({ {.id = "other", .title = "A Tune", .composer = "Composer", .part = "Lead", .total_pages = 1}, {.id = "chosen", .title = "B Tune", .composer = "Composer", .part = "Lead", .total_pages = 2}, }); - auto practice = vamp_zero::group_practice_exercises({catalogue_exercise("Eb", "002"), catalogue_exercise("Eb", "001")}); + auto practice = vamp_zero::group_practice_exercises({catalog_exercise("Eb", "002"), catalog_exercise("Eb", "001")}); BOOST_REQUIRE(practice); - auto catalog = std::make_shared(vamp_zero::catalogue{std::move(performance), std::move(*practice)}); + auto catalog = std::make_shared(vamp_zero::catalog{std::move(performance), std::move(*practice)}); auto original = vamp_zero::app_state{ .catalog = catalog, .mode = vamp_zero::app_mode::PERFORMANCE_PART_MENU, @@ -211,7 +211,7 @@ BOOST_AUTO_TEST_CASE(catalogue_snapshots_heartbeat_and_navigation_share_immutabl BOOST_TEST(parts.selected_practice_part_index == 0U); } -BOOST_AUTO_TEST_CASE(catalogue_performance_groups_use_title_and_composer_with_part_indices) { +BOOST_AUTO_TEST_CASE(catalog_performance_groups_use_title_and_composer_with_part_indices) { auto library = vamp_zero::group_library_parts({ {.id = "other-composer", .title = "Shared Title", .composer = "Bea", .part = "Lead"}, {.id = "vocal", .title = "Shared Title", .composer = "Ada", .part = "Vocal"}, @@ -242,15 +242,15 @@ BOOST_AUTO_TEST_CASE(catalogue_performance_groups_use_title_and_composer_with_pa BOOST_TEST(library.parts[library.groups[1].parts[1]].id == "vocal"); } -BOOST_AUTO_TEST_CASE(catalogue_practice_requires_complete_ordered_parts_and_key_agreement) { +BOOST_AUTO_TEST_CASE(catalog_practice_requires_complete_ordered_parts_and_key_agreement) { for (auto authored : {"Eb", "C dorian", "No key", ""}) { - auto first = catalogue_exercise(authored); + auto first = catalog_exercise(authored); std::reverse(first.parts.begin(), first.parts.end()); auto last = first.parts.back(); first.parts.pop_back(); - auto fragment = catalogue_exercise(authored); + auto fragment = catalog_exercise(authored); fragment.parts = {last}; - auto grouped = vamp_zero::group_practice_exercises({catalogue_exercise("F", "002"), first, fragment}); + auto grouped = vamp_zero::group_practice_exercises({catalog_exercise("F", "002"), first, fragment}); BOOST_REQUIRE(grouped); BOOST_REQUIRE_EQUAL(grouped->exercises.size(), 2U); auto const &exercise = grouped->exercises[0]; @@ -262,8 +262,8 @@ BOOST_AUTO_TEST_CASE(catalogue_practice_requires_complete_ordered_parts_and_key_ } } for (auto conflict : {"F", ""}) { - auto first = catalogue_exercise(); - auto second = catalogue_exercise(conflict); + auto first = catalog_exercise(); + auto second = catalog_exercise(conflict); second.parts = {second.parts.back()}; first.parts.pop_back(); auto grouped = vamp_zero::group_practice_exercises({first, second}); @@ -271,21 +271,21 @@ BOOST_AUTO_TEST_CASE(catalogue_practice_requires_complete_ordered_parts_and_key_ BOOST_TEST(grouped.error().code() == static_cast(vamp_zero::error_code::INVALID_ARGUMENT)); BOOST_TEST(grouped.error().message().find("disagree on key") != std::string_view::npos); } - auto missing = catalogue_exercise(); + auto missing = catalog_exercise(); missing.parts.pop_back(); BOOST_TEST(!vamp_zero::group_practice_exercises({missing})); - auto duplicate = catalogue_exercise(); + auto duplicate = catalog_exercise(); duplicate.parts.back().part = "Acoustic"; BOOST_TEST(!vamp_zero::group_practice_exercises({duplicate})); } -BOOST_AUTO_TEST_CASE(catalogue_individual_score_lookup_uses_category_and_reports_missing_or_invalid) { - auto db = catalogue_database{}; - auto song = catalogue_bundle("Song", "F"); - auto exercise = catalogue_bundle("Exercise", "D", "Electric"); +BOOST_AUTO_TEST_CASE(catalog_individual_score_lookup_uses_category_and_reports_missing_or_invalid) { + auto db = catalog_database{}; + auto song = catalog_bundle("Song", "F"); + auto exercise = catalog_bundle("Exercise", "D", "Electric"); db.write("songs/shared.json", song); for (auto part : vamp_zero::practice_part_names()) { - db.write(std::filesystem::path{"practice/001"} / vamp_test::part_filename(part), catalogue_bundle("Exercise", "D", part)); + db.write(std::filesystem::path{"practice/001"} / vamp_test::part_filename(part), catalog_bundle("Exercise", "D", part)); } BOOST_REQUIRE(db.build()); db.query("UPDATE practice_exercises SET id = 'shared' WHERE part = 'Electric'"); @@ -317,9 +317,9 @@ BOOST_AUTO_TEST_CASE(catalogue_individual_score_lookup_uses_category_and_reports } } -BOOST_AUTO_TEST_CASE(catalogue_sequential_score_loads_keep_executor_stack_bounded) { - auto db = catalogue_database{}; - db.write("songs/repeated.json", catalogue_bundle("Repeated")); +BOOST_AUTO_TEST_CASE(catalog_sequential_score_loads_keep_executor_stack_bounded) { + auto db = catalog_database{}; + db.write("songs/repeated.json", catalog_bundle("Repeated")); BOOST_REQUIRE(db.build()); auto repeated_loads = [](boost::asio::any_io_executor executor, vamp_zero::database_config config, std::string assets) -> boost::asio::awaitable> { @@ -332,13 +332,13 @@ BOOST_AUTO_TEST_CASE(catalogue_sequential_score_loads_keep_executor_stack_bounde } co_return 4096U; }; - auto result = run_catalogue_operation(repeated_loads, db.config, db.assets.string()); + auto result = run_catalog_operation(repeated_loads, db.config, db.assets.string()); BOOST_REQUIRE_MESSAGE(result, (result ? "" : result.error().describe())); BOOST_TEST(*result == 4096U); } -BOOST_AUTO_TEST_CASE(catalogue_reload_uses_cached_metadata_without_parsing_score_json) { - auto db = catalogue_database{}; +BOOST_AUTO_TEST_CASE(catalog_reload_uses_cached_metadata_without_parsing_score_json) { + auto db = catalog_database{}; BOOST_REQUIRE(db.build()); // Complete cached metadata must not require parsing or retaining the score body. db.query("INSERT INTO songs(id, title, composer, source_json, part, source_path, " @@ -388,9 +388,9 @@ BOOST_AUTO_TEST_CASE(catalogue_reload_uses_cached_metadata_without_parsing_score BOOST_TEST(disagreement.error().message().find("disagree on key") != std::string_view::npos); } -BOOST_AUTO_TEST_CASE(catalogue_validation_rejects_missing_legacy_and_corrupt_files_without_repair) { - auto db = catalogue_database{}; - auto validate = [&] { return run_catalogue_operation(vamp_zero::async_validate_database, db.config, true); }; +BOOST_AUTO_TEST_CASE(catalog_validation_rejects_missing_legacy_and_corrupt_files_without_repair) { + auto db = catalog_database{}; + auto validate = [&] { return run_catalog_operation(vamp_zero::async_validate_database, db.config, true); }; BOOST_TEST(!validate()); BOOST_TEST(!std::filesystem::exists(db.config.path)); BOOST_REQUIRE(db.build()); @@ -419,11 +419,11 @@ BOOST_AUTO_TEST_CASE(catalogue_validation_rejects_missing_legacy_and_corrupt_fil BOOST_TEST(!std::filesystem::exists(db.config.path + "-journal")); } -BOOST_AUTO_TEST_CASE(catalogue_reads_readonly_database_without_json_and_resolves_relocated_assets) { - auto db = catalogue_database{}; - db.write("songs/tune.json", catalogue_bundle("Relocated")); +BOOST_AUTO_TEST_CASE(catalog_reads_readonly_database_without_json_and_resolves_relocated_assets) { + auto db = catalog_database{}; + db.write("songs/tune.json", catalog_bundle("Relocated")); for (auto part_name : vamp_zero::practice_part_names()) { - db.write(std::filesystem::path{"practice/001"} / vamp_test::part_filename(part_name), catalogue_bundle("Exercise", "Eb", part_name)); + db.write(std::filesystem::path{"practice/001"} / vamp_test::part_filename(part_name), catalog_bundle("Exercise", "Eb", part_name)); } BOOST_REQUIRE(db.build()); std::filesystem::remove(db.assets / "songs/tune.json"); @@ -433,7 +433,7 @@ BOOST_AUTO_TEST_CASE(catalogue_reads_readonly_database_without_json_and_resolves db.assets = moved; auto timestamp = std::filesystem::last_write_time(db.config.path); std::filesystem::permissions(db.config.path, std::filesystem::perms::owner_read); - BOOST_REQUIRE(run_catalogue_operation(vamp_zero::async_validate_database, db.config, true)); + BOOST_REQUIRE(run_catalog_operation(vamp_zero::async_validate_database, db.config, true)); auto songs = db.performance(); auto practice = db.practice(); auto score = db.score(vamp_zero::score_category::PERFORMANCE, "tune"); @@ -449,9 +449,9 @@ BOOST_AUTO_TEST_CASE(catalogue_reads_readonly_database_without_json_and_resolves BOOST_TEST(!std::filesystem::exists(db.config.path + "-journal")); } -BOOST_AUTO_TEST_CASE(catalogue_rejects_paths_outside_the_asset_root) { - auto db = catalogue_database{}; - db.write("songs/tune.json", catalogue_bundle("Fixture")); +BOOST_AUTO_TEST_CASE(catalog_rejects_paths_outside_the_asset_root) { + auto db = catalog_database{}; + db.write("songs/tune.json", catalog_bundle("Fixture")); BOOST_REQUIRE(db.build()); for (auto path : {"/tmp/tune.json", "../tune.json", "songs/../../tune.json", "songs/./tune.json", "songs\\tune.json", ""}) { if (std::string_view{path}.empty()) { diff --git a/tests/test_health.cpp b/tests/test_health.cpp index dcf4abb..ee1e894 100644 --- a/tests/test_health.cpp +++ b/tests/test_health.cpp @@ -29,7 +29,7 @@ #include #include -#include "catalogue_fixture.hpp" +#include "catalog_fixture.hpp" namespace { @@ -189,7 +189,7 @@ auto make_assets(vamp_zero::config const &config) -> void { name.front() = static_cast(name.front() - 'A' + 'a'); write_score(root / "practice" / "001" / (name + ".json"), std::string{part_name}); } - BOOST_REQUIRE(vamp_test::build_catalogue(root, config.database.path)); + BOOST_REQUIRE(vamp_test::build_catalog(root, config.database.path)); } auto run_once(std::filesystem::path const &root, vamp_zero::config const &config) -> std::expected { @@ -336,7 +336,7 @@ BOOST_AUTO_TEST_CASE(portal_remote_commands_refresh_connection_without_backgroun } } -BOOST_AUTO_TEST_CASE(readiness_reads_packaged_catalogue_without_hardware_or_heartbeat) { +BOOST_AUTO_TEST_CASE(readiness_reads_packaged_catalog_without_hardware_or_heartbeat) { auto root = temporary_directory{}; auto config = fixture_config(root.path); make_assets(config); @@ -399,7 +399,7 @@ BOOST_AUTO_TEST_CASE(readiness_validates_generated_audio_using_configured_asset_ auto score = boost::json::parse(read_file(path)); score.as_object()["playback"] = boost::json::parse(read_file(fixture / "score.json")).at("playback"); write_file(path, boost::json::serialize(score)); - BOOST_REQUIRE(vamp_test::build_catalogue(assets, config.database.path)); + BOOST_REQUIRE(vamp_test::build_catalog(assets, config.database.path)); auto ready = run_once(root.path, config); BOOST_REQUIRE_MESSAGE(ready, (ready ? "" : ready.error().describe())); auto &sample = score.at("playback").at("samples").as_array().front().as_object(); @@ -409,7 +409,7 @@ BOOST_AUTO_TEST_CASE(readiness_validates_generated_audio_using_configured_asset_ sample["sha256"] = checksum(corrupt); write_file(audio, corrupt); write_file(path, boost::json::serialize(score)); - BOOST_REQUIRE(vamp_test::build_catalogue(assets, config.database.path)); + BOOST_REQUIRE(vamp_test::build_catalog(assets, config.database.path)); ready = run_once(root.path, config); BOOST_REQUIRE(!ready); BOOST_TEST(std::string{ready.error().message()}.find("PCM16 WAV") != std::string::npos); @@ -437,7 +437,7 @@ BOOST_AUTO_TEST_CASE(readiness_rejects_corrupt_checksums_and_escaping_pages) { } } -BOOST_AUTO_TEST_CASE(readiness_rejects_missing_or_malformed_catalogues) { +BOOST_AUTO_TEST_CASE(readiness_rejects_missing_or_malformed_catalogs) { auto root = temporary_directory{}; auto config = fixture_config(root.path); BOOST_TEST(!run_once(root.path, config)); @@ -479,7 +479,7 @@ BOOST_AUTO_TEST_CASE(readiness_decodes_pages_even_when_checksums_match) { write_file(score_path.parent_path() / std::string{page.at("path").as_string()}, bytes); page["sha256"] = checksum(bytes); write_file(score_path, boost::json::serialize(score)); - BOOST_REQUIRE(vamp_test::build_catalogue(config.runtime.assets_path, config.database.path)); + BOOST_REQUIRE(vamp_test::build_catalog(config.runtime.assets_path, config.database.path)); auto ready = run_once(root.path, config); BOOST_REQUIRE_MESSAGE(!ready, kind); BOOST_TEST(ready.error().describe().find("first.pages/1.png") != std::string::npos); diff --git a/tests/test_music_model.py b/tests/test_music_model.py index fa37c5f..1c81807 100644 --- a/tests/test_music_model.py +++ b/tests/test_music_model.py @@ -22,9 +22,9 @@ class MusicModelTests(unittest.TestCase): engraving = patch('generator.__main__.engrave_tree') engraving.start() self.addCleanup(engraving.stop) - catalogue = patch('generator.__main__.build_catalogue') - catalogue.start() - self.addCleanup(catalogue.stop) + catalog = patch('generator.__main__.build_catalog') + catalog.start() + self.addCleanup(catalog.stop) 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}, 'C7b5': {0, 4, 6, 10}, 'G7b9': {7, 11, 2, 5, 8}, 'Fmaj7#11': {5, 9, 0, 4, 11}} @@ -35,7 +35,7 @@ class MusicModelTests(unittest.TestCase): pitches = chord_pitches(definition, chord_positions(*chord_requirements(definition))[0]) self.assertTrue(essentials.issubset({pitch_number(p) % 12 for p in pitches}), label) with self.assertRaises(ValueError): - harmony.parse_symbol('Cunrecognised') + harmony.parse_symbol('Cunrecognized') 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}): p['alter'] = rational(p['alter']) @@ -101,7 +101,7 @@ class MusicModelTests(unittest.TestCase): for text in ('Med.', 'Med. Swing', 'Medium', 'Medium Swing'): self.assertEqual(canonical_style(text), 'Medium Swing') with self.assertRaises(ValueError): - canonical_style('Unrecognised style') + canonical_style('Unrecognized style') def test_failure_preserves_generated_assets(self): with tempfile.TemporaryDirectory() as temp: root = Path(temp) diff --git a/tests/test_musicxml.py b/tests/test_musicxml.py index 0b6081a..07b0027 100644 --- a/tests/test_musicxml.py +++ b/tests/test_musicxml.py @@ -43,7 +43,7 @@ class MusicXMLTests(unittest.TestCase): score = load_musicxml(Path('tests/fixtures/notation.xml')) self.assertEqual(len(score['staves'][0]['measures']), 4) self.assertEqual(len(score['staves'][0]['octave_shifts']), 1) - def test_catalogue_imports(self): + def test_catalog_imports(self): sources = sorted(Path('data').rglob('*.xml')) self.assertTrue(sources) for path in sources: diff --git a/tests/test_parts.py b/tests/test_parts.py index bbd7fd7..466bf67 100644 --- a/tests/test_parts.py +++ b/tests/test_parts.py @@ -100,7 +100,7 @@ class PartGenerationTests(unittest.TestCase): (root / 'data' / category).mkdir(parents=True) (root / 'data' / category / (name + '.xml')).write_text(xml_score(xml_harmony() + xml_note())) args = argparse.Namespace(data_dir=root / 'data', output_dir=root / 'output', static_assets_dir=Path('package/assets'), check=False) - with patch('generator.arrange.generate.load_musicxml', wraps=load_musicxml) as song_load, patch('generator.practice.generate.load_musicxml', wraps=load_musicxml) as practice_load, patch('generator.parts.concert_document', wraps=concert_document) as normalize, patch('generator.__main__.engrave_tree'), patch('generator.__main__.build_catalogue'), patch('generator.__main__.compile_tree'): + with patch('generator.arrange.generate.load_musicxml', wraps=load_musicxml) as song_load, patch('generator.practice.generate.load_musicxml', wraps=load_musicxml) as practice_load, patch('generator.parts.concert_document', wraps=concert_document) as normalize, patch('generator.__main__.engrave_tree'), patch('generator.__main__.build_catalog'), patch('generator.__main__.compile_tree'): generate_assets(args) song_load.assert_called_once() practice_load.assert_called_once() diff --git a/tests/test_playback.cpp b/tests/test_playback.cpp index 076eea6..67d6ceb 100644 --- a/tests/test_playback.cpp +++ b/tests/test_playback.cpp @@ -22,7 +22,7 @@ auto playback_score() -> vamp_zero::score_document { } auto performance_state() -> vamp_zero::app_state { - auto catalog = std::make_shared(); + auto catalog = std::make_shared(); catalog->performance.parts.push_back({.id = "part", .title = "Song", .part = "Vocal", .total_pages = 2U}); catalog->performance.groups.push_back({.title = "Song", .parts = {0U}}); auto state = vamp_zero::app_state{}; @@ -71,7 +71,7 @@ BOOST_AUTO_TEST_CASE(playback_completion_is_session_guarded) { auto state = vamp_zero::control_state{{}, performance_state()}; state.finish_playback(41U); BOOST_CHECK(state.snapshot().mode == vamp_zero::app_mode::PERFORMANCE_SCORE); - state.finish_playback(42U, {}, std::make_shared()); + state.finish_playback(42U, {}, std::make_shared()); BOOST_CHECK(state.snapshot().mode == vamp_zero::app_mode::PERFORMANCE_SCORE); state.finish_playback(42U, "sample missing"); BOOST_CHECK(state.snapshot().mode == vamp_zero::app_mode::PERFORMANCE_SCORE); @@ -91,7 +91,7 @@ BOOST_AUTO_TEST_CASE(playback_completion_is_session_guarded) { BOOST_CHECK(quiet.snapshot().mode == vamp_zero::app_mode::PERFORMANCE_SCORE); } -BOOST_AUTO_TEST_CASE(playback_worker_discards_cancelled_preparation_and_reports_errors_once) { +BOOST_AUTO_TEST_CASE(playback_worker_discards_canceled_preparation_and_reports_errors_once) { auto engine = sonus::engine::create({.no_device = true}); BOOST_REQUIRE(engine); auto audio = vamp_zero::audio_controller{{}, std::move(*engine)}; diff --git a/tests/test_score_bundle.py b/tests/test_score_bundle.py index 574a40a..acd8005 100644 --- a/tests/test_score_bundle.py +++ b/tests/test_score_bundle.py @@ -158,7 +158,7 @@ class ScoreBundleTests(unittest.TestCase): source['staves'][0]['instruments'] = [] with self.assertRaisesRegex(ValueError, 'unknown score instrument'): validate_document(source) - def test_catalogue_roundtrip_preserves_written_pitch_and_rhythm(self): + def test_catalog_roundtrip_preserves_written_pitch_and_rhythm(self): for path in Path('data').rglob('*.xml'): with self.subTest(path=path): source = load_musicxml(path) diff --git a/tests/test_score_loader.cpp b/tests/test_score_loader.cpp index 2ad082b..1ecded5 100644 --- a/tests/test_score_loader.cpp +++ b/tests/test_score_loader.cpp @@ -64,7 +64,7 @@ struct controlled_scores { struct loader_fixture { boost::asio::io_context io{}; std::shared_ptr scores{std::make_shared(io)}; - std::shared_ptr catalog{std::make_shared()}; + std::shared_ptr catalog{std::make_shared()}; std::shared_ptr completions{std::make_shared()}; vamp_zero::score_loader loader{io.get_executor(), [scores = scores](vamp_zero::score_reference reference) { return scores->load(std::move(reference)); }, @@ -147,11 +147,11 @@ BOOST_AUTO_TEST_CASE(score_loader_stale_error_does_not_replace_pending_selection BOOST_CHECK_EQUAL(fixture.loader.snapshot().document->id, "available"); } -BOOST_AUTO_TEST_CASE(score_loader_catalogue_identity_and_category_invalidate_selection) { +BOOST_AUTO_TEST_CASE(score_loader_catalog_identity_and_category_invalidate_selection) { auto fixture = loader_fixture{}; fixture.loader.select(fixture.catalog, performance("same-id")); drain(fixture.io); - fixture.catalog = std::make_shared(); + fixture.catalog = std::make_shared(); fixture.loader.select(fixture.catalog, performance("same-id")); fixture.scores->complete(0); drain(fixture.io); @@ -279,7 +279,7 @@ BOOST_AUTO_TEST_CASE(score_loader_destruction_during_load_does_not_call_consumer auto loader = vamp_zero::score_loader{io.get_executor(), [scores](vamp_zero::score_reference reference) { return scores->load(std::move(reference)); }, [&] { ++completions; }}; - loader.select(std::make_shared(), performance("a")); + loader.select(std::make_shared(), performance("a")); drain(io); BOOST_REQUIRE_EQUAL(scores->requests.size(), 1U); } @@ -301,7 +301,7 @@ BOOST_AUTO_TEST_CASE(score_loader_completion_can_overlap_stop_and_consumer_destr if (auto target = weak.lock()) { ++*target; } }); - loader->select(std::make_shared(), performance("a")); + loader->select(std::make_shared(), performance("a")); auto worker = std::thread{[&] { io.run(); }}; auto entered_status = entered_future.wait_for(std::chrono::seconds{5}); if (entered_status == std::future_status::ready) { @@ -317,7 +317,7 @@ BOOST_AUTO_TEST_CASE(score_loader_completion_can_overlap_stop_and_consumer_destr } BOOST_AUTO_TEST_CASE(selected_score_reference_uses_active_part_and_practice_part) { - auto catalog = std::make_shared(); + auto catalog = std::make_shared(); catalog->performance.parts = {{.id = "part-a"}, {.id = "part-b"}}; catalog->practice.exercises = {{.id = "scale", .parts = {{.id = "scale-c"}, {.id = "scale-d"}}}}; auto state = vamp_zero::app_state{.catalog = catalog};