@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
@@ -0,0 +1,38 @@
1
+ /**
2
+ * What the pump does with the notes it drained, as pure logic.
3
+ *
4
+ * Kept apart from the posting so the rule that matters can be tested without
5
+ * an audio device: a note whose moment has already passed by more than the
6
+ * grace window is skipped rather than fired late.
7
+ *
8
+ * That rule is the one deliberate exception to "no note is ever dropped", and
9
+ * it earns its place. Without it, a main-thread stall turns into a backlog
10
+ * that arrives in a single burst — every note that should have been spread
11
+ * over the stall landing at once. Late notes are worse than absent ones: the
12
+ * absent one is a gap, the burst is a garble across every part at once.
13
+ *
14
+ * A note only slightly late is still played, because the ear will not notice
15
+ * and dropping it would be gratuitous.
16
+ *
17
+ * **The React Native sample engine is now its only consumer.** The web engine
18
+ * hands a four-second horizon of self-releasing notes to the fluidsynth
19
+ * sequencer instead, so nothing there is ever dispatched late enough to need
20
+ * skipping — see `soundfont-engine.ts`'s `HORIZON_SECONDS`. RN has no sequencer
21
+ * to hand a horizon to and keeps this model.
22
+ */
23
+ import type { PlaybackNote as ScheduledNote } from '@sudobility/music_types';
24
+ export type Dispatch = {
25
+ note: ScheduledNote;
26
+ atSeconds: number;
27
+ };
28
+ export declare function planDispatch({ notes, secondsForTick, positionSeconds, graceSeconds, }: {
29
+ notes: readonly ScheduledNote[];
30
+ /** Tick to playback seconds — the engine's, since it owns the tempo map and speed. */
31
+ secondsForTick: (tick: number) => number;
32
+ positionSeconds: number;
33
+ graceSeconds: number;
34
+ }): {
35
+ due: Dispatch[];
36
+ skipped: ScheduledNote[];
37
+ };
38
+ //# sourceMappingURL=pump-window.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pump-window.d.ts","sourceRoot":"","sources":["../../src/shared/pump-window.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,OAAO,KAAK,EAAE,YAAY,IAAI,aAAa,EAAE,MAAM,yBAAyB,CAAC;AAE7E,MAAM,MAAM,QAAQ,GAAG;IAAE,IAAI,EAAE,aAAa,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAAC;AAElE,wBAAgB,YAAY,CAAC,EAC3B,KAAK,EACL,cAAc,EACd,eAAe,EACf,YAAY,GACb,EAAE;IACD,KAAK,EAAE,SAAS,aAAa,EAAE,CAAC;IAChC,sFAAsF;IACtF,cAAc,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;IACzC,eAAe,EAAE,MAAM,CAAC;IACxB,YAAY,EAAE,MAAM,CAAC;CACtB,GAAG;IAAE,GAAG,EAAE,QAAQ,EAAE,CAAC;IAAC,OAAO,EAAE,aAAa,EAAE,CAAA;CAAE,CAShD"}
@@ -0,0 +1,13 @@
1
+ export function planDispatch({ notes, secondsForTick, positionSeconds, graceSeconds, }) {
2
+ const due = [];
3
+ const skipped = [];
4
+ for (const note of notes) {
5
+ const atSeconds = secondsForTick(note.tick);
6
+ if (positionSeconds - atSeconds > graceSeconds)
7
+ skipped.push(note);
8
+ else
9
+ due.push({ note, atSeconds });
10
+ }
11
+ return { due, skipped };
12
+ }
13
+ //# sourceMappingURL=pump-window.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pump-window.js","sourceRoot":"","sources":["../../src/shared/pump-window.ts"],"names":[],"mappings":"AA0BA,MAAM,UAAU,YAAY,CAAC,EAC3B,KAAK,EACL,cAAc,EACd,eAAe,EACf,YAAY,GAOb;IACC,MAAM,GAAG,GAAe,EAAE,CAAC;IAC3B,MAAM,OAAO,GAAoB,EAAE,CAAC;IACpC,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,SAAS,GAAG,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC5C,IAAI,eAAe,GAAG,SAAS,GAAG,YAAY;YAAE,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;;YAC9D,GAAG,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC,CAAC;IACrC,CAAC;IACD,OAAO,EAAE,GAAG,EAAE,OAAO,EAAE,CAAC;AAC1B,CAAC"}
@@ -0,0 +1,25 @@
1
+ import type { Score } from '@sudobility/music_types';
2
+ import type { RenderPlan } from '@sudobility/music_types';
3
+ export type { RenderEvent, RenderPlan, RenderTrack, } from '@sudobility/music_types';
4
+ /**
5
+ * The plan an export should sound, matching live playback.
6
+ *
7
+ * **Mute and solo are respected**, because they are part of how the score
8
+ * currently sounds, and an export that ignored them would not match what you
9
+ * just heard. Solo wins: if anything is soloed, only soloed tracks sound.
10
+ *
11
+ * Every track appears in `tracks` regardless — silent, muted, soloed away —
12
+ * because the renderer sizes its mix headroom by how many channels exist, as
13
+ * playback does. Only a track's *notes* are dropped when it is silenced.
14
+ *
15
+ * `isPercussion` comes from the clef and not the MIDI channel, and the voice is
16
+ * left for the renderer to resolve from `midiProgram`/`instrumentName`: both
17
+ * are the signals live playback uses, and the export sounding different from
18
+ * the thing it is an export *of* is the bug being fixed here.
19
+ *
20
+ * That parity is now structural: the notes come from `flattenScoreNotes`, the
21
+ * same traversal `playbackPlan` uses. Walking the score separately is what let
22
+ * the two drift over ties.
23
+ */
24
+ export declare function renderEvents(score: Score): RenderPlan;
25
+ //# sourceMappingURL=render-events.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"render-events.d.ts","sourceRoot":"","sources":["../../src/shared/render-events.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,yBAAyB,CAAC;AAErD,OAAO,KAAK,EAEV,UAAU,EAEX,MAAM,yBAAyB,CAAC;AACjC,YAAY,EACV,WAAW,EACX,UAAU,EACV,WAAW,GACZ,MAAM,yBAAyB,CAAC;AAKjC;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,KAAK,GAAG,UAAU,CAsDrD"}
@@ -0,0 +1,79 @@
1
+ /**
2
+ * A score flattened into timed events, ready for an offline audio render.
3
+ *
4
+ * Deliberately score-in, events-out: the actual rendering needs Tone.js and an
5
+ * audio context, so it lives in music_io. Everything *musical* about an export
6
+ * — which tracks sound, when each note starts, how long the file has to be —
7
+ * is decided here, where it can be tested without a browser.
8
+ */
9
+ import { TempoMap } from '@sudobility/music_types';
10
+ import { fermataTempoMap } from '@sudobility/music_types';
11
+ import { flattenScoreNotes } from '@sudobility/music_types';
12
+ import { resolveVoice } from './plan.js';
13
+ /** Release tail, so the last note is not cut off mid-decay. */
14
+ const TAIL_SEC = 1;
15
+ /**
16
+ * The plan an export should sound, matching live playback.
17
+ *
18
+ * **Mute and solo are respected**, because they are part of how the score
19
+ * currently sounds, and an export that ignored them would not match what you
20
+ * just heard. Solo wins: if anything is soloed, only soloed tracks sound.
21
+ *
22
+ * Every track appears in `tracks` regardless — silent, muted, soloed away —
23
+ * because the renderer sizes its mix headroom by how many channels exist, as
24
+ * playback does. Only a track's *notes* are dropped when it is silenced.
25
+ *
26
+ * `isPercussion` comes from the clef and not the MIDI channel, and the voice is
27
+ * left for the renderer to resolve from `midiProgram`/`instrumentName`: both
28
+ * are the signals live playback uses, and the export sounding different from
29
+ * the thing it is an export *of* is the bug being fixed here.
30
+ *
31
+ * That parity is now structural: the notes come from `flattenScoreNotes`, the
32
+ * same traversal `playbackPlan` uses. Walking the score separately is what let
33
+ * the two drift over ties.
34
+ */
35
+ export function renderEvents(score) {
36
+ // The same derived map live playback uses, so an exported file holds its
37
+ // pauses for exactly as long as the transport just did.
38
+ const tempoMap = new TempoMap(fermataTempoMap(score), score.ppq);
39
+ const anySolo = score.tracks.some(t => t.solo);
40
+ const tracks = score.tracks.map(track => {
41
+ const isPercussion = track.clef === 'percussion';
42
+ // The renderer picks its sample pack from this, and a percussion track's
43
+ // midiProgram addresses a kit rather than an instrument.
44
+ const voice = resolveVoice(track.midiProgram, isPercussion, track.instrumentName);
45
+ return {
46
+ id: track.id,
47
+ midiProgram: track.midiProgram,
48
+ instrumentName: track.instrumentName,
49
+ isPercussion,
50
+ volume: track.volume,
51
+ pan: track.pan,
52
+ voiceProgram: voice.program,
53
+ voiceName: voice.name,
54
+ };
55
+ });
56
+ // The same traversal live playback uses, so ties are joined here too. They
57
+ // were not, and a tie re-articulated in the exported file while sustaining
58
+ // on screen — measured: one note of 960 ticks live, two of 0.5s offline.
59
+ const silenced = new Set(score.tracks.filter(t => (anySolo ? !t.solo : t.muted)).map(t => t.id));
60
+ const events = [];
61
+ for (const note of flattenScoreNotes(score)) {
62
+ if (silenced.has(note.trackId))
63
+ continue;
64
+ const startSec = tempoMap.ticksToSeconds(note.tick);
65
+ const endSec = tempoMap.ticksToSeconds(note.tick + note.durTicks);
66
+ events.push({
67
+ trackId: note.trackId,
68
+ midi: note.midi,
69
+ startSec,
70
+ // Never zero: a rounding error should not silence a note.
71
+ durationSec: Math.max(0.01, endSec - startSec),
72
+ velocity: note.velocity / 127,
73
+ });
74
+ }
75
+ events.sort((a, b) => a.startSec - b.startSec);
76
+ const lastEnd = events.reduce((max, e) => Math.max(max, e.startSec + e.durationSec), 0);
77
+ return { tracks, events, durationSec: lastEnd + TAIL_SEC };
78
+ }
79
+ //# sourceMappingURL=render-events.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"render-events.js","sourceRoot":"","sources":["../../src/shared/render-events.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Which notes are sounding at a given moment, by two monotonic cursors.
3
+ *
4
+ * Replaces deriving the set from a list of pending note-offs the pump walked
5
+ * every tick — a list that no longer exists, because releases happen in the
6
+ * worklet now. One cursor over notes by start, one over the same notes by end;
7
+ * both only ever move forward, so a step costs the number of notes that
8
+ * actually started or stopped rather than the size of the score.
9
+ *
10
+ * `advanceTo` returns `null` when nothing changed, which is the point. The
11
+ * engine used to hand its observer a freshly-allocated array twenty times a
12
+ * second whether or not a single note had begun or ended, waking every
13
+ * consumer downstream — notation colour, the piano keyboard's key lights — for
14
+ * nothing.
15
+ */
16
+ import type { SoundingNote } from '@sudobility/music_types';
17
+ import type { PlaybackNote as ScheduledNote } from '@sudobility/music_types';
18
+ export declare class SoundingSet {
19
+ private byStart;
20
+ private byEnd;
21
+ private startCursor;
22
+ private endCursor;
23
+ /**
24
+ * Keyed by note id so an end can remove the right entry, valued by the
25
+ * resolved note so a consumer never has to search the score for the track and
26
+ * pitch the scheduler already knew.
27
+ */
28
+ private sounding;
29
+ /** Forces the next `advanceTo` to emit, so a reset is always observable. */
30
+ private dirty;
31
+ load(notes: readonly ScheduledNote[], secondsForTick: (tick: number) => number): void;
32
+ /**
33
+ * Drops everything sounding and moves both cursors to `positionSeconds`.
34
+ *
35
+ * A note spanning that moment is deliberately *not* restored: the engine
36
+ * silences the synth on every seek and stop, so nothing is audible there
37
+ * either. Highlighting a note that is not sounding would be a lie.
38
+ */
39
+ reset(positionSeconds: number): void;
40
+ /** The ids sounding at `positionSeconds`, or `null` if they are what they were. */
41
+ advanceTo(positionSeconds: number): SoundingNote[] | null;
42
+ /** How many notes are sounding. */
43
+ get size(): number;
44
+ }
45
+ //# sourceMappingURL=sounding-set.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sounding-set.d.ts","sourceRoot":"","sources":["../../src/shared/sounding-set.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAC5D,OAAO,KAAK,EAAE,YAAY,IAAI,aAAa,EAAE,MAAM,yBAAyB,CAAC;AAE7E,qBAAa,WAAW;IACtB,OAAO,CAAC,OAAO,CAA2D;IAC1E,OAAO,CAAC,KAAK,CAAqD;IAClE,OAAO,CAAC,WAAW,CAAK;IACxB,OAAO,CAAC,SAAS,CAAK;IACtB;;;;OAIG;IACH,OAAO,CAAC,QAAQ,CAAmC;IACnD,4EAA4E;IAC5E,OAAO,CAAC,KAAK,CAAQ;IAErB,IAAI,CACF,KAAK,EAAE,SAAS,aAAa,EAAE,EAC/B,cAAc,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,GACvC,IAAI;IAgBP;;;;;;OAMG;IACH,KAAK,CAAC,eAAe,EAAE,MAAM,GAAG,IAAI;IAWpC,mFAAmF;IACnF,SAAS,CAAC,eAAe,EAAE,MAAM,GAAG,YAAY,EAAE,GAAG,IAAI;IA2BzD,mCAAmC;IACnC,IAAI,IAAI,IAAI,MAAM,CAEjB;CACF"}
@@ -0,0 +1,81 @@
1
+ export class SoundingSet {
2
+ byStart = [];
3
+ byEnd = [];
4
+ startCursor = 0;
5
+ endCursor = 0;
6
+ /**
7
+ * Keyed by note id so an end can remove the right entry, valued by the
8
+ * resolved note so a consumer never has to search the score for the track and
9
+ * pitch the scheduler already knew.
10
+ */
11
+ sounding = new Map();
12
+ /** Forces the next `advanceTo` to emit, so a reset is always observable. */
13
+ dirty = true;
14
+ load(notes, secondsForTick) {
15
+ this.byStart = notes
16
+ .map(n => ({
17
+ note: { noteId: n.noteId, trackId: n.trackId, midi: n.midi },
18
+ startSeconds: secondsForTick(n.tick),
19
+ }))
20
+ .sort((a, b) => a.startSeconds - b.startSeconds);
21
+ this.byEnd = notes
22
+ .map(n => ({
23
+ noteId: n.noteId,
24
+ endSeconds: secondsForTick(n.tick + n.durTicks),
25
+ }))
26
+ .sort((a, b) => a.endSeconds - b.endSeconds);
27
+ this.reset(0);
28
+ }
29
+ /**
30
+ * Drops everything sounding and moves both cursors to `positionSeconds`.
31
+ *
32
+ * A note spanning that moment is deliberately *not* restored: the engine
33
+ * silences the synth on every seek and stop, so nothing is audible there
34
+ * either. Highlighting a note that is not sounding would be a lie.
35
+ */
36
+ reset(positionSeconds) {
37
+ this.sounding.clear();
38
+ this.startCursor = lowerBound(this.byStart, positionSeconds, e => e.startSeconds);
39
+ this.endCursor = lowerBound(this.byEnd, positionSeconds, e => e.endSeconds);
40
+ this.dirty = true;
41
+ }
42
+ /** The ids sounding at `positionSeconds`, or `null` if they are what they were. */
43
+ advanceTo(positionSeconds) {
44
+ let changed = this.dirty;
45
+ this.dirty = false;
46
+ while (this.startCursor < this.byStart.length &&
47
+ this.byStart[this.startCursor].startSeconds <= positionSeconds) {
48
+ const { note } = this.byStart[this.startCursor];
49
+ this.sounding.set(note.noteId, note);
50
+ this.startCursor += 1;
51
+ changed = true;
52
+ }
53
+ while (this.endCursor < this.byEnd.length &&
54
+ this.byEnd[this.endCursor].endSeconds <= positionSeconds) {
55
+ // Deleting an id that was never added — its start was skipped by a reset
56
+ // — is a no-op, which is what makes seeking into a held note safe.
57
+ if (this.sounding.delete(this.byEnd[this.endCursor].noteId))
58
+ changed = true;
59
+ this.endCursor += 1;
60
+ }
61
+ return changed ? [...this.sounding.values()] : null;
62
+ }
63
+ /** How many notes are sounding. */
64
+ get size() {
65
+ return this.sounding.size;
66
+ }
67
+ }
68
+ /** First index whose key is >= `value`. */
69
+ function lowerBound(items, value, key) {
70
+ let lo = 0;
71
+ let hi = items.length;
72
+ while (lo < hi) {
73
+ const mid = (lo + hi) >> 1;
74
+ if (key(items[mid]) < value)
75
+ lo = mid + 1;
76
+ else
77
+ hi = mid;
78
+ }
79
+ return lo;
80
+ }
81
+ //# sourceMappingURL=sounding-set.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sounding-set.js","sourceRoot":"","sources":["../../src/shared/sounding-set.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Hand-built `PlaybackPlan`s for engine tests.
3
+ *
4
+ * The engines take a plan, not a score, so their tests build one directly
5
+ * rather than importing a score fixture and a converter from music_lib —
6
+ * which is what kept the platform layer depending on the domain. A plan is
7
+ * plain data, so this is also clearer: a test that needs two notes says so
8
+ * instead of asserting against whatever a fixture happens to contain.
9
+ *
10
+ * The default tempo is linear: 120 BPM at ppq 480 is 960 ticks per second, so
11
+ * `ticksToSeconds` is `tick / 960` and the arithmetic in assertions stays
12
+ * readable.
13
+ */
14
+ import type { PlaybackNote, PlaybackPlan, PlaybackTrack, TempoConversion } from '@sudobility/music_types';
15
+ export declare const TICKS_PER_SECOND = 960;
16
+ export declare const LINEAR_TEMPO: TempoConversion;
17
+ export declare function testTrack(over: Partial<PlaybackTrack> & {
18
+ id: string;
19
+ }): PlaybackTrack;
20
+ export declare function testNote(over: Partial<PlaybackNote> & {
21
+ trackId: string;
22
+ }): PlaybackNote;
23
+ export declare function testPlan(over?: Partial<PlaybackPlan>): PlaybackPlan;
24
+ /** `count` pitched tracks numbered `t0..`, each on its own GM program. */
25
+ export declare function planOfTracks(count: number, isPercussion?: boolean): PlaybackPlan;
26
+ /**
27
+ * Two tracks over four bars of 4/4 — a quarter note per beat, alternating.
28
+ *
29
+ * The length is load-bearing at both ends. Long enough that the transport tests,
30
+ * which advance the clock by seconds and assert on scheduling several seconds
31
+ * ahead, still have music left to schedule; short enough that the sounding-note
32
+ * test, which steps ten seconds looking for the set to empty, sees it end. At
33
+ * 960 ticks per second this runs eight seconds.
34
+ */
35
+ export declare function twoTrackPlan(): PlaybackPlan;
36
+ //# sourceMappingURL=test-plan.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"test-plan.d.ts","sourceRoot":"","sources":["../../src/shared/test-plan.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,KAAK,EACV,YAAY,EACZ,YAAY,EACZ,aAAa,EACb,eAAe,EAChB,MAAM,yBAAyB,CAAC;AAEjC,eAAO,MAAM,gBAAgB,MAAM,CAAC;AAEpC,eAAO,MAAM,YAAY,EAAE,eAG1B,CAAC;AAEF,wBAAgB,SAAS,CACvB,IAAI,EAAE,OAAO,CAAC,aAAa,CAAC,GAAG;IAAE,EAAE,EAAE,MAAM,CAAA;CAAE,GAC5C,aAAa,CAcf;AAED,wBAAgB,QAAQ,CACtB,IAAI,EAAE,OAAO,CAAC,YAAY,CAAC,GAAG;IAAE,OAAO,EAAE,MAAM,CAAA;CAAE,GAChD,YAAY,CAYd;AAED,wBAAgB,QAAQ,CAAC,IAAI,GAAE,OAAO,CAAC,YAAY,CAAM,GAAG,YAAY,CAevE;AAED,0EAA0E;AAC1E,wBAAgB,YAAY,CAC1B,KAAK,EAAE,MAAM,EACb,YAAY,UAAQ,GACnB,YAAY,CAMd;AAED;;;;;;;;GAQG;AACH,wBAAgB,YAAY,IAAI,YAAY,CAoB3C"}
@@ -0,0 +1,83 @@
1
+ export const TICKS_PER_SECOND = 960;
2
+ export const LINEAR_TEMPO = {
3
+ ticksToSeconds: tick => tick / TICKS_PER_SECOND,
4
+ secondsToTicks: seconds => seconds * TICKS_PER_SECOND,
5
+ };
6
+ export function testTrack(over) {
7
+ const midiProgram = over.midiProgram ?? 0;
8
+ return {
9
+ midiProgram,
10
+ instrumentName: 'x',
11
+ isPercussion: false,
12
+ volume: 1,
13
+ pan: 0,
14
+ muted: false,
15
+ solo: false,
16
+ voiceProgram: midiProgram,
17
+ voiceName: 'x',
18
+ ...over,
19
+ };
20
+ }
21
+ export function testNote(over) {
22
+ return {
23
+ tick: 0,
24
+ durTicks: 480,
25
+ midi: 60,
26
+ // MIDI's 0-127, which is the unit `PlaybackNote` carries — `RenderPlan` is
27
+ // the one that uses 0..1. This read 0.8 for a while, which both engines
28
+ // treat as the quietest note there is.
29
+ velocity: 96,
30
+ noteId: `n-${over.tick ?? 0}-${over.trackId}`,
31
+ ...over,
32
+ };
33
+ }
34
+ export function testPlan(over = {}) {
35
+ const tracks = over.tracks ?? [testTrack({ id: 't0' })];
36
+ const notes = over.notes ?? [];
37
+ return {
38
+ tracks,
39
+ notes,
40
+ clicks: over.clicks ?? [],
41
+ tempo: over.tempo ?? LINEAR_TEMPO,
42
+ // Identity by default: an engine test is about scheduling, not repeats,
43
+ // and this is what a score with no repeats produces.
44
+ timeline: over.timeline ?? { segments: [], durationTicks: 0 },
45
+ durationTicks: over.durationTicks ??
46
+ notes.reduce((n, note) => Math.max(n, note.tick + note.durTicks), 0),
47
+ };
48
+ }
49
+ /** `count` pitched tracks numbered `t0..`, each on its own GM program. */
50
+ export function planOfTracks(count, isPercussion = false) {
51
+ return testPlan({
52
+ tracks: Array.from({ length: count }, (_, i) => testTrack({ id: `t${i}`, midiProgram: i, isPercussion, voiceProgram: i })),
53
+ });
54
+ }
55
+ /**
56
+ * Two tracks over four bars of 4/4 — a quarter note per beat, alternating.
57
+ *
58
+ * The length is load-bearing at both ends. Long enough that the transport tests,
59
+ * which advance the clock by seconds and assert on scheduling several seconds
60
+ * ahead, still have music left to schedule; short enough that the sounding-note
61
+ * test, which steps ten seconds looking for the set to empty, sees it end. At
62
+ * 960 ticks per second this runs eight seconds.
63
+ */
64
+ export function twoTrackPlan() {
65
+ const BEATS = 16;
66
+ const tracks = [
67
+ testTrack({ id: 't0', midiProgram: 0, voiceProgram: 0 }),
68
+ testTrack({ id: 't1', midiProgram: 1, voiceProgram: 1 }),
69
+ ];
70
+ const notes = Array.from({ length: BEATS }, (_, i) => testNote({
71
+ trackId: i % 2 === 0 ? 't0' : 't1',
72
+ tick: i * 480,
73
+ durTicks: 480,
74
+ midi: 60 + (i % 12),
75
+ noteId: `n${i}`,
76
+ }));
77
+ const clicks = Array.from({ length: BEATS }, (_, i) => ({
78
+ tick: i * 480,
79
+ accent: i % 4 === 0,
80
+ }));
81
+ return testPlan({ tracks, notes, clicks });
82
+ }
83
+ //# sourceMappingURL=test-plan.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"test-plan.js","sourceRoot":"","sources":["../../src/shared/test-plan.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,23 @@
1
+ /**
2
+ * The player singleton.
3
+ *
4
+ * Same shape as the rest of the family's services — initialise once at
5
+ * start-up, read it everywhere, reset it in tests.
6
+ *
7
+ * A singleton because there is one transport: one piece of music is playing,
8
+ * and two players would be two playheads, which is exactly the disagreement
9
+ * `MusicPosition` exists to prevent.
10
+ */
11
+ import type { IMusicPlayer } from './types.js';
12
+ export declare class MusicPlayerNotInitializedError extends Error {
13
+ constructor();
14
+ }
15
+ /**
16
+ * Idempotent, so a re-mounting composition root does not swap the transport out
17
+ * from under everything subscribed to it.
18
+ */
19
+ export declare function initializeMusicPlayer(player: IMusicPlayer): IMusicPlayer;
20
+ export declare function getMusicPlayer(): IMusicPlayer;
21
+ /** Test-only: clears the singleton so suites cannot leak into each other. */
22
+ export declare function resetMusicPlayer(): void;
23
+ //# sourceMappingURL=singleton.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"singleton.d.ts","sourceRoot":"","sources":["../src/singleton.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAI/C,qBAAa,8BAA+B,SAAQ,KAAK;;CAOxD;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,YAAY,GAAG,YAAY,CAGxE;AAED,wBAAgB,cAAc,IAAI,YAAY,CAG7C;AAED,6EAA6E;AAC7E,wBAAgB,gBAAgB,IAAI,IAAI,CAEvC"}
@@ -0,0 +1,26 @@
1
+ let instance = null;
2
+ export class MusicPlayerNotInitializedError extends Error {
3
+ constructor() {
4
+ super('The music player has not been initialized. Call initializeMusicPlayer() from your app composition root before using playback.');
5
+ this.name = 'MusicPlayerNotInitializedError';
6
+ }
7
+ }
8
+ /**
9
+ * Idempotent, so a re-mounting composition root does not swap the transport out
10
+ * from under everything subscribed to it.
11
+ */
12
+ export function initializeMusicPlayer(player) {
13
+ if (!instance)
14
+ instance = player;
15
+ return instance;
16
+ }
17
+ export function getMusicPlayer() {
18
+ if (!instance)
19
+ throw new MusicPlayerNotInitializedError();
20
+ return instance;
21
+ }
22
+ /** Test-only: clears the singleton so suites cannot leak into each other. */
23
+ export function resetMusicPlayer() {
24
+ instance = null;
25
+ }
26
+ //# sourceMappingURL=singleton.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"singleton.js","sourceRoot":"","sources":["../src/singleton.ts"],"names":[],"mappings":"AAYA,IAAI,QAAQ,GAAwB,IAAI,CAAC;AAEzC,MAAM,OAAO,8BAA+B,SAAQ,KAAK;IACvD;QACE,KAAK,CACH,+HAA+H,CAChI,CAAC;QACF,IAAI,CAAC,IAAI,GAAG,gCAAgC,CAAC;IAC/C,CAAC;CACF;AAED;;;GAGG;AACH,MAAM,UAAU,qBAAqB,CAAC,MAAoB;IACxD,IAAI,CAAC,QAAQ;QAAE,QAAQ,GAAG,MAAM,CAAC;IACjC,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,MAAM,UAAU,cAAc;IAC5B,IAAI,CAAC,QAAQ;QAAE,MAAM,IAAI,8BAA8B,EAAE,CAAC;IAC1D,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,6EAA6E;AAC7E,MAAM,UAAU,gBAAgB;IAC9B,QAAQ,GAAG,IAAI,CAAC;AAClB,CAAC"}
@@ -0,0 +1,84 @@
1
+ import type { PlaybackBus } from './shared/bus.js';
2
+ import type { PlaybackLoadState, PlaybackTrack, Score, ScoreRange, SoundingNote, TransportPlaybackState } from '@sudobility/music_types';
3
+ export type Unsubscribe = () => void;
4
+ /**
5
+ * The transport.
6
+ *
7
+ * Takes a `Score`, not a plan: this package owns `playbackPlan`, so making the
8
+ * caller build one first would be a two-step dance whose second step is here.
9
+ * The *engine* underneath still takes a `PlaybackPlan` and does no score maths,
10
+ * which is where music_io's "handed a plan, never a score" rule was always
11
+ * actually about.
12
+ *
13
+ * Deliberately has no caret, and no `togglePlay`, `seekToMeasure`,
14
+ * `previousMeasure`, `nextMeasure` or loop-from-selection. Those read and write
15
+ * the caret or the selection, both of which are editing state; a copy of either
16
+ * here is a second thing that can disagree with the first. See `MusicPosition`
17
+ * in music_types for what that cost last time. They live in music_lib's
18
+ * playback adapter and compose the primitives below.
19
+ */
20
+ export interface IMusicPlayer {
21
+ /**
22
+ * Adopts a score.
23
+ *
24
+ * **While playing, this is a mix change and nothing else** — the note queue is
25
+ * kept. The edit lock is what makes that safe: while the transport runs, the
26
+ * only score change that can reach here is a mix change. A score arriving
27
+ * from outside that path (a generation result, an opened snapshot) must be
28
+ * preceded by `stop()`, or it will be heard as the old score.
29
+ */
30
+ /**
31
+ * Rejects on failure rather than swallowing. The host translates: the message
32
+ * a user sees is a localized string, and this package carries no copy.
33
+ */
34
+ load(score: Score, opts?: {
35
+ visibleTrackIds?: readonly string[];
36
+ }): Promise<void>;
37
+ play(): Promise<void>;
38
+ pause(): void;
39
+ stop(): void;
40
+ /** Seeks to a *written* tick; the first performance of it, if repeats make several. */
41
+ seek(scoreTick: number): void;
42
+ setLoop(range: ScoreRange | null): void;
43
+ setTempoMultiplier(multiplier: number): void;
44
+ setMetronome(enabled: boolean): void;
45
+ setMasterVolume(volume: number): void;
46
+ /** Live, without rebuilding the note queue: mixing is not editing. */
47
+ applyMix(tracks: readonly PlaybackTrack[]): void;
48
+ /** A track sounds only if it is visible *and* not muted. */
49
+ setVisibleTracks(visibleTrackIds: readonly string[]): void;
50
+ /**
51
+ * Sounds a pitch for as long as it is held — auditioning a key while editing.
52
+ *
53
+ * Touches no transport state on purpose: no caret move, no play/pause change,
54
+ * and no appearance in the active-note highlighting that follows the score.
55
+ *
56
+ * `isPercussion` must be passed for a percussion-clef track, or the tap sounds
57
+ * a pitched instrument while the same note plays back as a drum. The program
58
+ * is resolved to a voice here rather than by the caller, because a percussion
59
+ * program addresses a kit and only this package's tables know which.
60
+ */
61
+ noteOn(midi: number, program: number, isPercussion?: boolean): void;
62
+ noteOff(midi: number): void;
63
+ /**
64
+ * The three high-frequency channels, as one object.
65
+ *
66
+ * Exposed rather than wrapped so a host binds React to the same bus the
67
+ * engine publishes into — two would be two answers to "what is sounding".
68
+ * `positionTick` and `sounding` hang off it for a subscriber joining
69
+ * mid-playback, which is why they are held at all.
70
+ */
71
+ readonly bus: PlaybackBus;
72
+ onPosition(fn: (scoreTick: number) => void): Unsubscribe;
73
+ onSounding(fn: (notes: readonly SoundingNote[]) => void): Unsubscribe;
74
+ onTransport(fn: (state: TransportPlaybackState) => void): Unsubscribe;
75
+ /**
76
+ * Synth load progress. Not one of the bus's three high-frequency channels —
77
+ * it reports per percent and behaves like ordinary React state, so the host
78
+ * puts it in its store. It is here because the host has no other way to hear
79
+ * about it.
80
+ */
81
+ onLoadState(fn: (state: PlaybackLoadState) => void): Unsubscribe;
82
+ dispose(): void;
83
+ }
84
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AACnD,OAAO,KAAK,EACV,iBAAiB,EACjB,aAAa,EACb,KAAK,EACL,UAAU,EACV,YAAY,EACZ,sBAAsB,EACvB,MAAM,yBAAyB,CAAC;AAEjC,MAAM,MAAM,WAAW,GAAG,MAAM,IAAI,CAAC;AAErC;;;;;;;;;;;;;;;GAeG;AACH,MAAM,WAAW,YAAY;IAC3B;;;;;;;;OAQG;IACH;;;OAGG;IACH,IAAI,CAAC,KAAK,EAAE,KAAK,EAAE,IAAI,CAAC,EAAE;QAAE,eAAe,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAElF,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACtB,KAAK,IAAI,IAAI,CAAC;IACd,IAAI,IAAI,IAAI,CAAC;IAEb,uFAAuF;IACvF,IAAI,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IAE9B,OAAO,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI,GAAG,IAAI,CAAC;IACxC,kBAAkB,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7C,YAAY,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAAC;IACrC,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IAEtC,sEAAsE;IACtE,QAAQ,CAAC,MAAM,EAAE,SAAS,aAAa,EAAE,GAAG,IAAI,CAAC;IAEjD,4DAA4D;IAC5D,gBAAgB,CAAC,eAAe,EAAE,SAAS,MAAM,EAAE,GAAG,IAAI,CAAC;IAE3D;;;;;;;;;;OAUG;IACH,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,YAAY,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC;IACpE,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAE5B;;;;;;;OAOG;IACH,QAAQ,CAAC,GAAG,EAAE,WAAW,CAAC;IAE1B,UAAU,CAAC,EAAE,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,GAAG,WAAW,CAAC;IACzD,UAAU,CAAC,EAAE,EAAE,CAAC,KAAK,EAAE,SAAS,YAAY,EAAE,KAAK,IAAI,GAAG,WAAW,CAAC;IACtE,WAAW,CAAC,EAAE,EAAE,CAAC,KAAK,EAAE,sBAAsB,KAAK,IAAI,GAAG,WAAW,CAAC;IACtE;;;;;OAKG;IACH,WAAW,CAAC,EAAE,EAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,IAAI,GAAG,WAAW,CAAC;IAEjE,OAAO,IAAI,IAAI,CAAC;CACjB"}
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
@@ -0,0 +1,24 @@
1
+ import type { SynthesizerLike } from './synth-types.js';
2
+ export type LoadedOfflineSynth = {
3
+ synth: SynthesizerLike;
4
+ sfontId: number;
5
+ sampleRate: number;
6
+ };
7
+ export type OfflineSynthRequest = {
8
+ fluidsynthModuleUrl: string;
9
+ fontUrl: string;
10
+ loadFont: (url: string) => Promise<ArrayBuffer>;
11
+ sampleRate: number;
12
+ /**
13
+ * Builds the synth. Injected only by tests — the real path first loads
14
+ * libfluidsynth into the page through a script tag, which a unit test has no
15
+ * way to satisfy. Supplying this skips that bring-up, the same seam
16
+ * `SynthHost` exposes for the same reason.
17
+ */
18
+ createSynth?: () => SynthesizerLike;
19
+ };
20
+ /** The shared offline synth, building it on first use. */
21
+ export declare function getOfflineSynth(request: OfflineSynthRequest): Promise<LoadedOfflineSynth>;
22
+ /** Drops the shared synth. Exposed for tests and teardown, not used in normal flow. */
23
+ export declare function releaseOfflineSynth(): void;
24
+ //# sourceMappingURL=offline-synth.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"offline-synth.d.ts","sourceRoot":"","sources":["../../../src/web/audio/offline-synth.ts"],"names":[],"mappings":"AAoBA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAUxD,MAAM,MAAM,kBAAkB,GAAG;IAC/B,KAAK,EAAE,eAAe,CAAC;IACvB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;CACpB,CAAC;AA6BF,MAAM,MAAM,mBAAmB,GAAG;IAChC,mBAAmB,EAAE,MAAM,CAAC;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,WAAW,CAAC,CAAC;IAChD,UAAU,EAAE,MAAM,CAAC;IACnB;;;;;OAKG;IACH,WAAW,CAAC,EAAE,MAAM,eAAe,CAAC;CACrC,CAAC;AAEF,0DAA0D;AAC1D,wBAAsB,eAAe,CAAC,OAAO,EAAE,mBAAmB,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAgC/F;AAED,uFAAuF;AACvF,wBAAgB,mBAAmB,IAAI,IAAI,CAE1C"}