@sudobility/music_player 0.1.27 → 0.1.29

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/CLAUDE.md CHANGED
@@ -10,8 +10,10 @@ building, and offline rendering. One of seven repos in the Moosiac family — se
10
10
  `music_app/docs/architecture.md`.
11
11
 
12
12
  **The rule: this package makes sound. `music_io` moves bytes. `music_codecs`
13
- reads and writes score formats. `music_lib` edits.** It takes a `Score` and
14
- produces audio; it knows nothing about editing, files or the store.
13
+ reads and writes score formats. `music_editing` edits, and `music_lib` binds
14
+ the two (`bindPlayer`, typed on this package's `IMusicPlayer`).** It takes a
15
+ `Score` and produces audio; it knows nothing about editing, files or the store —
16
+ and music_editing does not depend on it either.
15
17
 
16
18
  Its own package because playback was previously spread across three: the engines
17
19
  in `music_io`, the transport brain in `music_lib` where it read the Zustand
@@ -40,6 +42,9 @@ installs `js-synthesizer` and a React Native app installs
40
42
 
41
43
  ## Gotchas
42
44
 
45
+ - **`ScheduledClick` and `Unsubscribe` are declared once each** — in `playback/synth-backend.ts` and `types.ts`. The web click and the bus import them; each had an identical second declaration, which agrees only until one is edited. `src/__one-declaration.test.ts` counts them. This is a within-package guard against drift, not the moved-names guard the other libraries carry: music_lib imports this package through `/core` by name rather than re-exporting it wholesale, so a duplicate here is not a TS2308 in music_lib — it is two types that silently part.
46
+
47
+
43
48
  - **One fact, one declaration — and `src/__single-source.test.ts` enforces it.** A constant restated in a second package agrees with the first right up until one of them is edited, and nothing fails when they part: the build is clean, the types match, and the only symptom is a wrong sound or a picker quietly missing an entry. Measured across the family, 23 UPPER_CASE constants were declared in more than one repo — `CC_VOLUME` three times, `DYNAMICS` four, `C_MAJOR` five, and `DEFAULT_TIME_SIGNATURE` in four places under two names. The guard reads the list of names **from music_types at runtime** rather than restating it, because a check against duplication that duplicates what it checks would drift like everything else; its `ALLOWED` list is empty on purpose, so an exemption is a decision somebody writes down. Two shapes matter beyond the constants themselves. **A closed vocabulary is declared as an array and the type read off it** (`export const DYNAMICS = [...] as const; export type Dynamic = (typeof DYNAMICS)[number];`) — a TypeScript union has no runtime form, so anything that must *validate* a value has to write the list out again, which is exactly how music_api's decoder came to check generated music against its own private copy. **A label or option list keyed by the vocabulary is a `Record<T, ...>`, never a parallel array** — a record fails to compile when a member is added, an array silently goes on offering the old set; that is why the inspector's picker lists are `ACCIDENTAL_OPTIONS`/`ARTICULATION_OPTIONS` built from the vocabulary rather than `ACCIDENTALS`/`ARTICULATIONS` retyped. Test fixtures follow the same rule: the score fixtures live once, in `@sudobility/music_types/test`, and a package that needs a rendering fixture of its own re-exports them and adds to them.
44
49
 
45
50
  - **`IMusicPlayer.load()` takes a `Score`; the engine still takes a plan.** This
@@ -90,30 +95,28 @@ installs `js-synthesizer` and a React Native app installs
90
95
  *production build that works fine*; music_app's `vite.config.ts` lists it
91
96
  beside music_lib and music_io.
92
97
 
93
- - **Platform *engines* are optional peers; `@breezystack/lamejs` is the one
94
- exception.** A React Native app must never pull a browser audio library into
95
- its graph, which is why `js-synthesizer` — libfluidsynth compiled to WASM, for
96
- an AudioWorklet — is an optional peer that music_app declares itself. lamejs
97
- stays a runtime `dependency` for two concrete reasons: `web-audio-codec.ts`
98
- imports it *statically*, so an app that skipped installing it would fail to
99
- load the entire web entry rather than just mp3 export; and `AudioCodec.encodeMp3`
100
- is synchronous in `music_types`, so it cannot be made lazy the way
101
- `js-synthesizer` is without changing that interface across four repos. It is
102
- pure JS and runs under Metro — and now that `encodeMp3` is shared, the RN
103
- entry imports it too, which settles the question: it is not a platform
104
- library at all.
105
- - **There is no RN audio, and that is a decision, not a gap in the port.** The
106
- web engine is libfluidsynth compiled to WASM running in an `AudioWorklet`;
107
- React Native has no AudioWorklet, so there is nothing to port — an RN engine
108
- means a different implementation of the same `PlaybackEngine` interface (a
109
- native synth module, or `react-native-audio-api` plus a soundfont player).
110
- What was there before subclassed the Tone engine over
111
- `react-native-audio-api` and synthesised its own approximations of the
112
- instruments, so it could never match what the web plays; a wrong sound is
113
- worse than a clear error. `unavailablePlayback()` now throws from anything
114
- that would make a sound and stays a silent no-op for the lifecycle methods, so
115
- a shared component calling `pause()` on unmount does not crash the app. The
116
- historical spikes are in `spikes/tone-on-rn-audio-api.md`.
98
+ - **Platform *engines* are optional peers, and there is no exception.** A React
99
+ Native app must never pull a browser audio library into its graph, which is
100
+ why `js-synthesizer` — libfluidsynth compiled to WASM, for an AudioWorklet —
101
+ and `react-native-audio-api` are both optional peers the app declares itself,
102
+ and `dependencies` is empty (`no-domain-imports.test.ts` pins it). The mp3
103
+ encoder, `@breezystack/lamejs`, is not this package's: encoding a rendered file
104
+ is music_io's, which carries it as its one runtime dependency and records why
105
+ it is not a peer.
106
+ - **React Native audio is a different implementation, not a port.** The web
107
+ engine is libfluidsynth compiled to WASM running in an `AudioWorklet`; React
108
+ Native has no AudioWorklet, so there was nothing to port — an RN engine had to
109
+ be a different implementation of the same `PlaybackEngine` interface. What was
110
+ there first subclassed the Tone engine over `react-native-audio-api` and
111
+ synthesised its own approximations of the instruments, so it could never match
112
+ what the web plays; it was deleted, and for a while RN deliberately had no
113
+ audio at all rather than a wrong sound. That gap is closed two ways, both in
114
+ `src/rn/`: `createMusicPlayer` builds `RNSamplePlaybackEngine`
115
+ (`playback/sample-engine.ts`, the FluidR3 sample engine described next), which
116
+ needs no native module and is the default; and `NativeSynthBackend`
117
+ (`playback/native-backend.ts`) drives a host-supplied native synth through the
118
+ same `SoundfontPlaybackEngine` scheduler the browser uses. The historical
119
+ spikes are in music_io's `spikes/tone-on-rn-audio-api.md`.
117
120
  - **The two platforms play the same font by different means, and that is the
118
121
  design.** Web runs libfluidsynth (WebAssembly) in an `AudioWorklet`; React
119
122
  Native has neither — `react-native-audio-api` exposes no `addModule` and
@@ -124,7 +127,7 @@ installs `js-synthesizer` and a React Native app installs
124
127
  What is lost is SF2's live per-zone filters, LFOs and modulator envelopes —
125
128
  expressive variation, not the instrument's identity. The engine that was
126
129
  deleted synthesised oscillator approximations, which is a different and much
127
- worse thing. See `spikes/rn-sample-playback.md`.
130
+ worse thing. See music_io's `spikes/rn-sample-playback.md`.
128
131
  - **`applyMix(score)` exists because mute and solo had setters and volume and pan did not.** A track's volume was read once at `loadScore` and pan only in `applyScoreToHost`, so once `music_lib`'s edit lock made a mix change stop reloading the score, moving a fader mid-playback moved the fader and not the sound. The web engine pushes CC7 and CC10 from the score and schedules nothing. **The RN engine reaches the same place with a node instead of a control change:** one gain-and-panner strip per track, built on first use, with voices connecting to their track's strip rather than to the master. Per-track rather than per-voice — the offline renderer builds a panner per voice because it schedules once and never changes its mind, where a live fader has to move under notes that are already sounding. That also settles where the headroom trim lives on RN: on the master, as it is on web, not multiplied into every voice's gain. **The metronome click bypasses the master on both platforms** so a score with more parts does not get a quieter click.
129
132
  - **The transport is shared; only voicing is per platform.** `shared/playback/`
130
133
  holds score flattening, the note cursor, the sounding set, the dispatch window
@@ -316,7 +319,7 @@ installs `js-synthesizer` and a React Native app installs
316
319
  schedules against a *live* clock and pumps 200ms at a time, so reusing it for
317
320
  export would make a render take as long as the piece. `RenderPlan` arrives
318
321
  fully resolved (seconds, not ticks; mute and solo already applied by
319
- `renderEvents` in music_lib), so `rn/audio/offline-render.ts` schedules the
322
+ `renderEvents` in `shared/render-events.ts`), so `rn/audio/offline-render.ts` schedules the
320
323
  whole thing up front against an `OfflineAudioContext`. What it *does* share
321
324
  with the engine is `PackLibrary` and `planVoice` — the sample choice and the
322
325
  gain — which is what keeps the file a recording of what was heard rather than
@@ -329,7 +332,8 @@ installs `js-synthesizer` and a React Native app installs
329
332
  - `music_types` — the model, the score domain, the playhead (`@sudobility/music_types`)
330
333
  - `music_codecs` — score file formats
331
334
  - `music_io` — files, audio encoding, XML, MIDI input
332
- - `music_lib` — editing; binds the store to this through its playback adapter
335
+ - `music_editing` — editing; depends on neither this package nor music_codecs
336
+ - `music_lib` — binds a store to this through `bindPlayer` and its playback adapter
333
337
  - `music_app` — the web UI
334
338
 
335
339
  ## Git Workflow
@@ -19,7 +19,7 @@
19
19
  * the caret and the piano keyboard genuinely care about different things.
20
20
  */
21
21
  import type { SoundingNote, TransportPlaybackState } from '@sudobility/music_types';
22
- export type Unsubscribe = () => void;
22
+ import type { Unsubscribe } from '../types.js';
23
23
  export declare class PlaybackBus {
24
24
  private readonly soundingListeners;
25
25
  private readonly transportListeners;
@@ -1 +1 @@
1
- {"version":3,"file":"bus.d.ts","sourceRoot":"","sources":["../../src/shared/bus.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,OAAO,KAAK,EACV,YAAY,EACZ,sBAAsB,EACvB,MAAM,yBAAyB,CAAC;AAEjC,MAAM,MAAM,WAAW,GAAG,MAAM,IAAI,CAAC;AAIrC,qBAAa,WAAW;IAUtB,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAE9B;IACJ,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAE/B;IAEJ,yGAAyG;IACzG,OAAO,CAAC,YAAY,CAA+B;IACnD,OAAO,CAAC,aAAa,CAAqC;IAE1D,UAAU,CAAC,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,GAAG,WAAW;IAIzD,UAAU,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,SAAS,YAAY,EAAE,KAAK,IAAI,GAAG,WAAW;IAK3E,WAAW,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,sBAAsB,KAAK,IAAI,GAAG,WAAW;IAK3E;;;;;;;;;;;;;OAaG;IACH,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAMnC;;;OAGG;IACH,eAAe,CAAC,KAAK,EAAE,SAAS,YAAY,EAAE,GAAG,IAAI;IAKrD,gBAAgB,CAAC,KAAK,EAAE,sBAAsB,GAAG,IAAI;IAKrD;;;;;;;;;;;;OAYG;IACH,IAAI,YAAY,IAAI,MAAM,CAEzB;IAED,IAAI,QAAQ,IAAI,SAAS,YAAY,EAAE,CAEtC;IAED,IAAI,SAAS,IAAI,sBAAsB,CAEtC;IAED,uEAAuE;IACvE,KAAK,IAAI,IAAI;CAId"}
1
+ {"version":3,"file":"bus.d.ts","sourceRoot":"","sources":["../../src/shared/bus.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,OAAO,KAAK,EACV,YAAY,EACZ,sBAAsB,EACvB,MAAM,yBAAyB,CAAC;AAEjC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAG/C,qBAAa,WAAW;IAUtB,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAE9B;IACJ,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAE/B;IAEJ,yGAAyG;IACzG,OAAO,CAAC,YAAY,CAA+B;IACnD,OAAO,CAAC,aAAa,CAAqC;IAE1D,UAAU,CAAC,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,GAAG,WAAW;IAIzD,UAAU,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,SAAS,YAAY,EAAE,KAAK,IAAI,GAAG,WAAW;IAK3E,WAAW,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,sBAAsB,KAAK,IAAI,GAAG,WAAW;IAK3E;;;;;;;;;;;;;OAaG;IACH,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAMnC;;;OAGG;IACH,eAAe,CAAC,KAAK,EAAE,SAAS,YAAY,EAAE,GAAG,IAAI;IAKrD,gBAAgB,CAAC,KAAK,EAAE,sBAAsB,GAAG,IAAI;IAKrD;;;;;;;;;;;;OAYG;IACH,IAAI,YAAY,IAAI,MAAM,CAEzB;IAED,IAAI,QAAQ,IAAI,SAAS,YAAY,EAAE,CAEtC;IAED,IAAI,SAAS,IAAI,sBAAsB,CAEtC;IAED,uEAAuE;IACvE,KAAK,IAAI,IAAI;CAId"}
@@ -1 +1 @@
1
- {"version":3,"file":"bus.js","sourceRoot":"","sources":["../../src/shared/bus.ts"],"names":[],"mappings":"AA2BA,OAAO,EAAE,sBAAsB,EAAE,MAAM,yBAAyB,CAAC;AAEjE,MAAM,OAAO,WAAW;IACtB;;;;;;;;MAQE;IACe,iBAAiB,GAAG,IAAI,GAAG,EAEzC,CAAC;IACa,kBAAkB,GAAG,IAAI,GAAG,EAE1C,CAAC;IAEJ,yGAAyG;IACjG,YAAY,GAA4B,EAAE,CAAC;IAC3C,aAAa,GAA2B,SAAS,CAAC;IAE1D,UAAU,CAAC,QAAgC;QACzC,OAAO,sBAAsB,EAAE,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC;IACtD,CAAC;IAED,UAAU,CAAC,QAAkD;QAC3D,IAAI,CAAC,iBAAiB,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QACrC,OAAO,GAAG,EAAE,CAAC,IAAI,CAAC,iBAAiB,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;IACvD,CAAC;IAED,WAAW,CAAC,QAAiD;QAC3D,IAAI,CAAC,kBAAkB,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QACtC,OAAO,GAAG,EAAE,CAAC,IAAI,CAAC,kBAAkB,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;IACxD,CAAC;IAED;;;;;;;;;;;;;OAaG;IACH,eAAe,CAAC,IAAY;QAC1B,wEAAwE;QACxE,mCAAmC;QACnC,sBAAsB,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IACxC,CAAC;IAED;;;OAGG;IACH,eAAe,CAAC,KAA8B;QAC5C,IAAI,CAAC,YAAY,GAAG,KAAK,CAAC;QAC1B,KAAK,MAAM,QAAQ,IAAI,IAAI,CAAC,iBAAiB;YAAE,QAAQ,CAAC,KAAK,CAAC,CAAC;IACjE,CAAC;IAED,gBAAgB,CAAC,KAA6B;QAC5C,IAAI,CAAC,aAAa,GAAG,KAAK,CAAC;QAC3B,KAAK,MAAM,QAAQ,IAAI,IAAI,CAAC,kBAAkB;YAAE,QAAQ,CAAC,KAAK,CAAC,CAAC;IAClE,CAAC;IAED;;;;;;;;;;;;OAYG;IACH,IAAI,YAAY;QACd,OAAO,sBAAsB,EAAE,CAAC,YAAY,CAAC;IAC/C,CAAC;IAED,IAAI,QAAQ;QACV,OAAO,IAAI,CAAC,YAAY,CAAC;IAC3B,CAAC;IAED,IAAI,SAAS;QACX,OAAO,IAAI,CAAC,aAAa,CAAC;IAC5B,CAAC;IAED,uEAAuE;IACvE,KAAK;QACH,IAAI,CAAC,iBAAiB,CAAC,KAAK,EAAE,CAAC;QAC/B,IAAI,CAAC,kBAAkB,CAAC,KAAK,EAAE,CAAC;IAClC,CAAC;CACF"}
1
+ {"version":3,"file":"bus.js","sourceRoot":"","sources":["../../src/shared/bus.ts"],"names":[],"mappings":"AA0BA,OAAO,EAAE,sBAAsB,EAAE,MAAM,yBAAyB,CAAC;AAEjE,MAAM,OAAO,WAAW;IACtB;;;;;;;;MAQE;IACe,iBAAiB,GAAG,IAAI,GAAG,EAEzC,CAAC;IACa,kBAAkB,GAAG,IAAI,GAAG,EAE1C,CAAC;IAEJ,yGAAyG;IACjG,YAAY,GAA4B,EAAE,CAAC;IAC3C,aAAa,GAA2B,SAAS,CAAC;IAE1D,UAAU,CAAC,QAAgC;QACzC,OAAO,sBAAsB,EAAE,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC;IACtD,CAAC;IAED,UAAU,CAAC,QAAkD;QAC3D,IAAI,CAAC,iBAAiB,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QACrC,OAAO,GAAG,EAAE,CAAC,IAAI,CAAC,iBAAiB,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;IACvD,CAAC;IAED,WAAW,CAAC,QAAiD;QAC3D,IAAI,CAAC,kBAAkB,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QACtC,OAAO,GAAG,EAAE,CAAC,IAAI,CAAC,kBAAkB,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;IACxD,CAAC;IAED;;;;;;;;;;;;;OAaG;IACH,eAAe,CAAC,IAAY;QAC1B,wEAAwE;QACxE,mCAAmC;QACnC,sBAAsB,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IACxC,CAAC;IAED;;;OAGG;IACH,eAAe,CAAC,KAA8B;QAC5C,IAAI,CAAC,YAAY,GAAG,KAAK,CAAC;QAC1B,KAAK,MAAM,QAAQ,IAAI,IAAI,CAAC,iBAAiB;YAAE,QAAQ,CAAC,KAAK,CAAC,CAAC;IACjE,CAAC;IAED,gBAAgB,CAAC,KAA6B;QAC5C,IAAI,CAAC,aAAa,GAAG,KAAK,CAAC;QAC3B,KAAK,MAAM,QAAQ,IAAI,IAAI,CAAC,kBAAkB;YAAE,QAAQ,CAAC,KAAK,CAAC,CAAC;IAClE,CAAC;IAED;;;;;;;;;;;;OAYG;IACH,IAAI,YAAY;QACd,OAAO,sBAAsB,EAAE,CAAC,YAAY,CAAC;IAC/C,CAAC;IAED,IAAI,QAAQ;QACV,OAAO,IAAI,CAAC,YAAY,CAAC;IAC3B,CAAC;IAED,IAAI,SAAS;QACX,OAAO,IAAI,CAAC,aAAa,CAAC;IAC5B,CAAC;IAED,uEAAuE;IACvE,KAAK;QACH,IAAI,CAAC,iBAAiB,CAAC,KAAK,EAAE,CAAC;QAC/B,IAAI,CAAC,kBAAkB,CAAC,KAAK,EAAE,CAAC;IAClC,CAAC;CACF"}
@@ -1,9 +1,22 @@
1
- /** A click already on the audio graph, and the two things a caller can do with it. */
2
- export type ScheduledClick = {
3
- /** When it has finished sounding, so a caller knows when to forget it. */
4
- readonly endsAt: number;
5
- /** Silences it from `atSeconds`, whether or not it has begun. */
6
- cancel(atSeconds: number): void;
7
- };
1
+ /**
2
+ * The metronome click, in plain Web Audio.
3
+ *
4
+ * Not routed through the synth, and not through the mixer. A click is a
5
+ * monitoring aid rather than part of the music, so the master headroom trim and
6
+ * the per-track balance have no business changing how loud it is — and it
7
+ * should not consume one of the sixteen MIDI channels a score might need.
8
+ *
9
+ * Unlike notes, clicks are scheduled *ahead* at an exact time: an oscillator
10
+ * started with `start(at)` sounds at that sample whatever the main thread is
11
+ * doing. So the click stays steady even when the pump runs late, which makes it
12
+ * a usable reference rather than a second thing to distrust.
13
+ *
14
+ * Which is also why scheduling one hands back a way to take it back. The
15
+ * engine keeps a whole horizon of them queued, and `allSoundOff` does not
16
+ * reach any of it — that speaks to the synth, and a click deliberately does
17
+ * not go through the synth. So pausing left the room ticking for as long as
18
+ * the horizon was deep.
19
+ */
20
+ import type { ScheduledClick } from '../../playback/synth-backend.js';
8
21
  export declare function scheduleClick(context: BaseAudioContext, destination: AudioNode, atSeconds: number, accent: boolean): ScheduledClick;
9
22
  //# sourceMappingURL=click.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"click.d.ts","sourceRoot":"","sources":["../../../src/web/playback/click.ts"],"names":[],"mappings":"AAyBA,sFAAsF;AACtF,MAAM,MAAM,cAAc,GAAG;IAC3B,0EAA0E;IAC1E,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,iEAAiE;IACjE,MAAM,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;CACjC,CAAC;AAEF,wBAAgB,aAAa,CAC3B,OAAO,EAAE,gBAAgB,EACzB,WAAW,EAAE,SAAS,EACtB,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,OAAO,GACd,cAAc,CAqChB"}
1
+ {"version":3,"file":"click.d.ts","sourceRoot":"","sources":["../../../src/web/playback/click.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iCAAiC,CAAC;AAQtE,wBAAgB,aAAa,CAC3B,OAAO,EAAE,gBAAgB,EACzB,WAAW,EAAE,SAAS,EACtB,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,OAAO,GACd,cAAc,CAqChB"}
@@ -1,22 +1,3 @@
1
- /**
2
- * The metronome click, in plain Web Audio.
3
- *
4
- * Not routed through the synth, and not through the mixer. A click is a
5
- * monitoring aid rather than part of the music, so the master headroom trim and
6
- * the per-track balance have no business changing how loud it is — and it
7
- * should not consume one of the sixteen MIDI channels a score might need.
8
- *
9
- * Unlike notes, clicks are scheduled *ahead* at an exact time: an oscillator
10
- * started with `start(at)` sounds at that sample whatever the main thread is
11
- * doing. So the click stays steady even when the pump runs late, which makes it
12
- * a usable reference rather than a second thing to distrust.
13
- *
14
- * Which is also why scheduling one hands back a way to take it back. The
15
- * engine keeps a whole horizon of them queued, and `allSoundOff` does not
16
- * reach any of it — that speaks to the synth, and a click deliberately does
17
- * not go through the synth. So pausing left the room ticking for as long as
18
- * the horizon was deep.
19
- */
20
1
  const ACCENT_HZ = 1500;
21
2
  const BEAT_HZ = 1000;
22
3
  const CLICK_SECONDS = 0.03;
@@ -1 +1 @@
1
- {"version":3,"file":"click.js","sourceRoot":"","sources":["../../../src/web/playback/click.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,SAAS,GAAG,IAAI,CAAC;AACvB,MAAM,OAAO,GAAG,IAAI,CAAC;AACrB,MAAM,aAAa,GAAG,IAAI,CAAC;AAC3B,wEAAwE;AACxE,MAAM,UAAU,GAAG,IAAI,CAAC;AAUxB,MAAM,UAAU,aAAa,CAC3B,OAAyB,EACzB,WAAsB,EACtB,SAAiB,EACjB,MAAe;IAEf,MAAM,UAAU,GAAG,OAAO,CAAC,gBAAgB,EAAE,CAAC;IAC9C,MAAM,IAAI,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC;IAClC,UAAU,CAAC,SAAS,CAAC,KAAK,GAAG,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC;IAC1D,UAAU,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IACzB,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC;IAE1B,6EAA6E;IAC7E,2BAA2B;IAC3B,MAAM,GAAG,GAAG,SAAS,GAAG,aAAa,CAAC;IACtC,IAAI,CAAC,IAAI,CAAC,cAAc,CAAC,UAAU,EAAE,SAAS,CAAC,CAAC;IAChD,IAAI,CAAC,IAAI,CAAC,4BAA4B,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAEpD,UAAU,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;IAC5B,UAAU,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IACrB,IAAI,QAAQ,GAAG,KAAK,CAAC;IACrB,UAAU,CAAC,OAAO,GAAG,GAAG,EAAE;QACxB,QAAQ,GAAG,IAAI,CAAC;QAChB,UAAU,CAAC,UAAU,EAAE,CAAC;QACxB,IAAI,CAAC,UAAU,EAAE,CAAC;IACpB,CAAC,CAAC;IAEF,OAAO;QACL,MAAM,EAAE,GAAG;QACX,MAAM,CAAC,IAAY;YACjB,oEAAoE;YACpE,yEAAyE;YACzE,kEAAkE;YAClE,IAAI,QAAQ,IAAI,IAAI,IAAI,GAAG;gBAAE,OAAO;YACpC,qEAAqE;YACrE,wDAAwD;YACxD,IAAI,CAAC,IAAI,CAAC,qBAAqB,CAAC,IAAI,CAAC,CAAC;YACtC,IAAI,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC;YAClC,6DAA6D;YAC7D,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACxB,CAAC;KACF,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"click.js","sourceRoot":"","sources":["../../../src/web/playback/click.ts"],"names":[],"mappings":"AAqBA,MAAM,SAAS,GAAG,IAAI,CAAC;AACvB,MAAM,OAAO,GAAG,IAAI,CAAC;AACrB,MAAM,aAAa,GAAG,IAAI,CAAC;AAC3B,wEAAwE;AACxE,MAAM,UAAU,GAAG,IAAI,CAAC;AAExB,MAAM,UAAU,aAAa,CAC3B,OAAyB,EACzB,WAAsB,EACtB,SAAiB,EACjB,MAAe;IAEf,MAAM,UAAU,GAAG,OAAO,CAAC,gBAAgB,EAAE,CAAC;IAC9C,MAAM,IAAI,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC;IAClC,UAAU,CAAC,SAAS,CAAC,KAAK,GAAG,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC;IAC1D,UAAU,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IACzB,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC;IAE1B,6EAA6E;IAC7E,2BAA2B;IAC3B,MAAM,GAAG,GAAG,SAAS,GAAG,aAAa,CAAC;IACtC,IAAI,CAAC,IAAI,CAAC,cAAc,CAAC,UAAU,EAAE,SAAS,CAAC,CAAC;IAChD,IAAI,CAAC,IAAI,CAAC,4BAA4B,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAEpD,UAAU,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;IAC5B,UAAU,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IACrB,IAAI,QAAQ,GAAG,KAAK,CAAC;IACrB,UAAU,CAAC,OAAO,GAAG,GAAG,EAAE;QACxB,QAAQ,GAAG,IAAI,CAAC;QAChB,UAAU,CAAC,UAAU,EAAE,CAAC;QACxB,IAAI,CAAC,UAAU,EAAE,CAAC;IACpB,CAAC,CAAC;IAEF,OAAO;QACL,MAAM,EAAE,GAAG;QACX,MAAM,CAAC,IAAY;YACjB,oEAAoE;YACpE,yEAAyE;YACzE,kEAAkE;YAClE,IAAI,QAAQ,IAAI,IAAI,IAAI,GAAG;gBAAE,OAAO;YACpC,qEAAqE;YACrE,wDAAwD;YACxD,IAAI,CAAC,IAAI,CAAC,qBAAqB,CAAC,IAAI,CAAC,CAAC;YACtC,IAAI,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC;YAClC,6DAA6D;YAC7D,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACxB,CAAC;KACF,CAAC;AACJ,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sudobility/music_player",
3
- "version": "0.1.27",
3
+ "version": "0.1.29",
4
4
  "description": "Playback for ScoreSmith: transport, synth engines and offline rendering, for web and React Native",
5
5
  "author": "Sudobility",
6
6
  "license": "BUSL-1.1",
@@ -63,17 +63,17 @@
63
63
  "scripts": {
64
64
  "build": "tsc -p tsconfig.esm.json",
65
65
  "clean": "rimraf dist",
66
- "test": "vitest run",
66
+ "test:unit": "vitest run",
67
67
  "test:watch": "vitest",
68
68
  "lint": "eslint src",
69
69
  "format": "prettier --write src/**/*.ts",
70
70
  "format:check": "prettier --check src/**/*.ts",
71
71
  "typecheck": "tsc --noEmit",
72
- "verify": "bun run format:check && bun run typecheck && bun run lint && bun run test && bun run build",
72
+ "verify": "bun run format:check && bun run typecheck && bun run lint && bun run test:unit && bun run build",
73
73
  "prepublishOnly": "bun run clean && bun run verify"
74
74
  },
75
75
  "peerDependencies": {
76
- "@sudobility/music_types": "^0.13.26",
76
+ "@sudobility/music_types": "^0.13.28",
77
77
  "js-synthesizer": "^1.13.0",
78
78
  "react-native-audio-api": ">=0.13.0"
79
79
  },
@@ -87,7 +87,7 @@
87
87
  },
88
88
  "devDependencies": {
89
89
  "@eslint/js": "^10.0.1",
90
- "@sudobility/music_types": "^0.13.26",
90
+ "@sudobility/music_types": "^0.13.28",
91
91
  "@types/node": "^26.2.0",
92
92
  "@typescript-eslint/eslint-plugin": "^8.65.0",
93
93
  "@typescript-eslint/parser": "^8.65.0",