@sudobility/music_player 0.1.0 → 0.1.1

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 +62 -0
  64. package/dist/shared/bus.d.ts.map +1 -0
  65. package/dist/shared/bus.js +73 -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 +716 -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,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"}
@@ -0,0 +1,92 @@
1
+ /**
2
+ * A long-lived synth for offline rendering, loaded once per page.
3
+ *
4
+ * Export cannot share playback's synth: Web Audio nodes belong to the context
5
+ * that created them, playback runs on a realtime `AudioContext`, and rendering
6
+ * faster than realtime needs an `OfflineAudioContext`. Nothing bridges the two.
7
+ * Worse, an `OfflineAudioContext` renders exactly once, so the previous design
8
+ * built a fresh worklet and reloaded the 23MB soundfont on *every export* —
9
+ * about five seconds each time, with libfluidsynth's build-stub notices along
10
+ * for the ride.
11
+ *
12
+ * `Synthesizer.render()` avoids all of it by needing no audio context at all:
13
+ * it fills buffers straight from the WASM synth. So one synth is built, the
14
+ * font is loaded into it once, and every export after the first is immediate.
15
+ *
16
+ * This one runs on the main thread rather than in a worklet, which is fine
17
+ * precisely because it is offline — there is no audio callback to starve.
18
+ */
19
+ import { CHANNELS_PER_INSTANCE } from '../playback/channel-allocator.js';
20
+ import { seedMainThreadQuietModule } from '../playback/quiet-stub-notices.js';
21
+ /** Matches playback's ceiling, so an export sounds like what was heard. */
22
+ const OFFLINE_POLYPHONY = 2048;
23
+ /** Resolves once `libfluidsynth-*.js` has been loaded and its WASM is ready. */
24
+ let fluidsynthReady = null;
25
+ /** The loaded synth, keyed so a changed font or rate rebuilds rather than lying. */
26
+ let cached = null;
27
+ /**
28
+ * Loads libfluidsynth into the *main thread*.
29
+ *
30
+ * A script tag rather than `addModule`: the non-worklet synth reads the module
31
+ * from the global scope, where `addModule` would put it inside the worklet
32
+ * instead. Memoised, since loading it twice would replace the global mid-flight.
33
+ */
34
+ async function ensureFluidsynth(moduleUrl) {
35
+ fluidsynthReady ??= (async () => {
36
+ if (typeof document === 'undefined') {
37
+ throw new Error('Offline rendering needs a document to load libfluidsynth');
38
+ }
39
+ // Before the script runs: emscripten reads the global Module on load.
40
+ seedMainThreadQuietModule();
41
+ await new Promise((resolve, reject) => {
42
+ const script = document.createElement('script');
43
+ script.src = moduleUrl;
44
+ script.onload = () => resolve();
45
+ script.onerror = () => reject(new Error(`Failed to load ${moduleUrl}`));
46
+ document.head.appendChild(script);
47
+ });
48
+ const jsSynth = await import('js-synthesizer');
49
+ await jsSynth.waitForReady();
50
+ })();
51
+ return fluidsynthReady;
52
+ }
53
+ /** The shared offline synth, building it on first use. */
54
+ export async function getOfflineSynth(request) {
55
+ const key = `${request.fluidsynthModuleUrl}|${request.fontUrl}|${request.sampleRate}`;
56
+ if (cached?.key === key)
57
+ return await cached.synth;
58
+ const building = (async () => {
59
+ let synth;
60
+ if (request.createSynth) {
61
+ synth = request.createSynth();
62
+ }
63
+ else {
64
+ await ensureFluidsynth(request.fluidsynthModuleUrl);
65
+ const jsSynth = await import('js-synthesizer');
66
+ synth = new jsSynth.Synthesizer();
67
+ }
68
+ // The same channel count playback uses, because both share
69
+ // `allocateChannels`: on a 16-channel synth an export would address
70
+ // channels that do not exist and silently lose every track past the
71
+ // sixteenth. Polyphony matches too, so an export sounds like what was heard.
72
+ synth.init(request.sampleRate, {
73
+ midiChannelCount: CHANNELS_PER_INSTANCE,
74
+ polyphony: OFFLINE_POLYPHONY,
75
+ });
76
+ const sfontId = await synth.loadSFont(await request.loadFont(request.fontUrl));
77
+ return { synth, sfontId, sampleRate: request.sampleRate };
78
+ })();
79
+ cached = { key, synth: building };
80
+ try {
81
+ return await building;
82
+ }
83
+ catch (error) {
84
+ cached = null; // a failed build must not be handed to the next caller
85
+ throw error;
86
+ }
87
+ }
88
+ /** Drops the shared synth. Exposed for tests and teardown, not used in normal flow. */
89
+ export function releaseOfflineSynth() {
90
+ cached = null;
91
+ }
92
+ //# sourceMappingURL=offline-synth.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"offline-synth.js","sourceRoot":"","sources":["../../../src/web/audio/offline-synth.ts"],"names":[],"mappings":"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"}