@sudobility/music_player 0.1.0 → 0.1.2

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 (159) hide show
  1. package/CLAUDE.md +335 -0
  2. package/dist/core.d.ts +33 -0
  3. package/dist/core.d.ts.map +1 -0
  4. package/dist/core.js +33 -0
  5. package/dist/core.js.map +1 -0
  6. package/dist/engine.d.ts +103 -0
  7. package/dist/engine.d.ts.map +1 -0
  8. package/dist/engine.js +2 -0
  9. package/dist/engine.js.map +1 -0
  10. package/dist/mocks/index.d.ts +46 -0
  11. package/dist/mocks/index.d.ts.map +1 -0
  12. package/dist/mocks/index.js +93 -0
  13. package/dist/mocks/index.js.map +1 -0
  14. package/dist/player.d.ts +81 -0
  15. package/dist/player.d.ts.map +1 -0
  16. package/dist/player.js +209 -0
  17. package/dist/player.js.map +1 -0
  18. package/dist/rn/audio/offline-render.d.ts +29 -0
  19. package/dist/rn/audio/offline-render.d.ts.map +1 -0
  20. package/dist/rn/audio/offline-render.js +134 -0
  21. package/dist/rn/audio/offline-render.js.map +1 -0
  22. package/dist/rn/index.d.ts +50 -0
  23. package/dist/rn/index.d.ts.map +1 -0
  24. package/dist/rn/index.js +39 -0
  25. package/dist/rn/index.js.map +1 -0
  26. package/dist/rn/playback/audio-api.d.ts +93 -0
  27. package/dist/rn/playback/audio-api.d.ts.map +1 -0
  28. package/dist/rn/playback/audio-api.js +24 -0
  29. package/dist/rn/playback/audio-api.js.map +1 -0
  30. package/dist/rn/playback/expression-table.d.ts +22 -0
  31. package/dist/rn/playback/expression-table.d.ts.map +1 -0
  32. package/dist/rn/playback/expression-table.js +926 -0
  33. package/dist/rn/playback/expression-table.js.map +1 -0
  34. package/dist/rn/playback/expression.d.ts +48 -0
  35. package/dist/rn/playback/expression.d.ts.map +1 -0
  36. package/dist/rn/playback/expression.js +120 -0
  37. package/dist/rn/playback/expression.js.map +1 -0
  38. package/dist/rn/playback/gm-pack-name.d.ts +40 -0
  39. package/dist/rn/playback/gm-pack-name.d.ts.map +1 -0
  40. package/dist/rn/playback/gm-pack-name.js +61 -0
  41. package/dist/rn/playback/gm-pack-name.js.map +1 -0
  42. package/dist/rn/playback/pack-library.d.ts +44 -0
  43. package/dist/rn/playback/pack-library.d.ts.map +1 -0
  44. package/dist/rn/playback/pack-library.js +85 -0
  45. package/dist/rn/playback/pack-library.js.map +1 -0
  46. package/dist/rn/playback/pack-names.json +130 -0
  47. package/dist/rn/playback/sample-engine.d.ts +216 -0
  48. package/dist/rn/playback/sample-engine.d.ts.map +1 -0
  49. package/dist/rn/playback/sample-engine.js +669 -0
  50. package/dist/rn/playback/sample-engine.js.map +1 -0
  51. package/dist/rn/playback/sample-pack.d.ts +71 -0
  52. package/dist/rn/playback/sample-pack.d.ts.map +1 -0
  53. package/dist/rn/playback/sample-pack.js +112 -0
  54. package/dist/rn/playback/sample-pack.js.map +1 -0
  55. package/dist/rn/playback/sustain-loop.d.ts +24 -0
  56. package/dist/rn/playback/sustain-loop.d.ts.map +1 -0
  57. package/dist/rn/playback/sustain-loop.js +111 -0
  58. package/dist/rn/playback/sustain-loop.js.map +1 -0
  59. package/dist/rn/playback/voice-plan.d.ts +60 -0
  60. package/dist/rn/playback/voice-plan.d.ts.map +1 -0
  61. package/dist/rn/playback/voice-plan.js +38 -0
  62. package/dist/rn/playback/voice-plan.js.map +1 -0
  63. package/dist/shared/bus.d.ts +73 -0
  64. package/dist/shared/bus.d.ts.map +1 -0
  65. package/dist/shared/bus.js +83 -0
  66. package/dist/shared/bus.js.map +1 -0
  67. package/dist/shared/midi.d.ts +18 -0
  68. package/dist/shared/midi.d.ts.map +1 -0
  69. package/dist/shared/midi.js +25 -0
  70. package/dist/shared/midi.js.map +1 -0
  71. package/dist/shared/mix.d.ts +75 -0
  72. package/dist/shared/mix.d.ts.map +1 -0
  73. package/dist/shared/mix.js +115 -0
  74. package/dist/shared/mix.js.map +1 -0
  75. package/dist/shared/note-queue.d.ts +43 -0
  76. package/dist/shared/note-queue.d.ts.map +1 -0
  77. package/dist/shared/note-queue.js +54 -0
  78. package/dist/shared/note-queue.js.map +1 -0
  79. package/dist/shared/plan.d.ts +34 -0
  80. package/dist/shared/plan.d.ts.map +1 -0
  81. package/dist/shared/plan.js +134 -0
  82. package/dist/shared/plan.js.map +1 -0
  83. package/dist/shared/pump-window.d.ts +38 -0
  84. package/dist/shared/pump-window.d.ts.map +1 -0
  85. package/dist/shared/pump-window.js +13 -0
  86. package/dist/shared/pump-window.js.map +1 -0
  87. package/dist/shared/render-events.d.ts +25 -0
  88. package/dist/shared/render-events.d.ts.map +1 -0
  89. package/dist/shared/render-events.js +79 -0
  90. package/dist/shared/render-events.js.map +1 -0
  91. package/dist/shared/sounding-set.d.ts +45 -0
  92. package/dist/shared/sounding-set.d.ts.map +1 -0
  93. package/dist/shared/sounding-set.js +81 -0
  94. package/dist/shared/sounding-set.js.map +1 -0
  95. package/dist/shared/test-plan.d.ts +36 -0
  96. package/dist/shared/test-plan.d.ts.map +1 -0
  97. package/dist/shared/test-plan.js +83 -0
  98. package/dist/shared/test-plan.js.map +1 -0
  99. package/dist/singleton.d.ts +23 -0
  100. package/dist/singleton.d.ts.map +1 -0
  101. package/dist/singleton.js +26 -0
  102. package/dist/singleton.js.map +1 -0
  103. package/dist/types.d.ts +84 -0
  104. package/dist/types.d.ts.map +1 -0
  105. package/dist/types.js +2 -0
  106. package/dist/types.js.map +1 -0
  107. package/dist/web/audio/offline-synth.d.ts +24 -0
  108. package/dist/web/audio/offline-synth.d.ts.map +1 -0
  109. package/dist/web/audio/offline-synth.js +92 -0
  110. package/dist/web/audio/offline-synth.js.map +1 -0
  111. package/dist/web/audio/soundfont-render.d.ts +31 -0
  112. package/dist/web/audio/soundfont-render.d.ts.map +1 -0
  113. package/dist/web/audio/soundfont-render.js +130 -0
  114. package/dist/web/audio/soundfont-render.js.map +1 -0
  115. package/dist/web/audio/synth-types.d.ts +24 -0
  116. package/dist/web/audio/synth-types.d.ts.map +1 -0
  117. package/dist/web/audio/synth-types.js +2 -0
  118. package/dist/web/audio/synth-types.js.map +1 -0
  119. package/dist/web/index.d.ts +40 -0
  120. package/dist/web/index.d.ts.map +1 -0
  121. package/dist/web/index.js +59 -0
  122. package/dist/web/index.js.map +1 -0
  123. package/dist/web/playback/channel-allocator.d.ts +54 -0
  124. package/dist/web/playback/channel-allocator.d.ts.map +1 -0
  125. package/dist/web/playback/channel-allocator.js +72 -0
  126. package/dist/web/playback/channel-allocator.js.map +1 -0
  127. package/dist/web/playback/click.d.ts +9 -0
  128. package/dist/web/playback/click.d.ts.map +1 -0
  129. package/dist/web/playback/click.js +61 -0
  130. package/dist/web/playback/click.js.map +1 -0
  131. package/dist/web/playback/clock.d.ts +36 -0
  132. package/dist/web/playback/clock.d.ts.map +1 -0
  133. package/dist/web/playback/clock.js +64 -0
  134. package/dist/web/playback/clock.js.map +1 -0
  135. package/dist/web/playback/governor.d.ts +45 -0
  136. package/dist/web/playback/governor.d.ts.map +1 -0
  137. package/dist/web/playback/governor.js +33 -0
  138. package/dist/web/playback/governor.js.map +1 -0
  139. package/dist/web/playback/quiet-stub-notices.d.ts +55 -0
  140. package/dist/web/playback/quiet-stub-notices.d.ts.map +1 -0
  141. package/dist/web/playback/quiet-stub-notices.js +93 -0
  142. package/dist/web/playback/quiet-stub-notices.js.map +1 -0
  143. package/dist/web/playback/soundfont-engine.d.ts +281 -0
  144. package/dist/web/playback/soundfont-engine.d.ts.map +1 -0
  145. package/dist/web/playback/soundfont-engine.js +735 -0
  146. package/dist/web/playback/soundfont-engine.js.map +1 -0
  147. package/dist/web/playback/soundfont-loader.d.ts +30 -0
  148. package/dist/web/playback/soundfont-loader.d.ts.map +1 -0
  149. package/dist/web/playback/soundfont-loader.js +68 -0
  150. package/dist/web/playback/soundfont-loader.js.map +1 -0
  151. package/dist/web/playback/synth-host.d.ts +142 -0
  152. package/dist/web/playback/synth-host.d.ts.map +1 -0
  153. package/dist/web/playback/synth-host.js +293 -0
  154. package/dist/web/playback/synth-host.js.map +1 -0
  155. package/package.json +18 -3
  156. package/dist/index.d.ts +0 -14
  157. package/dist/index.d.ts.map +0 -1
  158. package/dist/index.js +0 -2
  159. package/dist/index.js.map +0 -1
package/CLAUDE.md ADDED
@@ -0,0 +1,335 @@
1
+ # music_player
2
+
3
+ > **Git policy — never auto-commit or auto-push.** Leave your work in the working tree.
4
+ > Run `git commit`, `git push` or `npm publish` **only when the user explicitly asks in
5
+ > that turn**. Approval for an earlier change does not carry forward, and finishing a
6
+ > task is not permission to commit it.
7
+
8
+ Everything that makes sound: the transport, the two synth engines, plan
9
+ building, and offline rendering. One of seven repos in the Moosiac family — see
10
+ `music_app/docs/architecture.md`.
11
+
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.
15
+
16
+ Its own package because playback was previously spread across three: the engines
17
+ in `music_io`, the transport brain in `music_lib` where it read the Zustand
18
+ store, and the plan builders in `music_lib` too. Nothing owned playback; three
19
+ packages owned a third of it each.
20
+
21
+ ## Structure
22
+
23
+ - `src/types.ts` — `IMusicPlayer`, the transport interface
24
+ - `src/engine.ts` — the engine contract (`PlaybackEngine`, `PlaybackObserver`, `AuditionVoice`)
25
+ - `src/player.ts` — `MusicPlayer`, the transport; was `PlaybackController`, minus its store
26
+ - `src/singleton.ts` — `initializeMusicPlayer` / `getMusicPlayer` / `resetMusicPlayer`
27
+ - `src/core.ts` — the **platform-free** entry: interface, singleton, engine contract, plan builders
28
+ - `src/shared/` — `plan.ts`, `render-events.ts`, `bus.ts`, and the scheduling primitives both engines use
29
+ - `src/web/` — libfluidsynth (WebAssembly) in an `AudioWorklet`
30
+ - `src/rn/` — a sample-based FluidR3 engine
31
+ - `src/mocks/` — `MockMusicPlayer`, for downstream tests
32
+
33
+ ## Package boundaries
34
+
35
+ `src/contract/no-domain-imports.test.ts` enforces: no `music_lib`, no
36
+ `music_io`, no `vexflow`, no runtime dependency. `music_types` is the only
37
+ required peer; the two engine libraries are **optional** peers, so a web app
38
+ installs `js-synthesizer` and a React Native app installs
39
+ `react-native-audio-api` without either carrying the other's.
40
+
41
+ ## Gotchas
42
+
43
+ - **`IMusicPlayer.load()` takes a `Score`; the engine still takes a plan.** This
44
+ package owns `playbackPlan`, so making the caller build one first would be a
45
+ two-step dance whose second step is here. Underneath, `PlaybackEngine.load()`
46
+ still takes a `PlaybackPlan` and does no score maths — which is where
47
+ `music_io`'s "handed a plan, never a score" rule was always actually about.
48
+ - **The player has no caret, and that is deliberate.** `togglePlay`,
49
+ `seekToMeasure`, `goToStart`, `previousMeasure`, `nextMeasure` and
50
+ loop-from-selection are **not** here: all of them read or write the caret or
51
+ the selection, which is editing state. A copy of either here would be a second
52
+ thing that can disagree with the first — the exact bug `MusicPosition` exists
53
+ to prevent. They live in music_lib's playback adapter and compose the
54
+ primitives this exposes.
55
+ - **`music_lib` imports this package only through `/core`.** The root export
56
+ resolves to the web entry (or the RN one under Metro), both of which construct
57
+ a synth. `/core` is the platform-free half. A bare root import from music_lib
58
+ would put an `AudioWorklet` in a package whose whole point is that it has none
59
+ — its own guard test asserts this, and it was caught that way.
60
+ - **The plan model stays in `music_types`; the engine contract lives here.**
61
+ `PlaybackPlan` composes `PerformanceTimeline`, which `performanceTimeline()`
62
+ in music_types produces, so the whole plan cluster is anchored there — moving
63
+ it would make music_types import from this package. The engine contract moved
64
+ because this package is the only thing that implements *or* calls it.
65
+ - **`load()` while playing is a mix change and nothing else.** The note queue is
66
+ kept. The host's edit lock is what makes that safe: while the transport runs,
67
+ the only score change that can reach here is a mix change. A score arriving
68
+ from outside that path — a generation result, an opened snapshot — **must be
69
+ preceded by `stop()`**, or it will be heard as the old score.
70
+ - **`load()` and `play()` reject; they do not swallow.** The host translates: the
71
+ message a user sees is localized and this package carries no copy. An earlier
72
+ draft took an `onError` callback and stranded music_lib's translated string.
73
+ - **The bus is exposed, not wrapped.** `IMusicPlayer.bus` is the real
74
+ `PlaybackBus`, so a host binds React to the same object the engine publishes
75
+ into. A delegating view was tried and is wrong: two objects answering "what is
76
+ sounding" is the disagreement the single source of truth exists to remove.
77
+ - **`bus.onPosition` and `IMusicPosition.tick` are not duplicates.** The channel
78
+ is a *change signal*; the authoritative value is dead-reckoned forward between
79
+ reports so the caret moves smoothly. A subscriber taking the tick from the
80
+ event gets the unsmoothed number.
81
+ - **Synth load progress is not a bus channel.** It reports per percent and
82
+ behaves like ordinary React state, so it goes to the host's store via
83
+ `onLoadState`.
84
+ - **Consumers must exclude this package from Vite's dep pre-bundling.** The web
85
+ entry lazily `import('js-synthesizer')`, and esbuild's pre-bundler resolves it
86
+ eagerly into a chunk whose worklet asset URLs no longer point where the app
87
+ serves them. The symptom is a blank page with no console error and a
88
+ *production build that works fine*; music_app's `vite.config.ts` lists it
89
+ beside music_lib and music_io.
90
+
91
+ - **Platform *engines* are optional peers; `@breezystack/lamejs` is the one
92
+ exception.** A React Native app must never pull a browser audio library into
93
+ its graph, which is why `js-synthesizer` — libfluidsynth compiled to WASM, for
94
+ an AudioWorklet — is an optional peer that music_app declares itself. lamejs
95
+ stays a runtime `dependency` for two concrete reasons: `web-audio-codec.ts`
96
+ imports it *statically*, so an app that skipped installing it would fail to
97
+ load the entire web entry rather than just mp3 export; and `AudioCodec.encodeMp3`
98
+ is synchronous in `music_types`, so it cannot be made lazy the way
99
+ `js-synthesizer` is without changing that interface across four repos. It is
100
+ pure JS and runs under Metro — and now that `encodeMp3` is shared, the RN
101
+ entry imports it too, which settles the question: it is not a platform
102
+ library at all.
103
+ - **There is no RN audio, and that is a decision, not a gap in the port.** The
104
+ web engine is libfluidsynth compiled to WASM running in an `AudioWorklet`;
105
+ React Native has no AudioWorklet, so there is nothing to port — an RN engine
106
+ means a different implementation of the same `PlaybackEngine` interface (a
107
+ native synth module, or `react-native-audio-api` plus a soundfont player).
108
+ What was there before subclassed the Tone engine over
109
+ `react-native-audio-api` and synthesised its own approximations of the
110
+ instruments, so it could never match what the web plays; a wrong sound is
111
+ worse than a clear error. `unavailablePlayback()` now throws from anything
112
+ that would make a sound and stays a silent no-op for the lifecycle methods, so
113
+ a shared component calling `pause()` on unmount does not crash the app. The
114
+ historical spikes are in `spikes/tone-on-rn-audio-api.md`.
115
+ - **The two platforms play the same font by different means, and that is the
116
+ design.** Web runs libfluidsynth (WebAssembly) in an `AudioWorklet`; React
117
+ Native has neither — `react-native-audio-api` exposes no `addModule` and
118
+ Hermes has no WebAssembly — so it plays **FluidR3 pre-rendered per note**
119
+ instead, from Benjamin Gleitzman's packs, generated from the same
120
+ `FluidR3_GM.sf2` the web plays as `FluidR3Mono_GM.sf3`. Every key of all 128
121
+ melodic programs is a real recording, so nothing is pitch-shifted in range.
122
+ What is lost is SF2's live per-zone filters, LFOs and modulator envelopes —
123
+ expressive variation, not the instrument's identity. The engine that was
124
+ deleted synthesised oscillator approximations, which is a different and much
125
+ worse thing. See `spikes/rn-sample-playback.md`.
126
+ - **`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.
127
+ - **The transport is shared; only voicing is per platform.** `shared/playback/`
128
+ holds score flattening, the note cursor, the sounding set, the dispatch window
129
+ and the master trim. `governor.ts` and `channel-allocator.ts`
130
+ stayed in `web/` despite importing nothing — they are fluidsynth-specific in
131
+ meaning (interpolation orders; channels per synth instance) — and
132
+ `soundfont-loader.ts` stayed because it uses the browser `caches` global.
133
+ Import-free is not the same as platform-free.
134
+ - **`pump-window.ts` has one consumer left, and it is the RN engine.** The web
135
+ engine hands a four-second horizon of self-releasing notes to the fluidsynth
136
+ sequencer, so nothing there is ever dispatched late enough to need skipping.
137
+ RN has no sequencer to hand a horizon to and keeps the lookahead-and-grace
138
+ model, which is also why `NoteQueue.drainUntil`'s cap is optional: the web
139
+ engine passes one because every note it schedules is a `postMessage`, and RN
140
+ posts nothing across a thread boundary so it has nothing to bound.
141
+ - **One synth instance addresses 256 channels, not 16.** `midiChannelCount`
142
+ takes 16-256 in multiples of 16, so every score up to 240 pitched tracks runs
143
+ on one instance with one copy of the 23MB font. A second instance — and a
144
+ second font copy — now opens past 240 tracks rather than past 15. Channels
145
+ where `c % 16 === 9` are reserved for percussion: only literal channel 9 is
146
+ documented as drum-typed by default, and whether fluidsynth extends that past
147
+ its first block cannot be determined without a real synth, so pitched tracks
148
+ stay off all of them. **`needsDrumTypeSwitch` must be false for a pitched
149
+ track** — `soundfont-render.ts` branches on
150
+ `isPercussion || needsDrumTypeSwitch`, so a pitched track carrying it exports
151
+ as percussion. That shipped for one commit.
152
+ - **Polyphony is init-time only.** `ISynthesizer` has `setInterpolation` and
153
+ `setGain` and no polyphony setter, so the ceiling (2048) is chosen once in
154
+ `synth-host.ts` and fluidsynth's overflow priority steals above it. Anything
155
+ proposing to adapt polyphony at runtime is proposing to re-init the synth,
156
+ which means reloading the font.
157
+ - **The offline renderer shares `allocateChannels` with playback.** Change the
158
+ channel count and `audio/offline-synth.ts` must move in the same commit, or an
159
+ export addresses channels its synth does not have and silently loses every
160
+ track past the sixteenth.
161
+ - **Timing lives in the worklet.** The pump tops up a rolling horizon
162
+ (`HORIZON_SECONDS`, 512 events per tick) and each note is one sequencer event
163
+ carrying its own duration, so fluidsynth releases it on the audio thread.
164
+ There is no grace window and no skip-late rule on the web engine; a note whose
165
+ moment passed during a stall sounds at once rather than being dropped. The old
166
+ 200ms window plus 200ms grace meant a stall over ~400ms silently lost every
167
+ note inside it, and a 200-track notation redraw measures 119ms. Do not
168
+ reintroduce a skip rule without re-reading that.
169
+ - **A four-second horizon means anything that changes the future has to take
170
+ the future back.** The three that do are `seek`/`stop` (already), a playback
171
+ **speed** change and the **loop** wrap. A speed change re-anchors the caret
172
+ instantly while every queued note still carries a delay computed at the old
173
+ speed, so without a re-seek the music kept the previous tempo for a whole
174
+ horizon and then jumped; `setTempoMultiplier` therefore re-seeks where it
175
+ stands when the pump is running, costing one 50ms gap. A loop wrap only
176
+ happens on the pump tick *after* the end is reached, so `horizonTick` clamps
177
+ the drain to `endTick - 1` — otherwise notes just past the end were already in
178
+ the sequencer and sounded before the loop came round, every pass. For the same
179
+ reason a loop never lets the transport stop: its range may run past the last
180
+ note. **Metronome clicks are the case that bites**, because they are real
181
+ oscillators on the graph and `allSoundOff` speaks only to the synth — a
182
+ scheduled click is unreachable from there. `scheduleClick` hands back a
183
+ `cancel`, the engine holds what is pending, and stop, seek, dispose and
184
+ switching the metronome off all take it back. Before that, pausing left the
185
+ room ticking for four seconds.
186
+ - **`headroomTrimFor` is in `shared/playback/mix.ts` so playback and export
187
+ cannot disagree, and for a while the web export simply did not call it.**
188
+ Every channel is scheduled at its own MIDI volume with nothing reconciling
189
+ them, so a rendered file came out `sqrt(n)` louder than what was heard and
190
+ the wav/mp3 encoders — which hard-clamp — turned that into clipping across
191
+ most of a large score. The RN renderer always applied it. `mix.ts`'s doc now
192
+ names all four call sites, so a fifth is noticed by its absence.
193
+ - **The export limiter is not the live one, and works backwards.** `limitPeaks`
194
+ is the offline half of the master bus, after the trim exactly as
195
+ `DynamicsCompressorNode` sits after the master gain live — the trim sizes the
196
+ mix for its *average* level and the phase-aligned moments still overshoot,
197
+ which in an export means the encoders' clamp rather than a sound card. It does
198
+ **not** reimplement that node: its knee, ratio and program-dependent behaviour
199
+ are not specified closely enough to reproduce, and the job — nothing leaves
200
+ above the ceiling, by ducking rather than clipping — is what is reproduced.
201
+ The gain envelope is computed **backwards** (each sample's gain is the lower
202
+ of what it needs and what the next allows plus one attack step) so the ramp is
203
+ finished when the peak lands, and so the ceiling is *exact*: a forward
204
+ one-pole never quite arrives, and clamping the residue is the clipping this
205
+ replaces. The forward pass is the release, and it is what stops a loud
206
+ sustained passage pumping once per cycle. RN's renderers have no limiter at
207
+ all, matching RN playback, which also has none.
208
+ - **A note-on at velocity 0 is a note-off.** The offline renderer always clamped
209
+ to 1..127; the live engine passed `PlaybackNote.velocity` through untouched,
210
+ so a plan carrying a fractional or zero velocity produced a note that silently
211
+ did not sound in playback and did sound in the export. `clampVelocity` in
212
+ `soundfont-engine.ts` closes that. Note the unit trap either side of it:
213
+ `PlaybackNote.velocity` is MIDI 0-127 and `RenderEvent.velocity` is 0..1.
214
+ - **`onActiveNotes` fires only on change** (`shared/playback/sounding-set.ts`).
215
+ It used to be a fresh array every pump tick whether or not a note had begun or
216
+ ended, waking notation colour and the keyboard's key lights twenty times a
217
+ second through a held chord. Anything that starts re-sending it per tick has
218
+ undone the reason that module exists.
219
+ - **The lit set comes off the clock, never off the voices — on both engines
220
+ now.** RN used to derive it from its scheduled voices, which are built a
221
+ lookahead ahead of the clock and torn down only after the instrument's
222
+ release: notation lit every note 200ms early and stayed lit through a half
223
+ second of decay after the written note was over. `SoundingSet` answers "what
224
+ is sounding *now*", which is the question notation is asking; a voice list
225
+ answers "what has been arranged for", which is a different one.
226
+ - **The RN engine deliberately does not reuse the web engine's pump.** Merging
227
+ the two transports behind a synth port is the obvious refactor and the wrong
228
+ order: the web engine is verified on real devices and the RN one cannot be
229
+ until it runs on a phone. Do that merge after RN audio is device-tested, not
230
+ before.
231
+ - **Pack names are not GM slugs.** Punctuation is *removed*, not turned into a
232
+ separator: `Honky-tonk Piano` is `honkytonk_piano` and `Lead 8 (bass + lead)`
233
+ keeps the double underscore where the `+` was. That covers 127 of 128; program
234
+ 54 is a real naming disagreement (GM "Voice Oohs" vs. the catalogue's "Synth
235
+ Voice"). `gm-pack-name.test.ts` checks all 128 against a vendored copy of the
236
+ CDN manifest, because a wrong name is a 404, which parses to zero samples,
237
+ which is an instrument that plays nothing with no error anywhere.
238
+ - **Drum kits are rendered here, not fetched.** The pre-rendered FluidR3 packs
239
+ are the 128 *melodic* programs only, and no CDN publishes GM percussion under
240
+ a licence this project can use — WebAudioFont, the one that has it, is
241
+ GPL-3.0. So `scripts/build-percussion-packs.mjs` renders the eight GM kits out
242
+ of the same `FluidR3Mono_GM.sf3` the web engine plays, with `fluidsynth` and
243
+ `lame`, into the melodic packs' own format so one parser reads both. FluidR3
244
+ is CC-BY 3.0, so redistributing the renderings is allowed with attribution
245
+ (the script writes an `ATTRIBUTION.md` beside them). Output is ~1.1-1.7MB per
246
+ kit; it is a build-time tool, run when the font changes, not part of
247
+ `bun run build`.
248
+ - **The expression a recording cannot carry is measured back in**
249
+ (`playback/expression.ts`, built by `scripts/measure-expression.mjs`). The
250
+ packs are recordings taken at full velocity, so playing one softer is just the
251
+ loud recording turned down — where fluidsynth also darkens it, shapes its
252
+ release per instrument, and sustains it while held. Three fixes, all measured
253
+ from the same `.sf3` the web plays:
254
+ 1. **Velocity is (v/127)², not v/127.** SF2's default velocity-to-attenuation
255
+ modulator is 960cB concave; fluidsynth's concave table
256
+ (`1 + (40/96)·log10(v/127)`) reduces exactly to a square. The linear curve
257
+ the engine had played velocity 32 at 0.252 against a true 0.063 — **12dB
258
+ too loud on every soft passage**, which flattens all dynamics.
259
+ 2. **A per-instrument low-pass** whose corner falls with velocity: piano goes
260
+ ~4989Hz at v96 to ~556Hz at v16, strings barely move, violin and flute not
261
+ at all. Only 57 of 128 instruments darken, and a node is built only when
262
+ the corner is below 16kHz.
263
+ 3. **A per-instrument release**, median 0.56s measured, against the flat 80ms
264
+ the engine applied to everything — seven times too short, chopping every
265
+ released chord.
266
+ - **The expression table is measured, not extracted, and that is the
267
+ interesting part.** The first attempt parsed `initialFilterFc` straight out of
268
+ the `pdta` chunk (readable even in SF3 — only `sdta` is Vorbis-compressed). It
269
+ gave Grand Piano a **300Hz** low-pass, which is plainly wrong: the rendered
270
+ piano has energy out to 4.6kHz. The cause is layering — FluidR3's piano preset
271
+ points at two instruments at once and the recording is their sum, so no single
272
+ layer's generators describe it. Generators cannot be read off a layered
273
+ preset; the rendered sound can. The script now renders all 128 programs at five
274
+ velocities and finds where the quiet spectrum falls 3dB below the loud one,
275
+ **after normalising both to the fundamental** — skipping that normalisation
276
+ produced a table reading 281Hz for all 128, because a quiet note is quieter at
277
+ every frequency and the first band examined already looks 3dB down.
278
+ - **Generated assets are pinned to the soundfont by hash.** Both
279
+ `expression-table.ts` and the percussion packs are derived from
280
+ `FluidR3Mono_GM.sf3` and regenerated by nobody. Each records the font's
281
+ SHA-256, and music_app — which owns the font — asserts the match in
282
+ `src/config/generated-assets.test.ts`. Without it a swapped font leaves RN
283
+ playing kits and velocity curves from the old one, silently, presenting as
284
+ "RN sounds subtly unlike web". The hash is read through
285
+ `@sudobility/music_io/rn/expression-table`, a dependency-free subpath, because
286
+ reaching it via the RN entry drags in optional peers a web app does not install.
287
+ - **Notes longer than 3.13 seconds used to go silent.** Every pack entry is
288
+ exactly that long, whatever the instrument, so a whole note at 60bpm or a tied
289
+ pad simply stopped sounding partway through with the gain envelope still
290
+ holding a level over nothing. `sustain-loop.ts` loops a window inside the
291
+ steady portion **spanning a whole number of periods of the sampled note** —
292
+ which is known, since it is the pack entry being played, so no pitch detection
293
+ is needed. Zero-crossing alignment alone was not enough: it removes the step
294
+ discontinuity but not the *phase* one, so an arbitrary-length window ends
295
+ part-way through a cycle and every harmonic restarts out of phase, audible as
296
+ a buzz once a second on a held note. Only for the 79
297
+ instruments measured as holding their level: looping a piano or a marimba
298
+ would sustain a note that is supposed to die away, which is a worse and more
299
+ obvious error than the truncation it fixes.
300
+ - **A drum note is never bent** (`exactSample`, not `nearestSample`). In a kit
301
+ a note number names a *different instrument* — 38 is a snare, 42 a closed
302
+ hi-hat — so reaching for the closest available slot answers a missing cowbell
303
+ with a detuned tom, confidently. A slot the kit does not define plays nothing,
304
+ which is what real hardware does; TR-808 genuinely leaves three of GM's slots
305
+ empty. `percussionPackName` addresses a kit by the program that selects it,
306
+ and goes through `gmKitAt`, because a percussion track's `midiProgram` is a
307
+ kit and never agrees with the instrument table (Brush is 40; program 40 is
308
+ Violin).
309
+ - **`percussionBase` has no default, deliberately.** The melodic packs have a
310
+ CDN to fall back on; the kits are app-hosted, so there is nowhere to point.
311
+ An app that ships scores with drums and never sets it gets a sentence naming
312
+ the build script rather than silent percussion.
313
+ - **An export is rendered offline, not played into a file.** The engine
314
+ schedules against a *live* clock and pumps 200ms at a time, so reusing it for
315
+ export would make a render take as long as the piece. `RenderPlan` arrives
316
+ fully resolved (seconds, not ticks; mute and solo already applied by
317
+ `renderEvents` in music_lib), so `rn/audio/offline-render.ts` schedules the
318
+ whole thing up front against an `OfflineAudioContext`. What it *does* share
319
+ with the engine is `PackLibrary` and `planVoice` — the sample choice and the
320
+ gain — which is what keeps the file a recording of what was heard rather than
321
+ a second voicing that drifts. Note the velocity units differ: `RenderPlan`
322
+ carries 0..1 and `planVoice` normalizes 0..127, so the renderer scales back up.
323
+ Getting that backwards renders a file ~127x too quiet, which is why it has its
324
+ own test.
325
+ ## Related Projects
326
+
327
+ - `music_types` — the model, the score domain, the playhead (`@sudobility/music_types`)
328
+ - `music_codecs` — score file formats
329
+ - `music_io` — files, audio encoding, XML, MIDI input
330
+ - `music_lib` — editing; binds the store to this through its playback adapter
331
+ - `music_app` — the web UI
332
+
333
+ ## Git Workflow
334
+
335
+ - Do not use feature branches for code changes. Always stay on the current branch.
package/dist/core.d.ts ADDED
@@ -0,0 +1,33 @@
1
+ /**
2
+ * The platform-free half: the interface and the singleton, and nothing that
3
+ * makes a sound.
4
+ *
5
+ * Its own entry point so a package that only *drives* playback does not pull an
6
+ * audio engine in with it. `music_lib` is exactly that case — it holds an
7
+ * `IMusicPlayer` and calls it, and its own guard test asserts it declares no
8
+ * platform dependency. Importing this package's root would resolve to the web
9
+ * entry and drag in `AudioWorklet` and `js-synthesizer`, which would make that
10
+ * assertion false.
11
+ *
12
+ * Same reasoning as `@sudobility/music_player/rn/expression-table`: a subpath
13
+ * exists so a consumer can reach one dependency-free thing without the peers
14
+ * that surround it.
15
+ */
16
+ export * from './types.js';
17
+ /** The engine contract, for a platform implementing one. */
18
+ export * from './engine.js';
19
+ export * from './singleton.js';
20
+ /**
21
+ * Plan building is here too: it is pure score maths over `@sudobility/music_types`
22
+ * and touches no engine, so a consumer that wants to know what *would* sound —
23
+ * music_lib's articulation tests, music_api if it ever needs to — can have it
24
+ * without an audio context.
25
+ */
26
+ export { playbackPlan, playbackTracks, resolveVoice } from './shared/plan.js';
27
+ export { renderEvents } from './shared/render-events.js';
28
+ /**
29
+ * The bus itself, for hosts that bind React to it and for their tests, which
30
+ * construct one and publish into it. Platform-free: three listener sets.
31
+ */
32
+ export { PlaybackBus } from './shared/bus.js';
33
+ //# sourceMappingURL=core.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"core.d.ts","sourceRoot":"","sources":["../src/core.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,cAAc,YAAY,CAAC;AAC3B,4DAA4D;AAC5D,cAAc,aAAa,CAAC;AAC5B,cAAc,gBAAgB,CAAC;AAE/B;;;;;GAKG;AACH,OAAO,EAAE,YAAY,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAC9E,OAAO,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AAEzD;;;GAGG;AACH,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC"}
package/dist/core.js ADDED
@@ -0,0 +1,33 @@
1
+ /**
2
+ * The platform-free half: the interface and the singleton, and nothing that
3
+ * makes a sound.
4
+ *
5
+ * Its own entry point so a package that only *drives* playback does not pull an
6
+ * audio engine in with it. `music_lib` is exactly that case — it holds an
7
+ * `IMusicPlayer` and calls it, and its own guard test asserts it declares no
8
+ * platform dependency. Importing this package's root would resolve to the web
9
+ * entry and drag in `AudioWorklet` and `js-synthesizer`, which would make that
10
+ * assertion false.
11
+ *
12
+ * Same reasoning as `@sudobility/music_player/rn/expression-table`: a subpath
13
+ * exists so a consumer can reach one dependency-free thing without the peers
14
+ * that surround it.
15
+ */
16
+ export * from './types.js';
17
+ /** The engine contract, for a platform implementing one. */
18
+ export * from './engine.js';
19
+ export * from './singleton.js';
20
+ /**
21
+ * Plan building is here too: it is pure score maths over `@sudobility/music_types`
22
+ * and touches no engine, so a consumer that wants to know what *would* sound —
23
+ * music_lib's articulation tests, music_api if it ever needs to — can have it
24
+ * without an audio context.
25
+ */
26
+ export { playbackPlan, playbackTracks, resolveVoice } from './shared/plan.js';
27
+ export { renderEvents } from './shared/render-events.js';
28
+ /**
29
+ * The bus itself, for hosts that bind React to it and for their tests, which
30
+ * construct one and publish into it. Platform-free: three listener sets.
31
+ */
32
+ export { PlaybackBus } from './shared/bus.js';
33
+ //# sourceMappingURL=core.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"core.js","sourceRoot":"","sources":["../src/core.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,cAAc,YAAY,CAAC;AAC3B,4DAA4D;AAC5D,cAAc,aAAa,CAAC;AAC5B,cAAc,gBAAgB,CAAC;AAE/B;;;;;GAKG;AACH,OAAO,EAAE,YAAY,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAC9E,OAAO,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AAEzD;;;GAGG;AACH,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC"}
@@ -0,0 +1,103 @@
1
+ /**
2
+ * The engine contract: what a platform must provide for sound to come out.
3
+ *
4
+ * Here rather than in `@sudobility/music_types` because this package is the
5
+ * only thing that implements it — the web's fluidsynth engine and React
6
+ * Native's sample engine — and the only thing that calls it. The *plan* those
7
+ * engines consume stays in music_types, and must: `PlaybackPlan` composes
8
+ * `PerformanceTimeline`, which `performanceTimeline()` produces there.
9
+ *
10
+ * So the line is: the plan model is score domain, the engine contract is
11
+ * playback.
12
+ */
13
+ import type { PlaybackLoadState, PlaybackPlan, PlaybackTrack, ScoreRange, SoundingNote, TransportPlaybackState } from '@sudobility/music_types';
14
+ /**
15
+ * A voice to audition, resolved from the GM tables **by the caller**.
16
+ *
17
+ * The engine cannot resolve it: a percussion voice's program addresses a drum
18
+ * kit, and only the GM tables know which kit an arbitrary address falls in.
19
+ * Resolving here is what lets the platform layer keep no catalogue of its own.
20
+ */
21
+ export type AuditionVoice = {
22
+ /** The kit's program on a percussion voice, the instrument's own otherwise. */
23
+ program: number;
24
+ /** The GM catalogue name for `program`. */
25
+ name: string;
26
+ isPercussion: boolean;
27
+ };
28
+ export type PlaybackObserver = {
29
+ onPositionTick(tick: number): void;
30
+ /**
31
+ * The notes currently sounding, whenever that set changes.
32
+ *
33
+ * Resolved rather than bare ids. The scheduler already knows each note's
34
+ * track and pitch — it scheduled them — and emitting only the id forced every
35
+ * consumer to search the whole score to get it back: `playingPitchesForTrack`
36
+ * was an O(score) scan per sounding note, twenty times a second.
37
+ */
38
+ onActiveNotes(notes: SoundingNote[]): void;
39
+ onStateChange(state: TransportPlaybackState): void;
40
+ /**
41
+ * How far along the engine is in becoming able to make a sound.
42
+ *
43
+ * Optional because not every engine has anything to load. The soundfont
44
+ * engine does: tens of megabytes to fetch and several seconds for the synth
45
+ * to digest, all on the first press of Play. Without this the transport
46
+ * simply looks broken for that whole time — which is exactly how it read
47
+ * before there was anywhere to report it.
48
+ */
49
+ onLoadStateChange?(state: PlaybackLoadState): void;
50
+ };
51
+ export interface PlaybackEngine {
52
+ initialize(): Promise<void>;
53
+ /** Adopts a plan. The engine is handed music, never a score. */
54
+ load(plan: PlaybackPlan): Promise<void>;
55
+ play(fromTick?: number): Promise<void>;
56
+ pause(): void;
57
+ stop(): void;
58
+ seek(tick: number): void;
59
+ setTempoMultiplier(multiplier: number): void;
60
+ setLoop(range: ScoreRange | null): void;
61
+ setTrackMute(trackId: string, muted: boolean): void;
62
+ setTrackSolo(trackId: string, solo: boolean): void;
63
+ /**
64
+ * Re-reads every track's volume, pan, mute and solo and pushes them,
65
+ * touching nothing that is scheduled.
66
+ *
67
+ * The mixing counterpart to `load`. Mute and solo had setters; volume and pan
68
+ * did not, and a track's volume was only ever read at load time — so once a
69
+ * mix change stopped reloading the score (the playback edit lock, see
70
+ * `music_lib`'s `score-slice`), moving a fader during playback moved the
71
+ * fader and not the sound.
72
+ *
73
+ * Takes only the tracks, so changing a gain does not rebuild every note —
74
+ * which is the whole reason this is separate from `load`. Idempotent, and
75
+ * takes them all rather than one property: the caller says "the mix changed,
76
+ * here it is" and does not work out which property it was.
77
+ */
78
+ applyMix(tracks: readonly PlaybackTrack[]): void;
79
+ /** Toggles the metronome click. */
80
+ setMetronome(enabled: boolean): void;
81
+ /** Sets overall output level, 0-1 linear gain. */
82
+ setMasterVolume(volume: number): void;
83
+ /**
84
+ * Sounds `midi` immediately on `program`'s voice and holds it, independent of
85
+ * the transport — for auditioning a key while editing.
86
+ *
87
+ * `voice.isPercussion` mirrors what playback does with a percussion-clef
88
+ * track: without it, tapping a note on a drum track auditioned a pitched
89
+ * instrument while the same note played back as a drum.
90
+ *
91
+ * Separate from `play` because the two answer different questions: `play`
92
+ * renders the written score along a timeline, this makes a sound *now*, for
93
+ * as long as the caller holds it, whether or not a score is even loaded.
94
+ * Implementations must not disturb transport state.
95
+ */
96
+ noteOn(midi: number, voice: AuditionVoice): void;
97
+ /** Releases a pitch started by `noteOn`. Silent no-op if it is not sounding. */
98
+ noteOff(midi: number): void;
99
+ /** Registers (or, with `null`, clears) the single observer receiving position/active-note/state updates. */
100
+ setObserver(observer: PlaybackObserver | null): void;
101
+ dispose(): void;
102
+ }
103
+ //# sourceMappingURL=engine.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,KAAK,EACV,iBAAiB,EACjB,YAAY,EACZ,aAAa,EACb,UAAU,EACV,YAAY,EACZ,sBAAsB,EACvB,MAAM,yBAAyB,CAAC;AAEjC;;;;;;GAMG;AACH,MAAM,MAAM,aAAa,GAAG;IAC1B,+EAA+E;IAC/E,OAAO,EAAE,MAAM,CAAC;IAChB,2CAA2C;IAC3C,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,OAAO,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACnC;;;;;;;OAOG;IACH,aAAa,CAAC,KAAK,EAAE,YAAY,EAAE,GAAG,IAAI,CAAC;IAC3C,aAAa,CAAC,KAAK,EAAE,sBAAsB,GAAG,IAAI,CAAC;IACnD;;;;;;;;OAQG;IACH,iBAAiB,CAAC,CAAC,KAAK,EAAE,iBAAiB,GAAG,IAAI,CAAC;CACpD,CAAC;AAEF,MAAM,WAAW,cAAc;IAC7B,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5B,gEAAgE;IAChE,IAAI,CAAC,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACxC,IAAI,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACvC,KAAK,IAAI,IAAI,CAAC;IACd,IAAI,IAAI,IAAI,CAAC;IACb,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,kBAAkB,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7C,OAAO,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI,GAAG,IAAI,CAAC;IACxC,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI,CAAC;IACpD,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,IAAI,CAAC;IACnD;;;;;;;;;;;;;;OAcG;IACH,QAAQ,CAAC,MAAM,EAAE,SAAS,aAAa,EAAE,GAAG,IAAI,CAAC;IACjD,mCAAmC;IACnC,YAAY,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAAC;IACrC,kDAAkD;IAClD,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtC;;;;;;;;;;;;OAYG;IACH,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,aAAa,GAAG,IAAI,CAAC;IACjD,gFAAgF;IAChF,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,4GAA4G;IAC5G,WAAW,CAAC,QAAQ,EAAE,gBAAgB,GAAG,IAAI,GAAG,IAAI,CAAC;IACrD,OAAO,IAAI,IAAI,CAAC;CACjB"}
package/dist/engine.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=engine.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"engine.js","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":""}
@@ -0,0 +1,46 @@
1
+ /**
2
+ * A player that records what it was asked to do and makes no sound.
3
+ *
4
+ * Shipped from this package rather than rewritten in each consumer, so
5
+ * music_lib's and music_app's tests exercise the same double this package's own
6
+ * suite does.
7
+ */
8
+ import type { PlaybackLoadState, PlaybackTrack, Score, ScoreRange, SoundingNote, TransportPlaybackState } from '@sudobility/music_types';
9
+ import { PlaybackBus } from '../shared/bus.js';
10
+ import type { IMusicPlayer, Unsubscribe } from '../types.js';
11
+ export declare class MockMusicPlayer implements IMusicPlayer {
12
+ readonly calls: string[];
13
+ /** The score it was last handed, for tests to assert on. */
14
+ loadedScore: Score | null;
15
+ private readonly position;
16
+ private readonly soundingListeners;
17
+ private readonly transport;
18
+ private readonly loadState;
19
+ /** A real bus, so a consumer's tests can publish into it exactly as the engine would. */
20
+ readonly bus: PlaybackBus;
21
+ load(score: Score, opts?: {
22
+ visibleTrackIds?: readonly string[];
23
+ }): Promise<void>;
24
+ play(): Promise<void>;
25
+ pause(): void;
26
+ stop(): void;
27
+ seek(tick: number): void;
28
+ setLoop(range: ScoreRange | null): void;
29
+ setTempoMultiplier(m: number): void;
30
+ setMetronome(on: boolean): void;
31
+ setMasterVolume(v: number): void;
32
+ applyMix(tracks: readonly PlaybackTrack[]): void;
33
+ setVisibleTracks(ids: readonly string[]): void;
34
+ noteOn(midi: number, program: number, isPercussion?: boolean): void;
35
+ noteOff(midi: number): void;
36
+ onPosition(fn: (t: number) => void): Unsubscribe;
37
+ onSounding(fn: (n: readonly SoundingNote[]) => void): Unsubscribe;
38
+ onTransport(fn: (s: TransportPlaybackState) => void): Unsubscribe;
39
+ onLoadState(fn: (s: PlaybackLoadState) => void): Unsubscribe;
40
+ dispose(): void;
41
+ emitPosition(tick: number): void;
42
+ emitSounding(notes: readonly SoundingNote[]): void;
43
+ emitTransport(state: TransportPlaybackState): void;
44
+ emitLoadState(state: PlaybackLoadState): void;
45
+ }
46
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/mocks/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,KAAK,EACV,iBAAiB,EACjB,aAAa,EACb,KAAK,EACL,UAAU,EACV,YAAY,EACZ,sBAAsB,EACvB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAC/C,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE7D,qBAAa,eAAgB,YAAW,YAAY;IAClD,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,CAAM;IAC9B,4DAA4D;IAC5D,WAAW,EAAE,KAAK,GAAG,IAAI,CAAQ;IAEjC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAkC;IAC3D,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAE9B;IACJ,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAkD;IAC5E,OAAO,CAAC,QAAQ,CAAC,SAAS,CAA6C;IAEvE,yFAAyF;IACzF,QAAQ,CAAC,GAAG,cAAqB;IAE3B,IAAI,CACR,KAAK,EAAE,KAAK,EACZ,IAAI,GAAE;QAAE,eAAe,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;KAAO,GACjD,OAAO,CAAC,IAAI,CAAC;IASV,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAG3B,KAAK,IAAI,IAAI;IAGb,IAAI,IAAI,IAAI;IAGZ,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAGxB,OAAO,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI,GAAG,IAAI;IAKvC,kBAAkB,CAAC,CAAC,EAAE,MAAM,GAAG,IAAI;IAGnC,YAAY,CAAC,EAAE,EAAE,OAAO,GAAG,IAAI;IAG/B,eAAe,CAAC,CAAC,EAAE,MAAM,GAAG,IAAI;IAGhC,QAAQ,CAAC,MAAM,EAAE,SAAS,aAAa,EAAE,GAAG,IAAI;IAGhD,gBAAgB,CAAC,GAAG,EAAE,SAAS,MAAM,EAAE,GAAG,IAAI;IAG9C,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,YAAY,UAAQ,GAAG,IAAI;IAGjE,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAI3B,UAAU,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,IAAI,GAAG,WAAW;IAIhD,UAAU,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,SAAS,YAAY,EAAE,KAAK,IAAI,GAAG,WAAW;IAIjE,WAAW,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,sBAAsB,KAAK,IAAI,GAAG,WAAW;IAIjE,WAAW,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,iBAAiB,KAAK,IAAI,GAAG,WAAW;IAK5D,OAAO,IAAI,IAAI;IAKf,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAIhC,YAAY,CAAC,KAAK,EAAE,SAAS,YAAY,EAAE,GAAG,IAAI;IAIlD,aAAa,CAAC,KAAK,EAAE,sBAAsB,GAAG,IAAI;IAGlD,aAAa,CAAC,KAAK,EAAE,iBAAiB,GAAG,IAAI;CAG9C"}