220 lines
34 KiB
Markdown
220 lines
34 KiB
Markdown
# Product Vamp Technical Guide
|
|
|
|
## Repository Shape
|
|
|
|
Product Vamp follows the Modulus product layout used by the khodo repos:
|
|
|
|
- `include/vamp_zero` and `src/vamp_zero`: Pi Zero C++23 application.
|
|
- `include/vamp_pico` and `src/vamp_pico`: Pi Pico 2 W C++17 firmware.
|
|
- `include/vamp_protocol` and `src/vamp_protocol`: shared route and page-command contracts used by both devices.
|
|
- `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 preprocessing for performance arrangements and practice variants.
|
|
- `package`: generic bundle inputs, scripts, config, static font assets, and device install files.
|
|
|
|
## Zero Runtime
|
|
|
|
The Zero process can run headless for local/API quick checks or with Luma for the Waveshare Zero-DISP-7A display. The API is plain HTTP on the private AP:
|
|
|
|
- `GET /api/status`
|
|
- `GET /api/health`
|
|
- `GET /api/remote/display`
|
|
- `POST /api/remote/heartbeat`
|
|
- `POST /api/remote/next`
|
|
- `POST /api/remote/previous`
|
|
- `POST /api/remote/select`
|
|
|
|
Legacy `/api/page/next` and `/api/page/previous` routes are still accepted for simple manual checks.
|
|
|
|
The AP setup pins NetworkManager to 2.4 GHz channel 6 with WPA2/RSN and CCMP ciphers so the Pico W can join using `CYW43_AUTH_WPA2_AES_PSK`.
|
|
|
|
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. Runtime navigation and rendering use that loaded Noto-backed state instead of rescanning asset folders. The UI supports a top-level mode menu, Performance song/part/score menus, and Practice exercise/key/score menus. Every practice exercise offers twelve pre-generated key variants; performance songs have no key-selection menu. The score view displays build-time engraved PNG pages. Musical interpretation, spacing, beaming, ties, accidentals, and pagination happen on the build host, not on the Pi. The view 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.
|
|
|
|
Practice exercise IDs are the three-digit positive source stems, such as `001`. Variant IDs append the canonical key stem, such as `001-db`. Duplicate titles and identical placeholder music remain independent exercises. Catalogue schema 2 has no practice-type or lesson-group columns, and control responses expose one flat exercise list without nested type/lesson copies. Older catalogues 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.
|
|
|
|
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.
|
|
|
|
## Score Generation
|
|
|
|
The build runs `python3 -m generator` against `data/` and writes build-only assets under the CMake binary tree. Performance source songs become acoustic, electric, and vocal arrangements. Numbered practice sources are emitted in all 12 keys at build time. Generated JSON keeps notation metadata while removing generator-only optional chord markers. The catalogue produces three bundles per song and twelve per practice exercise; source counts are discovered, not fixed in code.
|
|
|
|
Practice sources live directly under `data/` with positive three-digit filenames, currently `001.xml` through `366.xml`. For example, `data/001.xml` generates `practice/001/{c,db,d,eb,e,f,gb,g,ab,a,bb,b}.json` and corresponding page directories. Performance sources stay in `data/songs/`. The filename number determines identity and ordering; MusicXML work/movement titles determine labels, falling back to the number when absent. Other root XML names and nested practice sources are rejected. Adding, renaming, or removing a numbered source requires no registry edit; the next build replaces obsolete generated assets and rebuilds the database. The current catalogue has 366 exercises, 4,392 practice variants, and six performance parts.
|
|
|
|
The pipeline is MuseScore -> uncompressed `.xml` -> musical model -> automatic arranger/transposer -> semantic JSON plus engraved pages. Export MusicXML 3.1 or 4.0, `score-partwise`, with one part and one staff. Independent rhythmic voices remain separate; simultaneous chord tones retain their own spelling, written duration, and tie links. The packaged music describes each transformed part or practice variant, not an archival copy of the source XML.
|
|
|
|
### MusicXML Contract
|
|
|
|
| Area | Accepted notation |
|
|
| --- | --- |
|
|
| Pitch | Exact rational letter/alteration/octave spelling, including microtones, double/triple accidentals, and courtesy accidentals. B-sharp and C-flat retain the correct octave. |
|
|
| Rhythm | All fourteen MusicXML types, maxima through 1024th; up to sixteen augmentation dots; exact positive rational metric durations and separately stored written values. Time modifications must reconcile the two. |
|
|
| Chords and rests | Independent voice lanes, simultaneous chord tones, shorter additional chord tones, rests, full-measure rests, hidden events, forward spacers, and rhythm slashes. |
|
|
| Ties and tuplets | Per-pitch ties, including partial chord ties; nested and cross-bar tuplet spans. Nested tuplets require explicit `tuplet-actual` and `tuplet-normal` numbers. |
|
|
| Small notes | Grace notes with zero metric duration, cue notes, and measured single/paired tremolos. |
|
|
| Staff state | G/F/C, percussion, TAB, and no-clef staves; tablature string/fret/tunings; octave clefs and octave-shift spans; standard major/minor/modal keys or custom seven-letter alterations; key, clef, and meter changes. |
|
|
| Meter | Simple, compound, additive, mixed-denominator, and unmetered timelines. A partial measure must be marked `implicit="yes"`; an initial implicit measure is the arrangement pickup. Leading rests are retained. |
|
|
| Harmony | Structured roots, kinds, inversions, slash basses, and degree additions/alterations/subtractions. Harmony events have their own offsets, including inside held notes. Their positions never split or shorten notes. |
|
|
| Text | Title, composer, part name, multiple lyric verses with syllabification, elisions, and extenders, rehearsal labels at measure starts, and Vamp directives. |
|
|
| Tempo | Metronome directions and positive quarter-note BPM from `<sound tempo="...">`, either within a direction or directly in a measure. Playback/navigation attributes on `sound` are rejected. |
|
|
| Repeats | Left forward and right backward bar repeats, explicit repeat counts, implicit start at the beginning, sequential/nested repeat groups, and contiguous numbered endings. |
|
|
|
|
Multiple staves/parts, D.C./D.S./coda/fine navigation, slurs, dynamics, articulations, and expressive ornaments are outside the product contract and rejected. Unknown musical elements also fail. Serialized times and alterations use reduced signed-64-bit numerators and positive denominators; pitch octaves are -4 through 16, clef/octave shifts span at most three octaves, and meter counts are 1-1024 with denominator units 1-4096. These resource bounds produce diagnostics, not clamping or truncation.
|
|
|
|
Layout/exporter information such as page defaults, credits, print layout, font/color/position attributes, and source pagination does not determine the small-screen layout. Engraving is regenerated. Beam groups and written note values are retained; unrelated engraving metadata does not authorize ignoring musical descendants. `defusedxml==0.7.1` forbids XML entities and external access. Ordinary MusicXML DOCTYPE declarations are allowed without fetching their DTDs.
|
|
|
|
Chord display text is not a substitute for structured harmony. Some Soundslice exports encode minor chord labels as `<kind text="-7">other</kind>` without any degrees; these fail import. Use `minor-seventh` for `-7`, `half-diminished` for `-7b5`, `major-minor` for `-maj7`, and `minor-sixth` for `-6`, keeping the `text` attribute if desired. MusicXML [`other`](https://www.w3.org/2021/06/musicxml40/musicxml-reference/data-types/kind-value/) requires explicit added `<degree>` elements defining the chord. Correct these exports before placing them in `data/`; the importer never guesses pitches from an incomplete chord label.
|
|
|
|
### Vamp Metadata
|
|
|
|
In MuseScore's score properties, set `vamp:style` to a supported player style. It exports as standard MusicXML miscellaneous metadata:
|
|
|
|
```xml
|
|
<miscellaneous>
|
|
<miscellaneous-field name="vamp:style">Medium Swing</miscellaneous-field>
|
|
</miscellaneous>
|
|
```
|
|
|
|
The default is Medium Swing; `Med.`, `Med. Swing`, and `Medium` remain aliases. A keyless exercise needs `vamp:tonic` (for example, `C`) as its transposition reference. Modes and local key changes are preserved when generating practice variants. Keyless variants remain unkeyed in the music and show their selected transposition reference in menus, for example `D (no key)`.
|
|
|
|
Place staff text `vamp:optional-chord` at the same onset as exactly one harmony to mark a turnaround omitted from the final arrangement section. The text may be hidden. Parenthesizing an ordinary chord does not make it optional. Orphaned/duplicate markers and unknown `vamp:` properties are errors; consumed directives never enter runtime JSON.
|
|
|
|
### Repeats
|
|
|
|
`generator/repeats.py` is the shared repeat analyzer and expander. Practice keeps compact authored bars and ending brackets; the engraver draws repeat dots, combined boundaries, repeat-count labels, and brackets continued across systems. Performance sources unfold before the existing seven-section arranger. Playback and guitar-feasibility checks use that same performed order, so skipped endings do not become extra notes.
|
|
|
|
Ending numbers are comma-separated positive passes, such as `1,2` followed by `3`. The first ending terminates at the backward repeat; following endings are contiguous, each pass belongs to exactly one ending, and the final ending contains the last pass. `stop` closes the bracket; `discontinue` leaves it open. Hidden brackets still affect performance. Explicit repeat counts must agree with ending passes. Crossing or unterminated groups, overlapping endings, middle-measure repeat marks, winged repeats, and navigation attributes are rejected. Nesting is bounded to 16 levels and unfolding to 10,000 measures.
|
|
|
|
Repeated notation retains separate sounding `<tie>` and printed `<tied>` markers. `time-only` selects passes in the innermost repeat, and each active sounding endpoint must match on the performed path. Use pass-specific ties when tying across a backward jump; dangling or unreachable endpoints fail rather than sustaining the wrong pitch. Expansion assigns fresh IDs and remaps ties, tuplets, and paired tremolos. A jump that splits a paired tremolo is rejected. Backward jumps restore staff and harmony state; skipped endings do not leak key changes into later passes. See the MusicXML references for [repeats](https://www.w3.org/2021/06/musicxml40/musicxml-reference/elements/repeat/) and [sounding ties](https://www.w3.org/2021/06/musicxml40/musicxml-reference/elements/tie/).
|
|
|
|
### Runtime Schema
|
|
|
|
A newly generated bundle has four top-level fields:
|
|
|
|
- `schema_version: 3`.
|
|
- `music`: metadata and a single staff containing measures, independent voice lanes, timed harmonies/attributes/tempos, instruments, tuplets, and octave spans.
|
|
- `presentation`: engraver/version, style checksum, fixed width/height, and ordered relative PNG paths with SHA-256 checksums.
|
|
- `playback`: versioned availability, exact performed beat duration/events, shared PCM sample descriptors, and a generated release envelope. Unavailable interpretations carry a reason instead of an approximate timeline.
|
|
|
|
Practice metadata includes optional `authored_key_signature`, captured before transposition and shared by all twelve variants. The menu's `SOURCE` label uses this value, not the first variant. Existing bundles without it remain supported and omit the source label. Explicit key-cancellation signatures transpose with the same interval as the music; the engraver cancels only altered pitches that actually change.
|
|
|
|
Measure durations, event onsets/durations, harmony offsets, span times, and pitch alterations use `{ "numerator": N, "denominator": D }`. Times are in quarter-note units and alterations in semitones. There is no tick grid, display-duration surrogate, or runtime flattening into one voice. Written note types, dots, and time modifications remain distinct from metric duration. Every event owns a voice; every chord tone has its own ID, written duration, duration, and reciprocal tie links. Tuplets reference events and belong to one voice.
|
|
|
|
The arranger normalizes instrument transposition to concert pitch and preserves all source voices in source-copy sections. Generated guitar rhythms occupy one lane. The seven-section form, optional turnaround handling, and twelve practice keys remain automatic. Generated guitar voicings and practice validation share standard six-string tuning (E2 A2 D3 G3 B3 E4), frets 0-20 inclusive, and a maximum four-fret difference between fretted positions; open strings do not count toward that span. The arranger searches all candidates in that range and retains its existing deterministic scoring and voicing policy. Unplayable performance harmonies still produce a warned rhythmic-slash fallback without changing the structured chord meaning or quantizing microtones.
|
|
|
|
Practice transposes from the declared source key, not from its first chord. Sources currently declare C major, but the importer does not force C or discard modes and local key changes. For example, a C-key exercise beginning with G7 produces A7 when D is selected. It preserves every authored tone, interval, inversion, rhythmic voice, tie, and tuplet. The closest transposition is tried first, followed by whole-exercise octave alternatives ordered by absolute displacement, with upward movement preferred on ties. Register adjustment does not change harmony symbols or key signatures. Sustained and tied tones keep their strings as other voices enter. This is a fretboard feasibility check, not a guarantee of comfortable fingering, and does not apply the performance arranger's chord-tone omissions or revoicing.
|
|
|
|
For the fretboard check, ordinary unmarked treble notation uses the project's written-guitar convention (one octave above sounding pitch). An explicit MusicXML instrument transposition is applied instead, once, including an explicit zero interval; the importer records this distinction with `transpose.explicit`. Octave-clef and TAB pitches do not receive the implicit octave subtraction. Nonstandard tunings, capo configurations, microtonal frets, or exercises that cannot retain their voicings in every key fail explicitly. Errors identify the source, target key, and failing passage; existing generated assets are preserved. No notes are omitted or revoiced to make a key fit.
|
|
|
|
Automatic performance arrangements require a pitched, non-TAB lead. Percussion and tablature remain supported in the musical model and engraver, but performance conversion rejects them rather than reinterpreting their pitches. All practice exercises now require pitched, standard-tuning guitar material. Transposition preserves authored TAB strings and validates the resulting exact integer frets against 0-20; it never invents new TAB. The old unchecked melody-practice path is removed.
|
|
|
|
### Playback Boundary
|
|
|
|
All musical compilation and tone synthesis run in Python (`generator/playback.py`). Each transformed score receives a sorted timeline of independent tone events with rational quarter-note `onset` and `end` positions. Ties become one continuous attack. Rests and spacers remain silence, including trailing silence. Harmony labels are not auto-accompaniment: only pitches actually present in the selected arrangement or exercise sound. Cue notes are silent, hidden pitched notes sound, and pitchless rhythm slashes are silent.
|
|
|
|
The `soft-struck-1` profile generates shared mono 48 kHz PCM16 WAVs with a short attack/decay and a seamless sustain loop, plus a shared 50 ms release envelope. Exact sounding pitches, including supported microtones, determine samples. Explicit instrument transposition is applied once; all practice and generated guitar parts use the existing written-guitar convention. Samples are shared across all scores and voices by content identity, not duplicated per complete performance. There are no SoundFonts, instrument sample bundles, oscillators, or MusicXML parsing at runtime.
|
|
|
|
Grace groups honor explicit steal-time-previous/following or make-time; unspecified groups share 25 percent of the following event. Conflicting grace modes, trailing grace groups, unsupported unpitched sound, tied measured tremolo, and incompatible paired tremolo make playback explicitly unavailable while retaining the notation. Measured tremolo uses written subdivision and tuplet ratios. The tone range is 20-18,000 Hz. Asset validation checks containment, SHA-256, PCM format/length, loop endpoints, envelope shape, event ordering, references, and rational timing. Generation remains transactional if validation fails.
|
|
|
|
At runtime the shared on-demand score loader serves both display and audio. A background worker validates and loads only the selected score's samples, converts absolute beat positions to rounded sample frames at the menu BPM, and prepares a Sonus sequence. Pitch does not change with BPM, and rounding absolute positions avoids cumulative timing drift. The I/O loop publishes the prepared sequence and polls completion without doing disk I/O or musical interpretation. Sonus mixes preallocated voices from immutable PCM and retires them on control threads; the callback never synthesizes tones or loads files. Limits are one million events, 512 distinct samples, 64 MiB decoded samples per selection, 64 simultaneous voices including releases, and 24 hours at the selected BPM. Exceeding them reports an error rather than dropping notes.
|
|
|
|
`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; the menu BPM is constant throughout a session. Selecting a practice variant or performance part starts a new session. Manual page changes never restart or seek. Natural completion, including trailing silence/release, returns to the corresponding key/part menu with its selection preserved. Leaving the score cancels playback; stale load/completion results cannot close a newer score. Errors stay in the score view and `/api/status` rather than being mistaken for completion. Existing bundles without `playback` still display but require regeneration to play.
|
|
|
|
`--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.
|
|
|
|
### Engraving Boundary
|
|
|
|
`generator/engraving/` is a custom Python engraver, not a wrapper around MuseScore or another notation engine. `layout_score(document, style, fonts)` produces immutable layout pages with exact source intervals, notation anchors and drawing primitives. `rasterize_page(page, fonts)` draws one page at four-times resolution and downsamples it. `engrave_tree(root, fonts)` retains the asset-pipeline entry point. `generator/musicxml_writer.py` remains a round-trip/export utility and is not involved in engraving.
|
|
|
|
`notation.py` interprets staff state, exact pitch positions and shared accidental state; `notes.py` and `attributes.py` construct symbols; `spans.py` handles beams, ties, tuplets, octave shifts and lyric continuations; `layout.py` handles spacing and pagination. `fonts.py` reads bundled OTF outlines and SMuFL metadata, caching metrics and outlines for one generation run. `raster.py` uses FontTools, aggdraw and Pillow only as low-level font and drawing libraries. All dependencies are pinned in `generator/requirements.txt` and remain build-only. Standard glyph names and their upstream notice are vendored alongside the fonts; no system music fonts are consulted.
|
|
|
|
The former backend-specific four-voice, duration, unequal-tone and tuplet restrictions are removed. Voices retain separate stems and collision offsets; chord tones retain their own written durations and per-tone ties. Pitched, percussion and explicitly fingered TAB material use their own staff positions. Unknown exact microtonal alterations use a numeric rational semitone annotation relative to the natural pitch, not a rounded accidental. An explicit unavailable SMuFL glyph is an error. The single-part/single-staff importer contract and exclusions for dynamics, accents, expressive ornaments and navigation remain unchanged.
|
|
|
|
`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.
|
|
|
|
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.
|
|
|
|
Diagnostics identify the file, part, measure, and unsupported element or invalid relationship. Examples include `part P1, measure 4: unsupported musical element <slur>`, `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.
|
|
|
|
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).
|
|
|
|
Generator checks compare the generated performance summary against `data/expected/performance_summary.json`. The summary stores compatibility invariants such as section layout, chord-label placement, slash-section placement, and vocal timing without committing the generated score files themselves. Refresh that file only when an intentional arrangement-logic change should become the new baseline.
|
|
|
|
## Pico Runtime
|
|
|
|
The Pico firmware uses the Pico SDK C++ stack and the `pico2_w` board definition. It initializes the ST7789T3 display over SPI, reads the CST816D touch controller over I2C, joins the Zero AP, polls `/api/remote/display`, and sends HTTP POST commands for gestures.
|
|
|
|
The display is rotated 90 degrees clockwise into a 320x240 landscape UI. The top status bar uses the shared palette:
|
|
|
|
- Blue: connecting.
|
|
- Yellow: retrying or remote request failed.
|
|
- Green: connected to the Zero API.
|
|
|
|
The body stays blank until a performance or practice score is active, then it shows the active title, selected part or active practice key, and page count. Touch controls are tap for next, double tap for previous, and tap-and-hold for select.
|
|
The Pico text renderer uses an antialiased bitmap atlas generated from `package/assets/fonts/petaluma/PetalumaScript.otf`. The shared font CMake rule generates `generated/vamp_style/font.cpp` separately in each native/Pico build directory, using `generator/generate_pico_font.py --output <path>`. Font and generator changes trigger regeneration. The generated atlas includes ASCII kerning pairs so display text such as `VAMP` uses the same spacing as the source font. The pinned Pillow build dependency is declared in `generator/requirements.txt`.
|
|
|
|
The root `CMakeLists.txt` is also the Pico firmware entrypoint when configured with `-DVAMP_PICO_FIRMWARE_PROJECT=ON`. That mode returns before the Zero/Linux targets are declared, so the Pico build can use the embedded ARM toolchain cleanly.
|
|
|
|
Build defaults can be overridden at package time with CMake cache variables:
|
|
|
|
- `VAMP_PICO_WIFI_SSID`
|
|
- `VAMP_PICO_WIFI_PASSPHRASE`
|
|
- `VAMP_ZERO_HOST`
|
|
- `VAMP_ZERO_PORT`
|
|
|
|
## Packaging
|
|
|
|
Modulus packages the CMake install stage. Product Vamp uses `install_prefix` `.` so the registry artifact is a top-level multi-device bundle rather than a root filesystem overlay.
|
|
|
|
The ARM64 package target builds:
|
|
|
|
- `zero/product-vamp-zero-linux-arm64.docker.tar`
|
|
- `pico/product-vamp-pico2w.uf2`
|
|
- `manifest.json`
|
|
|
|
Building the full package requires Docker/buildx, the ARM embedded GCC toolchain, and the Pico SDK. CI passes `PICO_SDK_PATH` directly. The generated Modulus Docker image also carries the pinned Pico SDK at `/opt/pico-sdk` for local package builds. The Zero Docker runtime targets Debian trixie to match current Raspberry Pi OS and the ARM64 dependency binaries, and includes `util-linux` for the `setsid --wait --ctty` launch path.
|
|
|
|
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.
|
|
|
|
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.
|
|
|
|
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.
|
|
|
|
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.
|
|
|
|
## Shared Protocol
|
|
|
|
The Pico firmware does not link Zero-side Linux application libraries such as Ligo, Luma, or Noto. Those remain Zero-side dependencies: Ligo serves the HTTP API, Luma renders the Zero display, and Noto owns SQLite persistence. Cross-device code should stay in `vamp_protocol` and `vamp_style`: plain C++17 with no OS, allocator-heavy, or Zero-only dependencies.
|
|
|
|
Luma owns the application shell, UI control tree, texture loading, and UI-thread texture lifetime. Product Vamp's page cache uses the uncached loading API; there is no score-specific Raylib font cache or handwritten notation renderer.
|
|
|
|
## LSP
|
|
|
|
`tooling.editor.clangd` in `modulus.json` generates the ignored `.clangd`, pointing native Zero and shared protocol code at `build/build_x86_debug`, and Pico code at `build/build_pico_debug`. Refresh both compile databases with:
|
|
|
|
```sh
|
|
python3 modulus.py lsp
|
|
```
|
|
|
|
The `tooling.lsp` section in `modulus.json` owns the Pico CMake configure. It unsets host compiler variables before configuring the Pico SDK project so clangd sees the embedded ARM toolchain. When running directly outside the generated Modulus Docker image, set `PICO_SDK_PATH` before refreshing the Pico database.
|