pepperscore 0.1.1 → 0.2.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.
package/CHANGELOG.md ADDED
@@ -0,0 +1,20 @@
1
+ # Changelog
2
+
3
+ ## 0.2.0
4
+
5
+ ### Added
6
+
7
+ - `clock` prop (`ExternalClock`: `{ now(): number; playing: boolean; subscribe(listener): unsubscribe }`). A host clock drives the playhead, note highlight and Vertical/Endless follow. Its seconds map to the score at the score's own tempo map (tempo changes included, pickup bar starting at 0). The BPM setting is ignored. With a clock the reader plays no sound and hides its own play, stop and BPM controls. The playhead shows for every `chrome` preset, `"none"` included. A negative time (count-in) shows no playhead. Transposing keeps the playhead at `clock.now()`.
8
+ - `onSeek(seconds)`: with a `clock`, a click on a note (nearest performed occurrence) or anywhere in a measure (its start) reports the time instead of seeking.
9
+ - `audio` prop (default `true`). `false` never loads or schedules sound, and self-driven playback runs silently.
10
+ - `onTimeUpdate(seconds)`: about 10 Hz during self-driven playback.
11
+ - Handle: `seek(seconds)` and `currentTime()`. With a `clock`, `seek` reports through `onSeek` and `currentTime` returns `clock.now()`.
12
+ - Type exports `ExternalClock` and `ClockEvent`.
13
+
14
+ ### Changed
15
+
16
+ - `handle.play()` and `handle.stop()` do nothing while a `clock` is set.
17
+
18
+ ## 0.1.1
19
+
20
+ - Initial public release.
package/README.md CHANGED
@@ -208,8 +208,12 @@ export default defineConfig({
208
208
  | `onReady` | `(info: { title, composer?, parts: { id, name }[], measures }) => void` | | Once per loaded score, after it is drawn. |
209
209
  | `onPlay` / `onStop` | `() => void` | | Playback starts or stops, including when another instance takes over playback. |
210
210
  | `onError` | `(error: Error) => void` | | Each error the reader shows: an unreadable file, a failed download or a renderer failure. |
211
+ | `clock` | `ExternalClock` | | A host clock that drives the playhead, see "Driving the playhead from your own clock". |
212
+ | `audio` | `boolean` | `true` | `false` never loads or schedules sound. Self-driven playback then runs silently on wall-clock time. |
213
+ | `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
+ | `onTimeUpdate` | `(seconds: number) => void` | | About 10 times a second during self-driven playback. |
211
215
  | `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). |
212
- | `ref` | `Ref<PepperScoreHandle>` | | `{ play(): Promise<void>; stop(): void; transpose(semitones: number): void }`. `play` does nothing while playing, before a score has loaded, or with `chrome="none"`. `transpose` sets the **absolute** transposition (clamped to −12…12), the same as `settings.semitones`. It does not add to the current value. |
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`. |
213
217
 
214
218
  **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`.
215
219
 
@@ -217,7 +221,33 @@ When more than one source prop is set, the first present wins, in this order: `m
217
221
 
218
222
  The component never reads `window.location`. To open `?score=…` links, read the query string yourself and pass it as `scoreUrl`.
219
223
 
220
- `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` and `SoundPackName`, plus `SOUNDS_VERSION` and `defaultAssetBaseUrl(pack)`.
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)`.
225
+
226
+ ### Driving the playhead from your own clock
227
+
228
+ Pass `clock` when something else owns time, such as your own audio engine, a video, or a practice app's transport. The reader then only shows where that clock is:
229
+
230
+ ```tsx
231
+ import { PepperScore, type ExternalClock } from 'pepperscore';
232
+
233
+ // Adapt your transport once (keep the object stable: a new object re-subscribes).
234
+ const clock: ExternalClock = {
235
+ now: () => transport.seconds, // seconds from score beat 0; negative during a count-in
236
+ get playing() { return transport.playing; },
237
+ subscribe: listener => transport.on('change', event => listener({ type: event.type, time: transport.seconds })),
238
+ };
239
+
240
+ <PepperScore chrome="none" audio={false} musicXml={xml} clock={clock} onSeek={seconds => transport.seek(seconds)} />
241
+ ```
242
+
243
+ - **Time mapping.** Seconds count from score beat 0 (the first beat of a pickup bar, if there is one). They follow the score's own tempo map, so every `<sound tempo>` / metronome change is included. The BPM setting is ignored. Repeats and jumps play in performed order. A fermata adds half its note's length.
244
+ - **Events.** `subscribe` should call the listener on `play`, `pause`, `seek` and `rate`. While `playing` is true, the reader reads `now()` every animation frame. Otherwise it reads it once per event.
245
+ - **No sound, no transport.** With a clock the reader never loads sound, whatever `audio` says. It also hides its own play, stop and BPM controls. The playhead, note highlight and Vertical/Endless follow work for every `chrome`, including `"none"`.
246
+ - **Seeking.** A click on a note reports that note's time (its performed occurrence nearest the clock), and a click elsewhere in a measure reports the measure's start. Both go through `onSeek`, and so does `ref.seek(seconds)`. Seek your clock there. The reader follows on the next `seek` event.
247
+ - `onPlay`/`onStop` are not called for clock play/pause.
248
+ - **Count-in.** A negative clock time shows no playhead and no highlighted note.
249
+ - Transposing while a `clock` is set does not move the playhead; it stays at `clock.now()`.
250
+ - `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.
221
251
 
222
252
  ### Chrome presets and `controls`
223
253
 
@@ -225,7 +255,7 @@ The component never reads `window.location`. To open `?score=…` links, read th
225
255
  | --- | --- | --- | --- |
226
256
  | Header transport (stop, play, BPM, view toggle, open, mixer) | yes | no | no |
227
257
  | Compact play/stop button (corner overlay) | no | yes | no |
228
- | Playhead, note highlight, follow | yes | yes | no |
258
+ | Playhead, note highlight, follow | yes | yes | no (yes with a `clock`) |
229
259
  | Bottom drawer and handle | yes | no | no |
230
260
  | Part icons, instrument picker, stave eyes | yes | no | no |
231
261
  | Page arrows and zoom | yes | no | no |
@@ -3,6 +3,7 @@ import { type PepperScoreSettings } from '../lib/prefs';
3
3
  import { type PersistNamespace } from '../lib/storage';
4
4
  import { type SoundPackName } from '../lib/assets';
5
5
  import { type SoundSource } from '../lib/sounds';
6
+ import type { ExternalClock } from '../lib/clock';
6
7
  export type PepperScoreReadyInfo = {
7
8
  title: string;
8
9
  composer?: string;
@@ -13,11 +14,19 @@ export type PepperScoreReadyInfo = {
13
14
  measures: number;
14
15
  };
15
16
  export type PepperScoreHandle = {
16
- /** Start playback (no-op while playing, or when the chrome loads no audio). */
17
+ /** Start playback (no-op while playing, when the chrome loads no audio, or with a `clock`). */
17
18
  play(): Promise<void>;
19
+ /** Stop and rewind (no-op with a `clock`: the host owns time). */
18
20
  stop(): void;
19
21
  /** Set the absolute transposition in semitones (clamped to -12…12), like `settings.semitones`. */
20
22
  transpose(semitones: number): void;
23
+ /**
24
+ * Self-driven: move the playhead to `seconds` (playback seconds at the current BPM); while playing, playback continues
25
+ * from there. With a `clock`: report `seconds` through `onSeek` (the host seeks its clock).
26
+ */
27
+ seek(seconds: number): void;
28
+ /** Seconds of the playhead: `clock.now()` with a `clock`, else the self-driven position. */
29
+ currentTime(): number;
21
30
  };
22
31
  export type PepperScoreProps = {
23
32
  /** Score sources; the first present wins, in this order. */
@@ -56,8 +65,20 @@ export type PepperScoreProps = {
56
65
  onPlay?(): void;
57
66
  onStop?(): void;
58
67
  onError?(error: Error): void;
68
+ /**
69
+ * A host clock that drives the playhead (seconds from score beat 0, at the score's own tempo; the BPM setting is ignored).
70
+ * With a clock, the reader never plays sound or runs its own transport, and the playhead shows for every chrome preset.
71
+ * Pass a stable object (a new object re-subscribes).
72
+ */
73
+ clock?: ExternalClock;
74
+ /** `false` never loads or schedules sound: self-driven playback runs silently on wall-clock time. Default `true`. */
75
+ audio?: boolean;
76
+ /** With a `clock`: a click on a note or measure (or `handle.seek`) reports its time here instead of seeking. */
77
+ onSeek?(seconds: number): void;
78
+ /** Self-driven playback time, about 10 times a second while playing. */
79
+ onTimeUpdate?(seconds: number): void;
59
80
  /** Focus the reader on mount so page-turn keys work before any click. */
60
81
  autoFocus?: boolean;
61
82
  ref?: React.Ref<PepperScoreHandle>;
62
83
  };
63
- export default function PepperScore({ musicXml, file, scoreUrl, abc, chrome: preset, controls, view, height, className, ariaLabel, settings, persist, assetBaseUrl, soundPack, soundSources, onReady, onPlay, onStop, onError, autoFocus, ref }: PepperScoreProps): import("react").JSX.Element;
84
+ export default function PepperScore({ musicXml, file, scoreUrl, abc, chrome: preset, controls, view, height, className, ariaLabel, settings, persist, assetBaseUrl, soundPack, soundSources, onReady, onPlay, onStop, onError, clock, audio: withAudio, onSeek, onTimeUpdate, autoFocus, ref }: PepperScoreProps): import("react").JSX.Element;
package/dist/index.d.ts CHANGED
@@ -5,6 +5,7 @@ export default PepperScore;
5
5
  export type { PepperScoreProps, PepperScoreHandle, PepperScoreReadyInfo } from './components/PepperScore';
6
6
  export type { ChromePreset, PepperScoreControls } from './lib/chrome';
7
7
  export type { PepperScoreSettings } from './lib/prefs';
8
+ export type { ExternalClock, ClockEvent } from './lib/clock';
8
9
  export type { SoundSource } from './lib/sounds';
9
10
  export type { SoundPackName } from './lib/assets';
10
11
  export { SOUNDS_VERSION, defaultAssetBaseUrl } from './lib/assets';