27 KiB
Product Vamp
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 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 services use Ligo for the HTTP API, Luma for touch UI rendering, Sonus for audio playback, and Modulus for shared build/runtime utilities.
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.
Device controls:
On the Zero, tap menu rows and switches directly; Tempo has minus/plus buttons. The main menu offers Performance and Settings.
On the Pico (and on the Zero while viewing a score):
- Tap: next menu item or next score page.
- Double tap: previous menu item or previous score page.
- Tap and hold: select the focused item, enter/leave tempo editing, or return from a score to its part menu.
The HTTP service is device-only: Pico controls, title artwork, and health checks. There is no browser remote.
Build
Configure and build the native development target:
python3 modulus.py configure
python3 modulus.py build
Release verification:
python3 modulus.py configure --target x86_64_gcc12_release
python3 modulus.py build --target x86_64_gcc12_release
python3 modulus.py test --target x86_64_gcc12_release
python3 modulus.py python -m unittest discover -s tests -p 'test_*.py'
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 read public packages anonymously from the khodo Conan registry, even when the project cache contains an expired login. configure --update refreshes both the exported Modulus tooling and Conan dependencies. --no-remote uses only locally cached packages. Publishing is the only operation that needs credentials. CMake checks the required Sonus and Luma APIs during configuration and reports stale dependencies early.
ARM builds use the versioned GCC 12 cross compilers. The selected compiler executables also identify internal Conan binaries, excluding old packages built with an unversioned compiler that could resolve to GCC 13. The first corrected build rebuilds missing internal packages; third-party caches remain reusable.
modulus.py is an unchanged copy of Modulus's canonical bootstrap. The project-specific build image lives in package/Dockerfile.build; tooling.docker declares its additional inputs and permits Docker socket access only for build/package commands that assemble the Zero image.
Refresh clangd compile databases, including the Pico firmware database, with:
python3 modulus.py lsp
When running directly outside the generated Modulus Docker image, set PICO_SDK_PATH before refreshing the Pico database.
Editor settings live in modulus.json under tooling.editor. Modulus generates the ignored .clangd, .clang-format, and pyrightconfig.json files; edit the JSON settings rather than these outputs. The clangd configuration routes Zero/shared code to the native compile database and Pico code to its firmware database.
Modulus also generates .nvim.lua for Neovim 0.11+ with exrc enabled. Review and trust it when Neovim asks; it preserves the existing LSP capabilities/keybindings and runs clangd 19 in the build container for this repo. Run python3 modulus.py lsp once to prepare both databases and the shared read-only header cache. Other editors can use python3 modulus.py clangd as their language-server command. Startup never downloads or builds anything, and never falls back to host clangd.
tooling.lsp.clangd configures the server executable, arguments, trusted compiler paths, and additional header roots. Product Vamp adds the bare-metal ARM compiler and /opt/pico-sdk; standard-library and SDK definitions open from an image-keyed cache under $XDG_CACHE_HOME/modulus/clangd (default ~/.cache/modulus/clangd). Set tooling.editor.nvim.enabled to false for a hand-managed editor setup. Existing unowned .nvim.lua files are preserved.
Package
Product packaging targets Raspberry Pi OS/Debian trixie on ARM64. The configured package builds both the Zero Docker image and Pico firmware. The build container includes the Pico SDK; when building directly outside that container, set PICO_SDK_PATH:
PICO_SDK_PATH=/path/to/pico-sdk python3 modulus.py package --update
The package command creates:
build/packages/product_vamp_arm_64_gcc12_release.tar.gz
build/packages/product_vamp_arm_64_gcc12_release.tar.gz.sha256
The tarball contains the Zero Docker archive, Pi install scripts, generated catalog database, pages, audio, fonts, Pico title artwork, and Pico UF2 firmware. Complete score JSON is stored inside catalog.db; individual JSON files remain build intermediates and are not deployed. Builds replace the same target-specific archive without a release label. The manifest and vamp_zero --build-info identify the source commit.
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.
The installer probes the incoming image before removing existing application data. A failed replacement leaves Product Vamp stopped and reports an error; rerun the installer after fixing the cause. Normal service restarts do not perform installation cleanup. No automatic backup of the previous installation is retained.
Publishing the rolling generic package requires credentials only for upload:
PACKAGE_REGISTRY_USER=<user> \
PACKAGE_REGISTRY_TOKEN=<token> \
python3 modulus.py publish
Artifacts are published to the khodo generic package registry's required latest slot. Publish the updated Modulus tooling before running Product Vamp CI so packaging and publishing agree on the stable filenames. CI checks this contract immediately after configure using a tiny fixture archive, without compiling or contacting the registry. A failure here means the exported Modulus packager needs updating; rerun the full workflow after publishing Modulus, not just the upload step.
Score Assets
Uncompressed MusicXML 3.1/4.0 source scores exported from Soundslice or MuseScore live in data/songs/. Generated score JSON is not committed.
data/songs/ supplies the original melody and harmony. data/composition/ holds guitar/difficulty settings, sparse accompaniment figures, and harmonic-validation policies. data/training/manifest.json registers authored solos and their source/slot pairings; data/models/solo.json holds the frozen pooled prediction model. The former scale, interval, and rhythm catalogs and handwritten phrase composer have been removed.
Six roles generate independently: acoustic accompaniment/solo; electric accompaniment/melody/solo; vocal melody. A process pool and per-role caches support parallel and incremental builds. Each role uses the source and its own inputs without reading another generated role. Canonical accompaniment repeats at corresponding source-form positions and does not react to generated solos. Vocal melody preserves the authored notes and lyrics. Electric melody preserves source pitches and harmonizes eligible sustained notes, reattacking them at chord changes.
Electric accompaniment and electric melody share compact chord families on strings 1–4. Melody harmonization adapts these to keep the original melody on the lowest-numbered feasible string, preferring it above the added chord tones. The separate Electric Solo stays monophonic. Acoustic accompaniment uses one bass note on string 5 or 6 and three upper notes on strings 2, 3 and 4. Transposable degree families cover triads, sevenths/sixths, suspensions, diminished/augmented and extended/altered chords, with root/fifth bass alternatives on both bass strings. Plain major/minor chords also allow third basses; explicit slash basses and inversions take priority. Unplayable variants are discarded. Each instrument selects a connected voicing path across the source form to reduce hand movement, then reuses it through the arrangement.
Accompaniment uses independently authored two-bar jazz pattern families, chosen against each song's original melody and harmonic rhythm. Acoustic repeats two patterns (A–B); electric repeats four (A–B–C–D). Occasional source-phrase-ending fills recur at the same positions in every iteration; both instruments allow at most one per eight full measures, including the repeat join. Acoustic normally has 1–3 attack positions per full bar with quarter/eighth bass and upper-chord notes played together or in sequential pairs. Electric has distinct patterns with normally 0–2 attacks per bar, stabs, sustains and space. Cross-bar holds use ties; repeated attacks remain attacks. Pattern phase follows source-form position, including partial entries and odd-length forms. Meter changes use compatible cycles, with explicit diagnostics if fewer distinct patterns are available. Tempo changes do not reshuffle the accompaniment.
Each instrument has 24 distinct two-bar patterns in 4/4, 24 in 3/4, and 24 for each of the 3+2 and 2+3 groupings in 5/4. Written additive grouping takes precedence. Otherwise, the original chord changes and melody choose one consistent 5/4 grouping per continuous meter section, shared as source analysis by the independently generated parts. Explicit meter matching prevents treating 6/8 as 3/4. The learned solo model and playback count-in are unchanged by this accompaniment expansion.
Both accompaniment parts balance movement with sounding register and a soft cost for mean fret positions above nine; comfortable shifts can win over staying high. Acoustic uses a register center of MIDI 53 and electric uses 60. Compact spans and preferred chord families remain higher priorities, including for the final sustained chord. Electric melody's supporting chords use the same balance after the existing melody-string and foreground priorities; the original melody pitches and rhythm remain fixed. Voicing preferences do not reseed accompaniment rhythms.
Acoustic attacks normally play bass and upper chord together. Consecutive attacks exactly an eighth note apart split into bass–upper pairs within the same harmony; an odd leftover attack plays both together. Pairing continues across barlines and two-bar pattern boundaries, but stops at chord changes, chorus boundaries and source-defined entries/handoffs. The first attack for each new chord therefore includes bass, including a qualified anticipated entrance. Delayed entrances remain delayed. Every new acoustic attack releases the previous sound: a bass eighth followed by an upper-chord quarter has no bass underneath the quarter. Complete-cycle selection, fills and final realization use this same release rule. Bass and upper notes share one engraved part; uninterrupted holds may still tie across barlines.
Each song has six canonical roles: electric melody, electric accompaniment, electric solo, acoustic accompaniment, acoustic solo, and vocal melody. Matching source-form positions reuse the same musical events and TAB whenever a role returns. Final-turnaround omission changes only affected harmony; source-defined pickups/handoffs and the three-bar instrumental closing sustain remain explicit exceptions.
Registered Solo 1 is notated exactly in the acoustic part and Solo 2 exactly in the electric part. Unregistered song/slot pairs use deterministic prediction from one pooled statistical model trained on both slots. There is no learned acoustic/electric style distinction. Prediction uses up to four recent pitch/rhythm events, source-melody features, harmony, and metrical position; it does not plan complete themes or guarantee long-range phrase development. The current corpus is four authored solos from Autumn Leaves and All of Me.
Training, authored score reuse, and prediction share contextual harmony validation based on actual notes, timing, and chords. Explanatory labels and justification tags are not required. Unsupported relationships fail with a diagnostic rather than silently changing notes. Both solo roles use strings 1–6 and frets 1–12 without open strings. No dynamics, accents, velocity shaping, or random timing are added.
./modulus.py python -m generator.compose --song ... --role acoustic_solo --data-dir data --output ... builds one independent role as JSON. To exercise prediction for a registered song, pass --settings with a JSON file containing {"predict": true}. Solos are included in the normal arranged scores and pages; separate solo MusicXML files are not generated. Authored XMLs remain under data/training, and regression tests compare their sounding events directly with generated score data.
See Shared Part Generation and Authored Repertoire and Learned Solos for the active pipeline and limitations. Keep temporary comparisons and one-off diagnostics under /tmp/; build/ retains product outputs, tools and dependencies.
To inspect generated assets without building the full package, run:
./modulus.py python -m generator --data-dir data --output-dir /tmp/product_vamp_generated --static-assets-dir package/assets --check
The --check path compares generation invariants against tests/fixtures/performance_summary.json.
Modulus owns the pinned build-only dependencies in its optional assets group, selected by tooling.python.groups in modulus.json. Configure prepares build/build_common/python; ./modulus.py python and CMake use that environment and the exported Modulus modules. Builds never install Python dependencies. After dependency pins change, run configure again. C++-only libraries do not opt in, and Python is not packaged for the Pi.
CMake builds use two composition workers and two engraving workers, with separate disposable caches under build/build_common/composition_cache and build/build_common/engraving_cache. Native and ARM packaging reuse unchanged roles and pages; CI also retains the page cache between runs. Music, recording, and catalog validation still run for every generation. Set VAMP_COMPOSITION_JOBS or VAMP_ENGRAVING_JOBS (1-16) to adjust concurrency, or set VAMP_ENGRAVING_CACHE_DIR to an empty CMake value to disable persistent page caching. Direct generator runs default to two composition workers and one engraving worker, without persistent caches; use --composition-cache, --engraving-cache, --composition-jobs, --jobs, and --progress as needed.
Identical notation bodies share layout and body rasterization even when titles or score identities differ; each score retains its own header, pages, music, and catalog identity. A cold namespace means no cache directory exists for the current renderer fingerprint. It does not mean that every score must be rendered independently: repeated bodies can still be reused within that build, including with persistent caching disabled. Progress separates full-score hits, body hits, newly rasterized bodies, and composed pages.
Add a source and its authored solo, then retrain the pooled model, without changing generator code:
./modulus.py python -m generator.train --add-source /path/to/song.xml --add-solo /path/to/solo1.xml --slot 1
./modulus.py python -m generator.train --add-source data/songs/song.xml --add-solo /path/to/solo2.xml --slot 2
To retrain existing pairs, run ./modulus.py python -m generator.train; add --leave-one-song-out for held-out likelihood diagnostics. Training writes data/models/solo.json and build/training/report.json. Ordinary builds use registered authored music or the saved model and never retrain automatically. Exact registered playback verifies authored reuse; it does not demonstrate model memorization or musical quality on unfamiliar songs. Training and playback details cover pairing, timing, pitch convention, data migration, and limitations.
The build-time pipeline is MusicXML -> source analysis -> independent roles -> assembled parts -> semantic JSON, supplied recordings, engraved PNG pages, Pico title artwork, and a read-only catalog database. Each source produces the six main performance sections plus the held Ending as Acoustic, Electric, and Vocal parts in its authored key, without key variants or a key-selection menu. Performance measure numbers run continuously across the complete arrangement, starting at 1 in the lead-in; they do not restart with a new section or chorus. Pickups occupy the preceding measure.
All generated monophonic guitar material uses frets 1–12 without open strings. Chords use two span tiers: one to three consecutive fret positions share the primary tier; exactly four positions are a fallback (e.g. 7–9 is primary, 7–10 is fallback). Five or more positions are forbidden. Selection minimizes fallback use before family, melody-string, movement or register preferences. A chord can extend to fret 20 while retaining at least one sounding anchor on frets 1–12 throughout its duration. Acoustic bass remains within frets 1–12 and upper chords need their own anchor when played separately. Some chord/key combinations still have no legal inversion; generation reports them instead of removing required tones. Electric source-melody harmonization retains its chord-change reattack policy. Import preserves exact source pitches and ties; unplayable or unsupported input fails explicitly rather than being silently octave-folded.
There are no genre presets or runtime composition. The complete arranged score is played in reading order. The source-defined form and continuous whole-arrangement measure numbering remain unchanged.
Normal browsing uses two music systems per page, with a single system only when the score ends on an odd system. Playback uses overlapping pairs (1–2, 2–3, 3–4): each new active line moves to the top, revealing the next below. The final line moves to the top with an empty lower half. Measure numbers sit above their starting barlines in a clear lane; playback outlines the active measure number in the accent color. Lines break only between measures and share consistent staff/TAB widths. A measure that cannot fit at readable spacing fails with a diagnostic instead of being split. Tall notation is scaled uniformly to fit the two rows; guitar notation and TAB stay together and aligned. TAB displays frets and ties without duplicate rests; standard notation retains the complete rhythm. The generated .pages/*.png files are the same page images displayed by the product.
Write all music in reading order: repeat bars, numbered endings, navigation jumps, and pass-specific ties are rejected. The performance arrangements, including the held Ending, remain fully written out. Chord labels use M, m, aug, dim, sus, and mM7, including explicit major triads such as CM. Recognized Soundslice text-only harmonies are normalized into structured chords; parenthesized chords mark optional turnarounds omitted from the final vocal pass.
Settings provides independent Metronome and Backing toggles, both off by default, plus Tempo. Song -> Part -> Score navigation stays the same. With both toggles off, pages turn manually. With either enabled, opening a part starts a one-bar count-in and an automatic performance; tap/double-tap page commands are ignored, and hold exits to the part menu. Natural completion also returns there.
A performance is an eight-bar intro followed by five complete passes: Vocal Melody, Guitar Melody, Acoustic Solo, Electric Solo, and Vocal Melody. A final Ending section adds one guitar chord attack sustained across three full measures, with vocal rests. The current 32-bar songs contain 171 numbered bars. The ending holds the final performed harmony (GM6 for All of Me, Dm for Autumn Leaves). Optional turnaround chords are omitted from the final vocal pass.
Audio comes exclusively from supplied WAV files: package/assets/sounds/beat1.wav accents each measure's first beat, and beat2.wav plays its other beats. Backing tracks belong in package/assets/backing/<song_id>_<bpm>.wav, for example all_of_me_120.wav. The normalized song ID comes from the source filename. Files are copied unchanged; the app never synthesizes notes or stretches recordings.
Tempo choices are 60, 70, 80, 90, 100, 110, and 120 BPM. With Backing on, choices are the tempos available for every catalog song; currently that is 120 BPM. Enabling Backing selects the closest available tempo, preferring the slower choice on a tie. If no tempo is shared, Backing is unavailable. Add recordings at the other tempos and rebuild to enable them.
Backing recordings include one bar of count-in, the complete arrangement including its three-bar held ending, and decay. The existing WAVs are copied unchanged. The recorded count-in plays alone. Metronome clicks stop at the start of the written Ending section; those three measures and the decay remain unclicked. Metronome-only mode supplies its own count-in, waits silently through the ending, and then stops. Playback line turns and measure highlighting follow the rendered audio clock through all 171 measures, including rests and ties; the final line stays visible through the recording decay. Preparation and count-in have no highlighted measure.
Code style is managed by Modulus, using compact two-space formatting. Python imports modules by their full names and qualifies imported symbols; only from __future__ import annotations is exempt, and imports have no aliases. 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 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. Configure installs the optional Modulus asset dependencies. Exact musical timing is retained through layout, with line breaks between complete measures. Missing glyphs, unrepresentable page geometry, and failed notation audits stop generation before replacing existing assets.
Every normal build also writes printable PDFs to build/<target>/generated_assets/print/: one complete acoustic, electric and vocal part per song, including every arranged iteration and the ending. Print at actual size on portrait US Letter paper. Pages have white backgrounds, black notation, at least half-inch printer margins, and multiple systems per sheet; guitar parts include TAB. These are fresh 300 DPI renders, without the duplicated lines used by playback transition pages. PDFs use the existing Modulus-managed Pillow dependency, are cached separately, and remain build-only artifacts rather than being installed on the device.
generator/palette.json preserves the sheet music’s existing dark palette across Zero, Pico, score pages, and previews. Menus use the same Petaluma Script lettering, quiet rules, tune/composer hierarchy and restrained accent colors for a jazz Real Book feel. Performance status shows Count-in, Playing or Sustain, BPM, page progress and a hold-to-exit hint. CMake generates the shared C++ colors from the same palette used by Python. Palette changes invalidate the engraving cache without changing music, spacing, or pagination.
Petaluma is from Steinberg under the SIL Open Font License. Petaluma supplies music glyphs, and Petaluma Script supplies all visible text on score pages and both device interfaces. The Zero uses packaged PetalumaScript.otf for dynamic UI text; the Pico displays prerendered Petaluma Script artwork. Runtime packages retain OFL.txt; the music font and engraver stay build-only.
Pico artwork generation requires Pillow's Raqm layout support to preserve kerning. On Debian/Ubuntu, install libfribidi0 alongside the pinned Python requirements. The build image and CI install both. See Pillow's installation requirements.
Pico firmware embeds a small set of fixed labels, part names, digits, and the page separator, generated as four-bit coverage sprites under generated/pico/static.p4. Score generation prerenders titles into content-addressed pico/titles/*.mask assets referenced by the catalog and served by the Zero when the selection changes. Pico blends those pixels and composes page counts from digit sprites; it does not load fonts, shape text, or rasterize glyphs. All Pico text shaping and artwork generation run on the build host. The build image and CI install the pinned dependencies automatically. For a direct build, first run:
./modulus.py configure
To inspect the fixed artwork independently, run ./modulus.py python -m generator.pico_artwork --static-output /tmp/product_vamp_pico/static.p4. Changing the source OTF or artwork generator rebuilds the firmware sprites and generated title assets. These generated files are not authored music and are not checked in.
License
Product Vamp is released under the MIT License. Bundled third-party fonts keep their own licenses next to the font files.