pepperscore 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/CHANGELOG.md +74 -0
  2. package/LICENSE +26 -12
  3. package/README.md +127 -9
  4. package/dist/THIRD_PARTY_NOTICES.txt +1222 -0
  5. package/dist/assets/{import.worker-D0XfuAHu.js → import.worker-Rop-z7az.js} +2 -2
  6. package/dist/assets/verovio.worker-DQm-ikdG.js +7 -0
  7. package/dist/audio.d.ts +24 -0
  8. package/dist/audio.js +198 -0
  9. package/dist/audio.js.map +1 -0
  10. package/dist/components/InstrumentChooser.d.ts +17 -0
  11. package/dist/components/OctaveTool.d.ts +21 -0
  12. package/dist/components/PepperScore.d.ts +96 -1
  13. package/dist/index.d.ts +3 -2
  14. package/dist/index.js +3738 -3382
  15. package/dist/index.js.map +1 -1
  16. package/dist/instruments.d.ts +2 -0
  17. package/dist/instruments.js +2 -0
  18. package/dist/lib/chrome.d.ts +10 -2
  19. package/dist/lib/file-state.d.ts +45 -5
  20. package/dist/lib/header-controls.d.ts +7 -3
  21. package/dist/lib/hit-test.d.ts +18 -0
  22. package/dist/lib/instrument-model.d.ts +80 -0
  23. package/dist/lib/instrument-switch-state.d.ts +169 -0
  24. package/dist/lib/instrument-view.d.ts +92 -0
  25. package/dist/lib/instruments.d.ts +82 -0
  26. package/dist/lib/metronome.d.ts +33 -0
  27. package/dist/lib/notation/document.d.ts +11 -1
  28. package/dist/lib/notation/mei-spelling.d.ts +9 -1
  29. package/dist/lib/notation/mei-tab.d.ts +58 -0
  30. package/dist/lib/notation/mei-view.d.ts +8 -0
  31. package/dist/lib/playback/timeline.d.ts +4 -1
  32. package/dist/lib/prefs.d.ts +67 -0
  33. package/dist/lib/range-fit-core.d.ts +26 -0
  34. package/dist/lib/render.d.ts +36 -1
  35. package/dist/lib/score.d.ts +23 -1
  36. package/dist/lib/sound-players.d.ts +9 -1
  37. package/dist/lib/tab/fret-assign.d.ts +53 -0
  38. package/dist/lib/tab/tab-parts.d.ts +64 -0
  39. package/dist/lib/transpose.d.ts +3 -2
  40. package/dist/lib/verovio-protocol.d.ts +15 -0
  41. package/dist/lib/verovio-runtime.d.ts +9 -1
  42. package/dist/range-fit-core-B1rMWdI4.js +468 -0
  43. package/dist/range-fit-core-B1rMWdI4.js.map +1 -0
  44. package/dist/runtime-files.d.ts +15 -0
  45. package/dist/{sf2-player-CRlOJDhD.js → sf2-player-oWC6yrUH.js} +28 -6
  46. package/dist/sf2-player-oWC6yrUH.js.map +1 -0
  47. package/dist/sound-players-DdLBBZaR.js +1172 -0
  48. package/dist/sound-players-DdLBBZaR.js.map +1 -0
  49. package/dist/style.css +1 -1
  50. package/dist/verovio/LICENSE +858 -0
  51. package/dist/verovio/verovio-module.mjs +0 -0
  52. package/dist/verovio/verovio.mjs +397 -0
  53. package/package.json +14 -2
  54. package/dist/assets/verovio.worker-DkHQmOsT.js +0 -19034
  55. package/dist/sf2-player-CRlOJDhD.js.map +0 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,79 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.0 - 2026-10-24
4
+
5
+ 0.3.0 was never published, so 0.4.0 also contains everything listed under 0.3.0 below (guitar TAB, text sizes, the shared audio engine and the host hooks).
6
+
7
+ ### Added
8
+
9
+ - Instrument switch (docs/specs/instrument-switch.md). `settings.instrument`, `settings.instruments` (per part, keyed by 1-based source part number or `onReady` part id), `settings.rangeLevel` (`'beginner' | 'intermediate' | 'advanced'`, default `'intermediate'`) and `settings.autoRange` (default `true`). A switched part changes sound, clef and written transposition (B♭ and E♭ instruments read at written pitch, also in a concert-pitch score) and each phrase is fitted into the instrument's range. A grand staff switches together. A piano part switched to guitar gets generated TAB, and a TAB staff doubling a switched part is hidden or regenerated.
10
+ - Highlights `ps-transposed` (moved by fitting or the octave tool), `ps-out-of-range` (`--ps-out-of-range`) and `ps-octave-selection`.
11
+ - `controls.instrumentSwitch` and `controls.octaveTool`: both on in `full`, off in `minimal` and `none` unless set. In `minimal` and `none`, `settings.instrument` / `settings.instruments` alone switch the part but show no switcher. The octave tool selects a note (with its tie chain), a passage or a phrase and moves it 8va/8vb relative to the shown pitch; a 0 override pins a note as written; Reset clears.
12
+ - The mixer separates Instrument from Sound (a playback override). Embeds get a popover switcher and, on a phone, a bottom toolbar. Part labels read "→ Instrument".
13
+ - `pepperscore/instruments`: the instrument model and range-fit core (`INSTRUMENTS`, `INSTRUMENT_IDS`, `EMBED_INSTRUMENTS`, levels, ranges, transpositions, clefs, spelling helpers, `writtenKeyFifths`, `joinRuns`, `tieChainOf`, `planShifts`, `applyOverrides`), shared with the editor.
14
+ - `onReady` parts gain `sourcePart` and `switchable`. Types `RangeLevel` and `SwitchInstrumentId` (from both `pepperscore` and `pepperscore/instruments`). The model's instrument type is `InstrumentInfo`, named apart from `pepperscore/audio`'s `Instrument`; `INSTRUMENTS` and `EMBED_INSTRUMENTS` are typed read-only and frozen.
15
+ - Saved per-score records (still version 2) keep each part's switch (`PartState.switch`: instrument, level, autoRange, octaves, sound) and a fingerprint of the host's instrument settings (`hostInstruments`, optional). Records without a switch open as written.
16
+ - Host instrument settings vs saved choices: a saved record's switches win while the host's instrument settings (`instrument`, `instruments`, `rangeLevel`, `autoRange`) are the ones it was saved under, or the host gives none. When they differ (the same score embedded as another instrument, or a block changed to another instrument), the host's apply again over the record; a part whose instrument changes loses its saved octave moves, and the rest of the record is kept.
17
+
18
+ ### Changed
19
+
20
+ - Space on a focused button inside the reader now presses that button instead of turning the page (affects all embeds).
21
+
22
+ ### Compatibility
23
+
24
+ - A reader's "keep as written" octave pin (8vb on an auto-raised phrase) is dropped once a transpose, key flip, level, instrument or fit change means auto-fit no longer moves that note, and it is not restored when transposing back.
25
+ - Saved records can now contain an octave override of 0. An older pepperscore reader sharing the same site storage accepts only +-12 and drops that part's whole saved switch.
26
+
27
+ ### Known limitations
28
+
29
+ - A switched part's written transposition and key come from its first measure; a mid-part transposition change is not reflected in the engraving.
30
+ - MEI-file imports do not group grand staves.
31
+ - The octave tool moves a note at most one octave from as written (±12 in total), so a phrase two octaves out of range stays flagged.
32
+
33
+ ## 0.3.0 - 2026-10-23
34
+
35
+ ### Added
36
+
37
+ - `pepperscore/audio`: the reader's audio engine as a framework-free subpath (`dist/audio.js` + `audio.d.ts`), so other apps on the page share one `AudioContext`, one playback owner and the same sounds. Exports `getSharedAudio`, `resumeAudio`, `claimPlayback`, `releasePlayback`, `loadInstrument(program | name | 'drums', { assetBaseUrl, soundPack, sources, context, destination, notes })`, `loadDrumKit`, `createMetronome({ voice: 'woodblock' | 'drum-machine' | 'click', gain })` and their types (`Instrument`, `AudioPlayer`, `Metronome`, `MetronomeVoice`, ...). Instruments resolve exactly as the reader does (sources, the pack's FluidR3 sound, TimGM6mb.sf2; drums from Perfect Drums plus TimGM6mb's kit), all from the asset base. The main entry's exports are unchanged, and both entries import the same chunk for the shared state. See README, "Shared audio engine".
38
+ - Text add-on sizes. Note names, chord symbols (source and inferred), beat counts and count subdivisions each have a size: `xs`, `sm`, `md`, `rg`, `lg` or `xl` (0.7x to 1.45x). `rg` (regular) is the default and draws exactly the earlier size. Hosts set them with `settings.annotationSizes` (`{ noteNames?, chordSymbols?, beatCounts?, subdivisions? }`, type `PepperScoreAnnotationSizes`; `AnnotationSize` is exported too); the full reader's options drawer has a segmented size control beside each add-on's switch while it is on. Lane rows and offsets grow with the labels, and source chord symbols are sized by Verovio (MEI `<rend fontsize>`), so it spaces them. Sizes are saved in the global prefs and in each score's record (`namesSize`, `chordSymbolsSize`, `countsSize`, `subdivisionsSize`; optional on read, so earlier records read as `rg`).
39
+ - Guitar TAB (docs/specs/guitar-tab.md). `settings.tabView` (`'notation'` default, `'tab'`, `'both'`), `settings.tabTuning` (open-string MIDI, low to high, default `[40, 45, 50, 55, 59, 64]`) and `settings.capo` (0–12). A score's own TAB staff (MusicXML clef `TAB`) shows in the TAB and Both views. Other scores get TAB generated by a fingering solver for their guitar parts, or for the first pitched part's top staff. Transposed views regenerate the frets. In TAB view the notation staff is hidden and its chord symbols, tempo and directions move to the TAB staff, and the playhead highlights the TAB.
40
+ - `controls.tabToggle`: a Notation / TAB / Both switch in the header (in the More panel on a phone) and in the drawer. With the drawer on, the header copy is left out between 761 and 1260px, where the full header has no room. On in the `full` preset, and shown only for a score with a pitched part.
41
+ - `settings.bpm` (initial tempo, 30–240) and `settings.keyFlip` (enharmonic spelling of a transposed view), which were reader-only settings before. Like `semitones`, they apply on each score load unless a saved per-score record has its own values.
42
+ - Saved per-score records (still version 2) keep `tabView`, `tabTuning` and `capo`. Records without them read with the defaults.
43
+ - `onElementClick(info)` (`PepperScoreClickInfo`): every click on the engraving, with every chrome, reports the note (`noteId`, `sourceId`), measure (`measureIndex`), position across it (`fraction`), nearest staff (`staff`) and `clientX`/`clientY`. With a `clock`, `onSeek` still fires as before. While it is set, notes and measures show a pointer cursor.
44
+ - `highlights` prop (`PepperScoreHighlight[]`): host classes on notes and measures, re-applied after every draw. New styles `ps-cursor`, `ps-selection` and `ps-cursor-measure` (`--ps-cursor`, `--ps-selection`).
45
+ - Handle: `scrollToMeasure(index)` (draws the measure's system if needed and scrolls it into view) and `seekToMeasure(index)` (seeks to the measure's first performed occurrence).
46
+ - Type exports `PepperScoreClickInfo` and `PepperScoreHighlight`.
47
+ - `PepperScoreClickInfo.nearestNoteId` / `nearestSourceId`: for a click that hits no note, the note or rest on the clicked staff in that measure whose notehead is nearest the click horizontally. A click anywhere in a note's drawn box (notehead, stem, flag, accidental, dots and the gaps between them) now reports that note as `noteId` (before, only the ink under the pointer did); where boxes overlap the nearest notehead wins, and noteheads stacked at one x (two voices, a chord) are split by the nearer in y. With a `clock`, such a click therefore seeks to that note (`onSeek`) where it used to seek to the start of the measure.
48
+ - Handle: `scrollIntoView(target, options)` brings notes (`ids`) or a measure (`measure`) into view with the least scroll. Nothing moves when the target is already inside `margin`. `lead` (0..1 of the view) adds room ahead in the direction of travel, and `bounds` (client coordinates) keeps the target clear of a host's overlays. `interactedSince` skips the scroll when the reader has scrolled, wheeled, touched or pressed on the score since that time (a momentum or smooth scroll that carries on from their wheel or flick counts until it stops). It does nothing while playback follows the playhead. Vertically it keeps the target's whole system in view. It draws the measure's system if needed, scrolls smoothly unless the user prefers reduced motion, and resolves `true` when it scrolled. New types: `PepperScoreScrollTarget`, `PepperScoreScrollOptions` and `PepperScoreInsets`.
49
+ - `onRender()` prop: called after every draw lands (the first paint, a re-layout or an in-place swap), once `highlights` are applied.
50
+ - `verovioBaseUrl` prop: engrave with another interface-compatible Verovio build (a directory laid out like the `verovio` package's `dist/`).
51
+ - `paintTransition` prop (`'reveal'` default, or `'none'`): with `'none'`, a new `musicXml` or `abc` for a score already on screen is swapped in place, with no fade, loading overlay or scroll reset.
52
+ - `dist/THIRD_PARTY_NOTICES.txt` (runtime dependencies, Verovio, the OFL fonts), linked as "Licences" in the full reader's options drawer.
53
+
54
+ ### Changed
55
+
56
+ - The options drawer uses the reader's width: its cards (Reading, a new Annotations card holding the add-on switches and sizes, Pitch and ABC notation) sit in as many ~272px columns as fit, so on a desktop they sit side by side and the drawer is short; on a phone they stack in one column.
57
+ - A MusicXML TAB staff that doubles a notation part is no longer a part of its own: it is not played (playback had every note twice), has no annotation lanes, and is not listed in the part controls, mixer or `onReady` parts. A TAB-only score still plays its TAB staff.
58
+ - A highlighted measure (`highlights` with `measures`, any class) now shows a dashed box: a `.ps-measure-highlight` `<rect>` (in a `.ps-highlight-layer` overlay, so measure bounds are unchanged) with a 2px non-scaling stroke in `--ps-cursor`. The old `ps-cursor-measure` CSS outline was drawn in viewBox units and was nearly invisible.
59
+ - New highlight class `ps-transposed` (`--ps-transposed`, default `#d9741c`) for notes.
60
+ - `ps-selection` defaults to ink blue `#2f6a9a` (was green `#1f9d55`), so it does not clash with `ps-transposed`.
61
+ - A new score source draws immediately; the redraw fade only applies to setting changes.
62
+ - During playback the next two systems are drawn ahead of the playhead (`LOOKAHEAD_SYSTEMS = 2`), so a system is ready before its bar starts. Before, a system was requested only when the playhead entered it, and on a large score at a fast tempo the bar could end before the draw finished.
63
+ - With no `highlights` set and none applied, the highlight effect does no DOM queries and does not subscribe to redraws.
64
+ - Verovio is no longer bundled into the worker chunk. Its own unmodified files ship in `dist/verovio/` (with a licence comment and a `LICENSE`) and the worker imports them at runtime; host bundlers copy them like the worker chunks. Serve `.mjs` as JavaScript. The LICENSE's relink offer now points at upstream Verovio 6.3.0 source and `verovioBaseUrl`.
65
+
66
+ ### Fixed
67
+
68
+ - `pepperscore/audio`: the metronome no longer leaks drum kits (overlapping `setVoice` calls share one load, a kit or TimGM6mb fallback that arrives after `dispose()` is released) and retries after a failed drum download. `createMetronome` resolves at once and plays the synthesized sound until the kit arrives (new `metronome.ready`); the default `'woodblock'` voice's ~6 MB download is documented. `Instrument.plan` is now the read-only `{ kind, sourceId }` (`InstrumentPlan`) and the internal `AudioPlayer.fallback` is no longer in the declarations.
69
+ - Chord-symbol sizing no longer scans to the end of the document for every symbol. The size buttons have accessible names ("Extra small" ... "Extra large"). Regular size draws an SVG with no `is-subdivision` class.
70
+ - Stopping playback now cancels SF2 (TimGM6mb and `kind: 'sf2'` source) notes already scheduled more than 200 ms ahead. smplr queues those in its own scheduler, and its argument-less `stop()` only stops sounding voices, so a queued note could still play after Stop. Each SF2 note now keeps its own stop handle (with a unique id: smplr's handle stopped every voice of the same pitch) and `stop()` calls them all.
71
+ - An ending (volta) that starts a line is drawn whole on that line. The page break went inside the `<ending>`, so Verovio drew a stub on the line before and labelled the bracket "(1)" as a continuation.
72
+
73
+ ### Known follow-ups
74
+
75
+ - the default `loadInstrument` note range 21-108 decodes up to 88 buffers up front; `instruments.ts` and the reader assemble sounds separately; a pre-0.3 per-score record resets the four sizes to `rg`; the shared audio chunk is named after sound-players.
76
+
3
77
  ## 0.2.0
4
78
 
5
79
  ### Added
package/LICENSE CHANGED
@@ -21,20 +21,34 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
21
  SOFTWARE.
22
22
 
23
23
  ---
24
- The MIT License above covers the pepperscore component code. It does not cover
25
- Verovio.
26
-
27
- dist/assets/verovio.worker-*.js is a combined work: pepperscore's own worker
28
- code (MIT) bundled with the Verovio 6.3.0 music engraving library and its
29
- WebAssembly module (https://www.verovio.org/, Copyright (c) the Verovio
30
- contributors). Verovio is used unmodified and is licensed under the GNU Lesser
31
- General Public License, version 3 or (at your option) any later version
24
+ The MIT License above covers the pepperscore component code, including its
25
+ Web Worker chunks in dist/assets/. It does not cover Verovio or the other
26
+ third-party components listed in dist/THIRD_PARTY_NOTICES.txt.
27
+
28
+ Verovio is not compiled or bundled into pepperscore's code. pepperscore ships
29
+ two unmodified files of the Verovio 6.3.0 music engraving library
30
+ (https://www.verovio.org/, Copyright (c) Laurent Pugin and the Verovio
31
+ contributors), taken from the npm package verovio@6.3.0, with only a licence
32
+ comment added at the top of each:
33
+
34
+ dist/verovio/verovio-module.mjs the WebAssembly module and its glue code
35
+ dist/verovio/verovio.mjs the JavaScript toolkit
36
+ dist/verovio/LICENSE their licence notice
37
+
38
+ pepperscore's worker loads them at runtime. Verovio is licensed under the GNU
39
+ Lesser General Public License, version 3 or (at your option) any later version
32
40
  (LGPL-3.0-or-later). Copies of the licenses are included in this package:
33
41
 
34
42
  LICENSES/LGPL-3.0.txt GNU Lesser General Public License, version 3
35
43
  LICENSES/GPL-3.0.txt GNU General Public License, version 3
36
44
 
37
- Verovio 6.3.0 source code: https://github.com/rism-digital/verovio/tree/version-6.3.0
38
- (also published on npm as verovio@6.3.0). You may replace the bundled Verovio
39
- with a compatible modified version by rebuilding this package from its source
40
- (https://github.com/pepperhorn/ph-scoreview) against your own verovio build.
45
+ Corresponding source for Verovio 6.3.0:
46
+ https://github.com/rism-digital/verovio/tree/version-6.3.0
47
+ (git tag version-6.3.0; also published on npm as verovio@6.3.0)
48
+
49
+ Replacing Verovio: you may use a modified or different, interface-compatible
50
+ build of Verovio in place of the shipped files without rebuilding pepperscore.
51
+ Either pass the URL of a directory holding your verovio-module.mjs and
52
+ verovio.mjs (laid out like the verovio npm package's dist/ directory) as the
53
+ `verovioBaseUrl` prop of PepperScore, or replace the two files in
54
+ dist/verovio/ (or the copies your bundler emitted) with your own.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # pepperscore
2
2
 
3
- An embeddable, browser-only music score reader for React. It opens MusicXML (`.musicxml`, `.xml`), compressed MXL, MIDI, MEI and ABC. [Verovio](https://www.verovio.org/) 6.3 engraves the score in a Web Worker, with its WASM bundled, so the page never blocks on layout. Playback follows one performance timeline that handles ties, dynamics, repeats, tempo changes and percussion, with a playhead, note highlighting and follow-scrolling. You can use it as a full reader page with a header, drawer and mixer, or as small inline snippets, several to a page.
3
+ An embeddable, browser-only music score reader for React. It opens MusicXML (`.musicxml`, `.xml`), compressed MXL, MIDI, MEI and ABC. [Verovio](https://www.verovio.org/) 6.3 engraves the score in a Web Worker, so the page never blocks on layout. Verovio ships with the package as its own, replaceable file (see [Verovio](#verovio)). Playback follows one performance timeline that handles ties, dynamics, repeats, tempo changes and percussion, with a playhead, note highlighting and follow-scrolling. You can use it as a full reader page with a header, drawer and mixer, or as small inline snippets, several to a page.
4
4
 
5
5
  ## Install
6
6
 
@@ -183,7 +183,7 @@ export default defineConfig({
183
183
 
184
184
  - Production builds (`astro build`) need nothing.
185
185
  - A plain Vite + React app needs nothing either, because Vite's dependency scan finds these from the app's entry. The same `include` is harmless there.
186
- - Do **not** add `optimizeDeps.exclude: ['pepperscore']`. It is not needed (the worker URLs survive pre-bundling with Vite 8), and with pnpm it breaks `react/jsx-runtime`.
186
+ - Do **not** add `optimizeDeps.exclude: ['pepperscore']`. It is not needed (the worker and Verovio URLs survive pre-bundling with Vite 8), and with pnpm it breaks `react/jsx-runtime`.
187
187
  - Your layout should declare `<meta charset="utf-8">` (Astro's starter layouts do). Without it, the server-rendered frame of a `client:load`/`client:visible` island is decoded as Latin-1 and React reports a hydration mismatch.
188
188
 
189
189
  ## Props
@@ -195,25 +195,33 @@ export default defineConfig({
195
195
  | `scoreUrl` | `string` | | A score to download. Relative URLs resolve against the page. Cross-origin URLs need CORS. |
196
196
  | `abc` | `string` | | ABC notation. |
197
197
  | `chrome` | `'full' \| 'minimal' \| 'none'` | `'full'` | See the chrome table below. |
198
- | `controls` | `{ transport?, viewToggle?, openFile?, mixer?, drawer?, zoom? }` (booleans) | from `chrome` | Per-control overrides of the preset, see below. |
198
+ | `controls` | `{ transport?, viewToggle?, openFile?, mixer?, drawer?, zoom?, tabToggle?, instrumentSwitch?, octaveTool? }` (booleans) | from `chrome` | Per-control overrides of the preset, see below. |
199
199
  | `view` | `'vertical' \| 'endless'` | saved or `'vertical'` | Vertical scrolls down, Endless scrolls sideways. Changing the prop re-applies it. |
200
200
  | `height` | `string` | natural | `"fill"`: the parent's height. Any CSS length (`"420px"`, `"60vh"`, `"100dvh"`) fixes it. Omitted: `full` fills its parent with a 480px floor, and `minimal`/`none` take the height of their score. |
201
201
  | `className` | `string` | | Extra classes on the `.pepperscore` root. |
202
202
  | `ariaLabel` | `string` | `"Score: <title>"`, or `"Score reader"` | Accessible name of the reader's landmark region. By default it is `Score: ` and the loaded score's title, or `Score reader` while there is no titled score. Give each snippet on a page a distinct name when titles repeat. |
203
- | `settings` | `PepperScoreSettings` | | **Initial** reader settings: `noteNames`, `chordSymbols`, `inferChords`, `beatCounts`, `subdivisions` (booleans); `musicFont` (`'Bravura' \| 'Petaluma'`); `staveFont` (`'Sans' \| 'Serif' \| 'Mono'`); `barsPerLine` (`'auto' \| 1–8`, Vertical view; without a `zoom`, an explicit count also sets the on-load size: that many bars fill the width, see below); `zoom` (stave scale, `null` = device default; with a `zoom`, `barsPerLine` only sets the line breaks); `semitones` (−12…12); `concert` (concert vs written pitch); `overlap` (0–3 lines kept on a page turn). Changing a key in the prop re-applies that key. Drawer edits still work in between. With `persist` on, **a saved per-score record overrides `settings` and `view`** when that score is opened again. The record holds the settings, view, zoom, transposition, part sounds and hidden staves the reader stored for that exact file. |
203
+ | `settings` | `PepperScoreSettings` | | **Initial** reader settings: `noteNames`, `chordSymbols`, `inferChords`, `beatCounts`, `subdivisions` (booleans); `musicFont` (`'Bravura' \| 'Petaluma'`); `staveFont` (`'Sans' \| 'Serif' \| 'Mono'`); `barsPerLine` (`'auto' \| 1–8`, Vertical view; without a `zoom`, an explicit count also sets the on-load size: that many bars fill the width, see below); `zoom` (stave scale, `null` = device default; with a `zoom`, `barsPerLine` only sets the line breaks); `semitones` (−12…12); `concert` (concert vs written pitch); `keyFlip` (spell a transposed view in the enharmonic key, for example Gb major for F# major); `bpm` (initial tempo, 30–240; the score's own tempo otherwise); `overlap` (0–3 lines kept on a page turn); `tabView`, `tabTuning`, `capo` (guitar TAB, see [Guitar TAB](#guitar-tab)); `annotationSizes` (text size per add-on, see below); `instrument`, `instruments`, `rangeLevel`, `autoRange` (the instrument switch, see below). Changing a key in the prop re-applies that key. Drawer edits still work in between. With `persist` on, **a saved per-score record overrides `settings` and `view`** when that score is opened again. The record holds the settings, view, zoom, transposition, part sounds, hidden staves and instrument switches the reader stored for that exact file. The instrument settings are the exception: when they differ from the ones the record was saved under, they apply again (see [Instrument switch](#instrument-switch)). |
204
204
  | `persist` | `string \| false` | `'pepperscore'` for `full`, `false` for `minimal`/`none` | Storage namespace, see below. `false` reads and writes nothing. |
205
205
  | `assetBaseUrl` | `string` | jsDelivr copy of `pepperscore-sounds-<soundPack>` | Root of a sounds-package layout. Relative values resolve against the page, and a missing trailing slash is added. |
206
+ | `verovioBaseUrl` | `string` | the copy in `dist/verovio/` | A directory with an interface-compatible Verovio build to engrave with, laid out like the `verovio` npm package's `dist/` (`verovio-module.mjs`, `verovio.mjs`), e.g. `https://cdn.jsdelivr.net/npm/verovio@6.3.0/dist/`. Shared by every reader on the page: set it before the first score engraves. See [Verovio](#verovio). |
206
207
  | `soundPack` | `'core' \| 'complete'` | `'core'` | Picks the default `assetBaseUrl`. With your own `assetBaseUrl`, the `pack.json` found there decides which FluidR3 sounds exist. |
207
208
  | `soundSources` | `SoundSource[]` | `[]` | Your own sounds for specific programs or the drum kit, see below. |
208
- | `onReady` | `(info: { title, composer?, parts: { id, name }[], measures }) => void` | | Once per loaded score, after it is drawn. |
209
+ | `onReady` | `(info: { title, composer?, parts: { id, name, sourcePart, switchable }[], measures }) => void` | | Once per loaded score, after it is drawn. `parts` lists every staff (a grand staff's staves share one `sourcePart` number); `switchable` says whether the instrument switch applies. |
209
210
  | `onPlay` / `onStop` | `() => void` | | Playback starts or stops, including when another instance takes over playback. |
210
211
  | `onError` | `(error: Error) => void` | | Each error the reader shows: an unreadable file, a failed download or a renderer failure. |
211
212
  | `clock` | `ExternalClock` | | A host clock that drives the playhead, see "Driving the playhead from your own clock". |
212
213
  | `audio` | `boolean` | `true` | `false` never loads or schedules sound. Self-driven playback then runs silently on wall-clock time. |
213
214
  | `onSeek` | `(seconds: number) => void` | | With a `clock`: a click on a note or measure (and `ref.seek`) reports its time here. The reader does not move. |
214
215
  | `onTimeUpdate` | `(seconds: number) => void` | | About 10 times a second during self-driven playback. |
216
+ | `onElementClick` | `(info: PepperScoreClickInfo) => void` | | Any click on the engraving, see "Host hooks". |
217
+ | `highlights` | `PepperScoreHighlight[]` | | Classes on notes and measures, see "Host hooks". |
215
218
  | `autoFocus` | `boolean` | `false` | Focus the reader on mount so the page-turn keys work before any click. Keyboard shortcuts belong to the focused reader only. Each reader is one Tab stop, and keyboard focus shows a focus ring on it (clicks do not). |
216
- | `ref` | `Ref<PepperScoreHandle>` | | `{ play(): Promise<void>; stop(): void; transpose(semitones: number): void; seek(seconds: number): void; currentTime(): number }`. `play` does nothing while playing, before a score has loaded, with `chrome="none"`, or with a `clock`. `stop` does nothing with a `clock`. `transpose` sets the **absolute** transposition (clamped to −12…12), the same as `settings.semitones`. It does not add to the current value. `seek` moves the playhead (and keeps playing from there when playing). With a `clock` it calls `onSeek` instead. `currentTime` returns the playhead seconds, or `clock.now()` with a `clock`. |
219
+ | `paintTransition` | `'reveal' \| 'none'` | `'reveal'` | How a new `musicXml` or `abc` replaces a score already on screen. `'reveal'` fades it out, shows the loading overlay, scrolls to the top and fades the new score in. `'none'` keeps the current engraving and scroll position until the new one replaces it in place, with no fade or overlay (for a host that edits the score it shows). The first paint, other sources and setting changes always reveal. |
220
+ | `ref` | `Ref<PepperScoreHandle>` | | `{ play(): Promise<void>; stop(): void; transpose(semitones: number): void; seek(seconds: number): void; currentTime(): number; scrollToMeasure(index: number): Promise<void>; seekToMeasure(index: number): void }`. `play` does nothing while playing, before a score has loaded, with `chrome="none"`, or with a `clock`. `stop` does nothing with a `clock`. `transpose` sets the **absolute** transposition (clamped to −12…12), the same as `settings.semitones`. It does not add to the current value. `seek` moves the playhead (and keeps playing from there when playing). With a `clock` it calls `onSeek` instead. `currentTime` returns the playhead seconds, or `clock.now()` with a `clock`. |
221
+
222
+ **The instrument switch.** `settings.instrument` (a `SwitchInstrumentId`: any `InstrumentId` but `'drum-kit'`) shows the first switchable part as that instrument: its clef, written pitch, range and sound. `settings.instruments` sets it per part and wins over `instrument`. A key is either the 1-based source part number as a string (`{ '2': 'trumpet' }`: parts in score order, a grand staff counted once, a drum kit counted too) or an `onReady` part id; a staff id of a grand staff switches the whole part. `onReady`'s `parts[].sourcePart` and `parts[].id` are both valid keys, and parts with `switchable: false` (drum kits, percussion) are ignored. `rangeLevel` (`RangeLevel`: `'beginner' | 'intermediate' | 'advanced'`, default `'intermediate'`) and `autoRange` (default `true`: phrases outside the range move by an octave) set how switched parts fit. They apply to a score without a saved record, and to a saved one when they differ from the host settings it was saved under (see [Instrument switch](#instrument-switch)). In `minimal` and `none` they switch the part without showing a switcher; set `controls.instrumentSwitch` for one.
223
+
224
+ **Text add-on sizes with `annotationSizes`.** Each text add-on has its own size, in six steps: `'xs' | 'sm' | 'md' | 'rg' | 'lg' | 'xl'` (type `AnnotationSize`). `'rg'` (regular) is the default and draws the size earlier versions drew; the steps scale it by 0.7, 0.8, 0.9, 1, 1.2 and 1.45. Keys: `noteNames`, `chordSymbols` (source chord symbols and inferred ones), `beatCounts` (on-beat counts) and `subdivisions` (the e/&/a counts between beats). Give only the add-ons you want to change, for example `settings={{ noteNames: true, annotationSizes: { noteNames: 'lg' } }}`; unknown sizes are ignored. The rows under the staff grow with their labels, and Verovio sizes and spaces source chord symbols itself; the stave size and bar layout do not change. In the full reader each add-on's size is also a segmented control beside its switch in the options drawer (shown while that add-on is on), saved with the other reader settings and in each score's record.
217
225
 
218
226
  **Sizing a snippet with `barsPerLine`.** In Vertical view, `settings={{ barsPerLine: N }}` without a `zoom` picks the stave size at which exactly N bars fill the reader's width, so N is the simplest way to set how large an embed draws: on the same width, `barsPerLine: 2` draws larger staves than `barsPerLine: 4`. The size is bounded (at most stave scale 2.5, at least 0.2; a single bar on a very wide reader is stretched to fill rather than drawn huge), and lines always hold N bars. A score shorter than one line takes its share of the width, centred. `'auto'` keeps the device default (four bars across on desktop). Endless view ignores `barsPerLine`.
219
227
 
@@ -221,7 +229,7 @@ When more than one source prop is set, the first present wins, in this order: `m
221
229
 
222
230
  The component never reads `window.location`. To open `?score=…` links, read the query string yourself and pass it as `scoreUrl`.
223
231
 
224
- `Reader` and `ReaderProps` are still exported as deprecated aliases of `PepperScore` and `PepperScoreProps` for one release. The package also exports the types `PepperScoreProps`, `PepperScoreHandle`, `PepperScoreReadyInfo`, `PepperScoreSettings`, `PepperScoreControls`, `ChromePreset`, `SoundSource`, `SoundPackName`, `ExternalClock` and `ClockEvent`, plus `SOUNDS_VERSION` and `defaultAssetBaseUrl(pack)`.
232
+ `Reader` and `ReaderProps` are still exported as deprecated aliases of `PepperScore` and `PepperScoreProps` for one release. The package also exports the types `PepperScoreProps`, `PepperScoreHandle`, `PepperScoreReadyInfo`, `PepperScoreSettings`, `PepperScoreControls`, `ChromePreset`, `SoundSource`, `SoundPackName`, `ExternalClock`, `ClockEvent`, `PepperScoreClickInfo` and `PepperScoreHighlight`, plus `SOUNDS_VERSION` and `defaultAssetBaseUrl(pack)`.
225
233
 
226
234
  ### Driving the playhead from your own clock
227
235
 
@@ -249,6 +257,14 @@ const clock: ExternalClock = {
249
257
  - Transposing while a `clock` is set does not move the playhead; it stays at `clock.now()`.
250
258
  - `validation/fixtures/clock-contract.musicxml` with `clock-contract.expected.json` (in the repository) pins the onset seconds within 1 ms (a pickup bar, and tempo 120 → 60 → 180 mid-bar). Use them to check your own tempo map against the reader's.
251
259
 
260
+ ### Host hooks
261
+
262
+ For hosts that build on the reader (selection, cursors, navigation):
263
+
264
+ - **`onElementClick(info)`** fires for any click on the engraving, with every `chrome` and with or without a `clock`. `info` is `{ noteId?, sourceId?, measureIndex?, fraction?, staff?, nearestNoteId?, nearestSourceId?, clientX, clientY }`: the note hit (and its MusicXML `id`, when known), the zero-based measure, the 0–1 position across that measure, and the 1-based nearest staff. A click anywhere in a note's drawn box (its notehead, stem, flag, accidental or dots, or the gaps between them) hits that note (overlapping boxes go to the nearest notehead; noteheads stacked at one x, as in two voices, to the nearer in y). When no note was hit, `nearestNoteId` and `nearestSourceId` name the note or rest on that staff in that measure whose notehead is nearest the click horizontally (stacked noteheads again split by y). Engraved spacing is not proportional to time, so this is a better guide than `fraction` to the event a click in staff space means. With a handler set, notes and measures show a pointer cursor.
265
+ - **`highlights`**: `{ ids?, measures?, className }[]`. Each entry adds `className` to those notes (note or MusicXML ids) and measures (zero-based), re-applied after every draw. Built-in classes: `ps-cursor`, `ps-selection` and `ps-transposed` (notes) and `ps-cursor-measure` (measures), coloured by `--ps-cursor`, `--ps-selection` (ink blue `#2f6a9a`) and `--ps-transposed` (orange `#d9741c`). Every highlighted measure, whatever its class, also gets a dashed box: a `<rect class="ps-measure-highlight" data-ps-class="...">` in a `<g class="ps-highlight-layer">` overlay beside the measures (so the measure's own bounds are unchanged), a few pixels outside the measure, with a 2px screen-space stroke (`vector-effect: non-scaling-stroke`) in `--ps-cursor`. Restyle it per class with `.ps-measure-highlight[data-ps-class~="my-class"] { stroke: ... }`. The box is removed when the highlight is cleared and redrawn with the measure.
266
+ - **`ref.scrollToMeasure(index)`** draws the measure's system if needed and scrolls it into view. **`ref.seekToMeasure(index)`** seeks to the measure's start, like `ref.seek` (with a `clock`, through `onSeek`).
267
+
252
268
  ### Chrome presets and `controls`
253
269
 
254
270
  | Control | `full` | `minimal` | `none` |
@@ -272,8 +288,50 @@ const clock: ExternalClock = {
272
288
  | `mixer` | The track mixer, plus the part icons, instrument picker and stave eyes. |
273
289
  | `zoom` | The stave-scale slider, pinch zoom and the page arrows row. |
274
290
  | `drawer` | The bottom drawer and its handle. |
291
+ | `instrumentSwitch` | The instrument switcher: the Instrument choice in the mixer and the embed popover, the range-level and auto-range controls, and the phone bottom toolbar of embeds. On in `full`; off in `minimal` and `none` unless you set it. `settings.instrument` / `settings.instruments` alone still switch the part, but show no switcher. |
292
+ | `octaveTool` | The octave tool (select a note, passage or phrase and move it 8va/8vb). On in `full`; off in `minimal` and `none` unless you set it. |
293
+ | `tabToggle` | The Notation / TAB / Both switch, in the header (in the More panel on a phone) and in the drawer. With the drawer on, the header copy is left out between 761 and 1260px, where the full header has no room for it. It shows only for a score with a pitched part. |
275
294
 
276
- The header appears when any of `transport`, `viewToggle`, `openFile`, `mixer` or `zoom` is on. For example, `<PepperScore chrome="minimal" controls={{ viewToggle: true }} … />` keeps the compact play button and adds a small header with only the view toggle.
295
+ The header appears when any of `transport`, `viewToggle`, `openFile`, `mixer`, `zoom` or `tabToggle` is on. For example, `<PepperScore chrome="minimal" controls={{ viewToggle: true }} … />` keeps the compact play button and adds a small header with only the view toggle.
296
+
297
+ ### Guitar TAB
298
+
299
+ `settings.tabView` sets which staves guitar parts show: `'notation'` (default), `'tab'` or `'both'`. Readers switch with `controls.tabToggle` (on in `full`).
300
+
301
+ | Setting | Type | Default | Meaning |
302
+ | --- | --- | --- | --- |
303
+ | `tabView` | `'notation' \| 'tab' \| 'both'` | `'notation'` | Notation only, TAB only (the notation staff is hidden and its chord symbols, tempo and directions move onto the TAB staff), or both. |
304
+ | `tabTuning` | `number[]` | `[40, 45, 50, 55, 59, 64]` | Open-string MIDI pitches, low to high, of generated TAB (EADGBE). |
305
+ | `capo` | `0`–`12` | `0` | Capo fret of generated TAB. Frets are relative to it. |
306
+
307
+ - **A score with a TAB staff** (MusicXML `<clef><sign>TAB</sign>` with `<staff-details>`, as MuseScore and Guitar Pro export) shows that staff in the TAB and Both views. The reader treats it as a view of its notation part: it is played once, has no note-name or count lanes, and the part controls and mixer list the instrument once.
308
+ - **Any other score** gets TAB generated from its pitches: for every guitar part (General MIDI programs 25–32, the guitar sound, or "guitar" in the part name), or else for the first pitched part's top staff. A fingering solver picks strings and frets for the whole passage (open strings, few position shifts, playable stretches). Notes out of range are moved by an octave. Chord notes that cannot all be fretted lose doubled notes first, then inner voices. MusicXML `<technical><string>`/`<fret>` on a note is kept as a fixed fingering.
309
+ - **Transposed views** regenerate TAB from the transposed pitches. An imported TAB staff is hidden then, because its frets no longer match.
310
+ - Tied continuations keep their rhythm on the TAB staff but show no fret.
311
+ - **Baked fingerings.** The repository's `npm run tab:bake` (`scripts/tab-bake.mjs`, see the root README) writes the solver's string/fret onto a score's notes ahead of time, and can add a TAB staff. The reader keeps those fingerings as they are.
312
+
313
+ ### Instrument switch
314
+
315
+ Full behaviour: `docs/specs/instrument-switch.md`. Choose an instrument for a part and the reader changes its sound, clef and written transposition (B♭ and E♭ instruments read at written pitch, also in a concert-pitch score), then fits each phrase into the instrument's range for the chosen level.
316
+
317
+ | Setting | Type | Default | Meaning |
318
+ | --- | --- | --- | --- |
319
+ | `instrument` | `SwitchInstrumentId` | | Shows the first switchable part as this instrument. |
320
+ | `instruments` | `Record<string, SwitchInstrumentId>` | | Per part: key is the 1-based source part number or an `onReady` part id. Wins over `instrument`. |
321
+ | `rangeLevel` | `RangeLevel` (`'beginner' \| 'intermediate' \| 'advanced'`) | `'intermediate'` | How wide a range the instrument is taken to have. |
322
+ | `autoRange` | `boolean` | `true` | Phrases outside the range move by an octave. `false` leaves them and marks them. |
323
+
324
+ - Notes moved by fitting get `ps-transposed` (`--ps-transposed`), notes still out of range get `ps-out-of-range` (`--ps-out-of-range`), and the octave tool's selection gets `ps-octave-selection`.
325
+ - A grand staff switches as one part. A piano part switched to guitar gets generated TAB, and a TAB staff that doubled the switched part is hidden or regenerated.
326
+ - The mixer lists Instrument (what the part is) and Sound (a playback override that keeps the notation).
327
+ - Embeds show a popover switcher with a short instrument list (`EMBED_INSTRUMENTS`), and on a phone a bottom toolbar.
328
+ - The octave tool (`controls.octaveTool`) selects a note and its tie chain, a passage (Shift-click) or a phrase, then moves it 8va or 8vb relative to its shown pitch. Reset clears the part's moves.
329
+ - The switcher shows in `full`, and in `minimal` or `none` only with `controls.instrumentSwitch: true`. Without it, `settings.instrument` / `settings.instruments` still switch the part.
330
+ - With `persist` on, saved records now hold instrument switches: each part's instrument, level, auto-range, octave moves and sound override, plus a fingerprint of the host's instrument settings (`instrument`, `instruments`, `rangeLevel`, `autoRange`). Records without a switch open as written.
331
+ - **Host settings vs saved choices.** Records are keyed by the score file, so one score embedded as piano in one lesson and as trumpet in another shares a record. When a score opens with a saved record, the reader compares the host's instrument settings with the ones saved in it. If they are the same, or the host gives none, the reader's saved choices win. If they differ (a teacher changed a block's instrument), the host's instrument, level and fit apply again; a part whose instrument changes loses its saved octave moves, and everything else in the record (other settings, zoom, sounds, hidden staves) is kept.
332
+ - `onReady` parts carry `sourcePart` and `switchable`.
333
+
334
+ **`pepperscore/instruments`** is a framework-free entry (no React, no Verovio) with the instrument model (`INSTRUMENTS`, `INSTRUMENT_IDS`, `EMBED_INSTRUMENTS`, levels, ranges, transpositions, clefs, `spellMidi`, `writtenPitch`, `transposePitchSpelled`, `writtenKeyFifths`) and the range-fit core (`joinRuns`, `tieChainOf`, `planShifts`, `applyOverrides`). The editor uses the same module. Its types include `InstrumentInfo` (one model entry; not the audio player's `Instrument` from `pepperscore/audio`), `InstrumentId`, `SwitchInstrumentId` and `RangeLevel`. `INSTRUMENTS` (`Readonly<Record<InstrumentId, Readonly<InstrumentInfo>>>`) and `EMBED_INSTRUMENTS` (`readonly InstrumentId[]`) are frozen.
277
335
 
278
336
  ### `persist`
279
337
 
@@ -321,6 +379,14 @@ cp -rL node_modules/pepperscore-sounds-core public/pepperscore-sounds # -L: pn
321
379
 
322
380
  Use the sounds package version that matches `SOUNDS_VERSION` (exported by this package). A base on another origin must send CORS headers (`Access-Control-Allow-Origin`), because fonts and sounds are fetched.
323
381
 
382
+ ### Verovio
383
+
384
+ Verovio is not bundled into pepperscore's code. The package ships upstream's own files from `verovio@6.3.0`, unmodified apart from a licence comment, in `dist/verovio/` (`verovio-module.mjs`, about 7.3 MB with its WebAssembly inlined, and `verovio.mjs`, the toolkit), and the Verovio worker imports them at runtime when the first score engraves. Host bundlers (Vite, Astro) copy them as assets like the worker chunks, so nothing needs configuring.
385
+
386
+ - Serve `.mjs` files as JavaScript (`text/javascript`). Most hosts do; a server that sends them as `application/octet-stream` makes the import fail, and the reader shows a "Could not load Verovio from …" error.
387
+ - To engrave with another build (a modified Verovio, a newer compatible version, or a CDN copy), pass `verovioBaseUrl`, a directory holding both files. A cross-origin directory must send CORS headers. Or replace the files your bundler emitted.
388
+ - The interface pepperscore expects: `verovio-module.mjs` default-exports an async factory returning the Emscripten module (`createVerovioModule`), and `verovio.mjs` exports `VerovioToolkit` and `enableLogToBuffer`, as in `verovio@6.3.0`.
389
+
324
390
  **If the assets cannot be loaded** (a wrong `assetBaseUrl`, or nothing published there yet), the reader keeps working: the score still engraves (Verovio's own glyphs; text glyphs fall back), playback runs silently with the usual "playing silently" notice and no error, and the console shows one warning naming the `assetBaseUrl` that failed. A failing `soundSources` entry of your own still reports through `onError`.
325
391
 
326
392
  ### Testing before the sounds packages are published
@@ -358,6 +424,54 @@ Drums use the Perfect Drums note files unless a source claims them. `soundSource
358
424
  />
359
425
  ```
360
426
 
427
+ ## Shared audio engine (`pepperscore/audio`)
428
+
429
+ The reader's audio engine is also a small, framework-free subpath (no React), so other music apps on the same page (a piano visualiser, a practice tool) share one `AudioContext`, one "who is playing" slot and the same sounds as every `PepperScore`:
430
+
431
+ ```ts
432
+ import { claimPlayback, createMetronome, getSharedAudio, loadInstrument, releasePlayback, resumeAudio } from 'pepperscore/audio';
433
+
434
+ const shared = getSharedAudio(); // { context, master } or null without Web Audio
435
+ await resumeAudio(shared!.context); // from a user gesture; resolves false if audio cannot start
436
+
437
+ const piano = await loadInstrument(1, { assetBaseUrl: '/pepperscore-sounds/' }); // GM program, FluidR3 name or 'drums'
438
+ const metronome = await createMetronome({ voice: 'woodblock', assetBaseUrl: '/pepperscore-sounds/' });
439
+
440
+ const owner = { stop: () => { piano.stop(); metronome.stop(); } };
441
+ claimPlayback(owner); // stops any PepperScore (or other owner) that is playing
442
+ const t = shared!.context.currentTime + .1;
443
+ for (let beat = 0; beat < 4; beat++) metronome.tick({ time: t + beat * .5, beat }); // beat 0 is accented
444
+ const stopNote = piano.start({ note: 60, time: t, duration: 1, velocity: 90 });
445
+ // later: stopNote() stops (or cancels) that note; owner.stop() stops everything still sounding or scheduled
446
+ releasePlayback(owner); // when your playback ends
447
+ ```
448
+
449
+ | Export | Signature | |
450
+ | --- | --- | --- |
451
+ | `getSharedAudio` | `() => { context: AudioContext; master: DynamicsCompressorNode } \| null` | The page's audio graph, created on first use. Readers play through `master`. |
452
+ | `resumeAudio` | `(context) => Promise<boolean>` | Resume with a bounded wait (a pending `resume()` never hangs your code). |
453
+ | `claimPlayback` / `releasePlayback` | `(owner: { stop(): void }) => void` | One player at a time: claiming stops the previous owner. A reader claims it when it starts playing, so your app and the readers never play over each other. |
454
+ | `loadInstrument` | `(sound: number \| string, options?) => Promise<Instrument>` | `sound`: GM program 1-128, FluidR3 name (`'acoustic_grand_piano'`) or `'drums'`. |
455
+ | `loadDrumKit` | `(options?) => Promise<Instrument>` | `loadInstrument('drums', options)`. |
456
+ | `createMetronome` | `(options?) => Promise<Metronome>` | See below. |
457
+ | `METRONOME_VOICES`, `SOUNDS_VERSION`, `defaultAssetBaseUrl` | | Constants. |
458
+
459
+ The drum kit is `'drums'` (`'drum-kit'`, the score part id, is accepted as an alias).
460
+
461
+ Options (`InstrumentOptions`): `assetBaseUrl`, `soundPack` (`'core'` default, `'complete'`) and `sources` (`SoundSource[]`) work exactly like the reader's `assetBaseUrl`, `soundPack` and `soundSources` props. `context` and `destination` default to the shared context and its master compressor. `notes` lists the notes to load before the promise resolves (default 21-108 for an instrument, none for the kit; call `loadNotes(notes)` for more, since a note that is not loaded starts silently).
462
+
463
+ An `Instrument` is an `AudioPlayer` (`start({ note, time, duration, velocity, destination? }) => stop`, `stop()`, `dispose()`, `loadNotes(notes)`, `covers(note)`) plus `context`, `output` (its volume `GainNode`) and `plan` (read-only `{ kind, sourceId }`: which source plays it, for example `'builtin:fluidr3'`). `stop()` stops every sounding note **and cancels every scheduled one**. A note's `destination` is honoured by sample-pack and note-file sounds. SF2 sounds always play through `output`.
464
+
465
+ Sounds resolve exactly as in the reader (above): your `sources`, then the pack's FluidR3 sound, then `TimGM6mb.sf2`. Drums use Perfect Drums, with TimGM6mb's kit for the notes it lacks. Every built-in file comes from `assetBaseUrl`, so a self-hosted base means no requests to any other host.
466
+
467
+ **Metronome.** `createMetronome({ voice?, gain?, ...InstrumentOptions })` returns `{ voice, output, ready, tick({ time, beat, accent? }) => stop, setVoice(voice), stop(), dispose() }`. It resolves at once and loads the drum kit in the background: beats play the synthesized sound until the kit arrives (`await metronome.ready` to wait; `setVoice` resolves when its notes have loaded). A failed kit download is not remembered: the next `setVoice`, or a tick after 10 seconds, tries again. `beat` counts from 0 within the bar, and beat 0 is accented (louder, or higher for synth sounds) unless `accent` says otherwise. `gain` defaults to a gentle 0.5.
468
+
469
+ | Voice | Beat 1 | Other beats | Source |
470
+ | --- | --- | --- | --- |
471
+ | `'woodblock'` (default) | GM hi wood block (76) | GM low wood block (77) | The drum kit (Perfect Drums has no wood blocks, so TimGM6mb's kit plays them). **The default voice downloads Perfect Drums' manifest and `TimGM6mb.sf2` (about 6 MB, once, cached) from `assetBaseUrl` in the background; use `voice: 'click'` to load nothing.** Without a kit wood block: a soft synthesized knock. |
472
+ | `'drum-machine'` | Kick (36) | Closed hi-hat (42) | The drum kit (Perfect Drums). Without a kit: synthesized ticks. |
473
+ | `'click'` | Soft high tick | Soft tick | Synthesized, short and quiet. Loads nothing. |
474
+
361
475
  ## Known v1 limits
362
476
 
363
477
  - **`deviceClass()` uses a viewport media query.** Layout sizes come from the reader's root element, but the choice between "desktop" (a fine pointer and a viewport wider than 850px) and "compact" defaults still queries the viewport. That choice sets the default stave scale and the large-file notice wording. So a narrow reader on a wide desktop page gets desktop defaults.
@@ -366,12 +480,16 @@ Drums use the Perfect Drums note files unless a source claims them. `soundSource
366
480
  - Settings are initial values, not fully controlled props.
367
481
  - Many large scores on one page engrave one after another, because they share a single Verovio worker. It is slow, not broken.
368
482
 
483
+ - **Instrument switch.** A switched part's written transposition and key come from its first measure, so a mid-part transposition change is not reflected in the engraving. MEI-file imports do not group grand staves, so a piano switches one staff at a time. The octave tool moves a note at most one octave from as written (±12 in total), and fitting moves a phrase by one octave, so a phrase two octaves out of range stays flagged `ps-out-of-range`.
484
+
369
485
  ## Licenses
370
486
 
371
487
  The package licence is `MIT AND LGPL-3.0-or-later`:
372
488
 
373
489
  - **The pepperscore component code is MIT licensed** (see `LICENSE`).
374
- - **The bundled Verovio worker is LGPL-3.0-or-later.** `dist/assets/verovio.worker-*.js` bundles Verovio 6.3.0 ([source](https://github.com/rism-digital/verovio/tree/version-6.3.0)), unmodified, with its WebAssembly module. The LGPL-3.0 and GPL-3.0 texts ship in `LICENSES/LGPL-3.0.txt` and `LICENSES/GPL-3.0.txt`. Hosts that bundle pepperscore redistribute that worker chunk as is; keep these notices with it.
490
+ - **The pepperscore worker chunks in `dist/assets/` are MIT licensed too**: they contain no Verovio code.
491
+ - **Verovio is LGPL-3.0-or-later** and stays a separate file. `dist/verovio/` holds Verovio 6.3.0's own `verovio-module.mjs` and `verovio.mjs` (unmodified, with a licence comment added) and a `LICENSE`; the worker loads them at runtime, and you can swap in your own build with `verovioBaseUrl` (see [Verovio](#verovio)). Source: [rism-digital/verovio, tag version-6.3.0](https://github.com/rism-digital/verovio/tree/version-6.3.0). The LGPL-3.0 and GPL-3.0 texts ship in `LICENSES/`. Hosts that deploy pepperscore redistribute these files; keep their notices with them.
492
+ - **Third-party notices**: `dist/THIRD_PARTY_NOTICES.txt` lists the runtime dependencies (MIT and ISC, with their licence texts), Verovio, the SIL OFL 1.1 music fonts Verovio embeds (Leipzig, Bravura, Gootville, Leland, Petaluma) and Poppins (OFL 1.1, through `@fontsource/poppins`). Host bundlers copy it as an asset, and the full reader's options drawer links it ("Licences", next to the soundfont credits).
375
493
  - **Sound packs** (`pepperscore-sounds-core`, `pepperscore-sounds-complete`): each asset set keeps its own license, listed in the package's `LICENSES.md`:
376
494
  - TimGM6mb.sf2: GPL-2.0
377
495
  - FluidR3 GM sample packs: CC BY 3.0 (attribution in `soundfonts/fluidr3/ATTRIBUTION.txt`)