@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,83 @@
1
+ import { getMusicPositionSource } from '@sudobility/music_types';
2
+ export class PlaybackBus {
3
+ positionListeners = new Set();
4
+ soundingListeners = new Set();
5
+ transportListeners = new Set();
6
+ /** The last values published, so a subscriber joining mid-playback is not blind until the next event. */
7
+ lastSounding = [];
8
+ lastTransport = 'stopped';
9
+ onPosition(listener) {
10
+ this.positionListeners.add(listener);
11
+ return () => this.positionListeners.delete(listener);
12
+ }
13
+ onSounding(listener) {
14
+ this.soundingListeners.add(listener);
15
+ return () => this.soundingListeners.delete(listener);
16
+ }
17
+ onTransport(listener) {
18
+ this.transportListeners.add(listener);
19
+ return () => this.transportListeners.delete(listener);
20
+ }
21
+ /**
22
+ * Every caret move in the app arrives here — the engine's position reports,
23
+ * a seek, a click on the staff, an arrow key — which is what makes this the
24
+ * one place that has to tell `IMusicPosition` about it. Reporting from the
25
+ * controller's report handler instead would have covered playback only, and
26
+ * left the shared position stale the moment somebody moved the caret by
27
+ * hand.
28
+ *
29
+ * The anchor is stamped **here**, inside the engine's own callback, rather
30
+ * than wherever a React subscriber eventually runs: that is the difference
31
+ * between measuring from the audio clock and measuring from event-loop
32
+ * latency, and it is why the caret used to drift away from the highlights
33
+ * and the keyboard under load.
34
+ */
35
+ publishPosition(tick) {
36
+ getMusicPositionSource().report(tick);
37
+ for (const listener of this.positionListeners)
38
+ listener(tick);
39
+ }
40
+ /**
41
+ * The engine already emits only on change (`SoundingSet`), so this does not
42
+ * filter again — doing so would hide a deliberate re-publish after a seek.
43
+ */
44
+ publishSounding(notes) {
45
+ this.lastSounding = notes;
46
+ for (const listener of this.soundingListeners)
47
+ listener(notes);
48
+ }
49
+ publishTransport(state) {
50
+ this.lastTransport = state;
51
+ for (const listener of this.transportListeners)
52
+ listener(state);
53
+ }
54
+ /**
55
+ * Where playback last reported it was. Read once on subscribe; do not poll
56
+ * this.
57
+ *
58
+ * Delegated to the playhead rather than kept here. The bus used to store its
59
+ * own copy of the reported tick beside the one `IMusicPositionSource` holds,
60
+ * which made two objects each own the same fact — and they duly came apart:
61
+ * when the engine's throttle stopped reporting, the playhead went on
62
+ * projecting while this copy stayed where it was, so the caret glided the
63
+ * length of the score with the scroll and the readouts still at bar one.
64
+ * Reading through means the smoothed position and the reported one are two
65
+ * views of one state, which is what the interface promises.
66
+ */
67
+ get positionTick() {
68
+ return getMusicPositionSource().reportedTick;
69
+ }
70
+ get sounding() {
71
+ return this.lastSounding;
72
+ }
73
+ get transport() {
74
+ return this.lastTransport;
75
+ }
76
+ /** Drops every listener. For teardown; a live app never calls this. */
77
+ clear() {
78
+ this.positionListeners.clear();
79
+ this.soundingListeners.clear();
80
+ this.transportListeners.clear();
81
+ }
82
+ }
83
+ //# sourceMappingURL=bus.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bus.js","sourceRoot":"","sources":["../../src/shared/bus.ts"],"names":[],"mappings":"AA2BA,OAAO,EAAE,sBAAsB,EAAE,MAAM,yBAAyB,CAAC;AAEjE,MAAM,OAAO,WAAW;IACL,iBAAiB,GAAG,IAAI,GAAG,EAA0B,CAAC;IACtD,iBAAiB,GAAG,IAAI,GAAG,EAEzC,CAAC;IACa,kBAAkB,GAAG,IAAI,GAAG,EAE1C,CAAC;IAEJ,yGAAyG;IACjG,YAAY,GAA4B,EAAE,CAAC;IAC3C,aAAa,GAA2B,SAAS,CAAC;IAE1D,UAAU,CAAC,QAAgC;QACzC,IAAI,CAAC,iBAAiB,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QACrC,OAAO,GAAG,EAAE,CAAC,IAAI,CAAC,iBAAiB,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;IACvD,CAAC;IAED,UAAU,CAAC,QAAkD;QAC3D,IAAI,CAAC,iBAAiB,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QACrC,OAAO,GAAG,EAAE,CAAC,IAAI,CAAC,iBAAiB,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;IACvD,CAAC;IAED,WAAW,CAAC,QAAiD;QAC3D,IAAI,CAAC,kBAAkB,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QACtC,OAAO,GAAG,EAAE,CAAC,IAAI,CAAC,kBAAkB,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;IACxD,CAAC;IAED;;;;;;;;;;;;;OAaG;IACH,eAAe,CAAC,IAAY;QAC1B,sBAAsB,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;QACtC,KAAK,MAAM,QAAQ,IAAI,IAAI,CAAC,iBAAiB;YAAE,QAAQ,CAAC,IAAI,CAAC,CAAC;IAChE,CAAC;IAED;;;OAGG;IACH,eAAe,CAAC,KAA8B;QAC5C,IAAI,CAAC,YAAY,GAAG,KAAK,CAAC;QAC1B,KAAK,MAAM,QAAQ,IAAI,IAAI,CAAC,iBAAiB;YAAE,QAAQ,CAAC,KAAK,CAAC,CAAC;IACjE,CAAC;IAED,gBAAgB,CAAC,KAA6B;QAC5C,IAAI,CAAC,aAAa,GAAG,KAAK,CAAC;QAC3B,KAAK,MAAM,QAAQ,IAAI,IAAI,CAAC,kBAAkB;YAAE,QAAQ,CAAC,KAAK,CAAC,CAAC;IAClE,CAAC;IAED;;;;;;;;;;;;OAYG;IACH,IAAI,YAAY;QACd,OAAO,sBAAsB,EAAE,CAAC,YAAY,CAAC;IAC/C,CAAC;IAED,IAAI,QAAQ;QACV,OAAO,IAAI,CAAC,YAAY,CAAC;IAC3B,CAAC;IAED,IAAI,SAAS;QACX,OAAO,IAAI,CAAC,aAAa,CAAC;IAC5B,CAAC;IAED,uEAAuE;IACvE,KAAK;QACH,IAAI,CAAC,iBAAiB,CAAC,KAAK,EAAE,CAAC;QAC/B,IAAI,CAAC,iBAAiB,CAAC,KAAK,EAAE,CAAC;QAC/B,IAAI,CAAC,kBAAkB,CAAC,KAAK,EAAE,CAAC;IAClC,CAAC;CACF"}
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Pure MIDI-number conversions used by the Tone.js adapter (Task 13). Kept
3
+ * dependency-free (no Tone.js import) so they're trivially unit-testable
4
+ * without mocking `tone`, per the Task 13 brief's guidance to keep every
5
+ * piece of logic that doesn't actually need Tone in small, separately
6
+ * testable functions.
7
+ */
8
+ /** Converts a MIDI note number (60 = C4) to its frequency in Hertz. */
9
+ export declare function midiToHertz(midi: number): number;
10
+ /**
11
+ * Converts a domain `NoteEvent.velocity` (spec §4: integer 0-127) to Tone's
12
+ * normalized 0-1 velocity range, clamped defensively in case of an
13
+ * out-of-range value slipping through (validation should catch this
14
+ * earlier, but the audio layer shouldn't crash or produce out-of-range
15
+ * gain if it doesn't).
16
+ */
17
+ export declare function normalizeVelocity(velocity: number): number;
18
+ //# sourceMappingURL=midi.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"midi.d.ts","sourceRoot":"","sources":["../../src/shared/midi.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAMH,uEAAuE;AACvE,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEhD;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAE1D"}
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Pure MIDI-number conversions used by the Tone.js adapter (Task 13). Kept
3
+ * dependency-free (no Tone.js import) so they're trivially unit-testable
4
+ * without mocking `tone`, per the Task 13 brief's guidance to keep every
5
+ * piece of logic that doesn't actually need Tone in small, separately
6
+ * testable functions.
7
+ */
8
+ /** A4 = 69 = 440Hz, the standard MIDI tuning reference. */
9
+ const A4_MIDI = 69;
10
+ const A4_HERTZ = 440;
11
+ /** Converts a MIDI note number (60 = C4) to its frequency in Hertz. */
12
+ export function midiToHertz(midi) {
13
+ return A4_HERTZ * 2 ** ((midi - A4_MIDI) / 12);
14
+ }
15
+ /**
16
+ * Converts a domain `NoteEvent.velocity` (spec §4: integer 0-127) to Tone's
17
+ * normalized 0-1 velocity range, clamped defensively in case of an
18
+ * out-of-range value slipping through (validation should catch this
19
+ * earlier, but the audio layer shouldn't crash or produce out-of-range
20
+ * gain if it doesn't).
21
+ */
22
+ export function normalizeVelocity(velocity) {
23
+ return Math.min(1, Math.max(0, velocity / 127));
24
+ }
25
+ //# sourceMappingURL=midi.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"midi.js","sourceRoot":"","sources":["../../src/shared/midi.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,2DAA2D;AAC3D,MAAM,OAAO,GAAG,EAAE,CAAC;AACnB,MAAM,QAAQ,GAAG,GAAG,CAAC;AAErB,uEAAuE;AACvE,MAAM,UAAU,WAAW,CAAC,IAAY;IACtC,OAAO,QAAQ,GAAG,CAAC,IAAI,CAAC,CAAC,IAAI,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC;AACjD,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,iBAAiB,CAAC,QAAgB;IAChD,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,QAAQ,GAAG,GAAG,CAAC,CAAC,CAAC;AAClD,CAAC"}
@@ -0,0 +1,75 @@
1
+ /**
2
+ * The master-bus rules, shared by live playback and offline export on both
3
+ * platforms — `web/playback/synth-host.ts`, `web/audio/soundfont-render.ts`,
4
+ * `rn/playback/sample-engine.ts` and `rn/audio/offline-render.ts`.
5
+ *
6
+ * They live here rather than in each caller because an export that mixed
7
+ * differently from playback is exactly the bug this module exists to prevent:
8
+ * the file is supposed to be a recording of what was heard, and copies of these
9
+ * numbers would drift apart the first time one was tuned.
10
+ *
11
+ * Living here is not the same as being used, which is how the web export came
12
+ * to sum its channels raw while playback trimmed them — `sqrt(n)` louder than
13
+ * what was heard, into encoders that hard-clamp. All four callers are named
14
+ * above so the next one to be added is noticed by its absence.
15
+ */
16
+ /**
17
+ * Ceiling for the safety limiter on the master bus, in dB below full scale.
18
+ *
19
+ * The headroom trim sizes the mix for how loud it is *on average*; this catches
20
+ * the peaks it cannot predict — the moments when parts happen to line up in
21
+ * phase, which sum far above the incoherent average. Just under full scale
22
+ * rather than at it, so the limiter, and not the sound card, decides the
23
+ * ceiling.
24
+ */
25
+ export declare const LIMITER_CEILING_DB = -1;
26
+ /**
27
+ * How far to pull the master down for `trackCount` channels summing into it.
28
+ *
29
+ * Every channel is scheduled at its own MIDI volume with nothing reconciling
30
+ * them, so the mix got louder with every track added. A nine-track arrangement
31
+ * summed to roughly two and a half times full scale and clipped through most of
32
+ * the piece; what that distortion buried first were the quiet, sustained
33
+ * voices, which is why tracks that were neither muted nor silent could not be
34
+ * heard at all.
35
+ *
36
+ * `1/sqrt(n)`, not `1/n`: separate instrument parts are not phase-correlated,
37
+ * so their powers add rather than their amplitudes. `1/n` is the law for copies
38
+ * of one signal and would leave a large arrangement far too quiet. A single
39
+ * track keeps unity — it has nothing to sum with.
40
+ */
41
+ export declare function headroomTrimFor(trackCount: number): number;
42
+ /**
43
+ * Holds a rendered buffer under `LIMITER_CEILING_DB`, in place.
44
+ *
45
+ * The offline counterpart of the `DynamicsCompressorNode` on the live master
46
+ * bus, and it exists for the same reason: the headroom trim sizes the mix for
47
+ * how loud it is *on average*, and the moments where parts happen to line up in
48
+ * phase sum far above that. Live those peaks meet a limiter; in an export they
49
+ * meet `encodeWav`/`encodeMp3`, which clamp — so without this the loudest
50
+ * instants of a file are hard-clipped rather than ducked.
51
+ *
52
+ * It is **not** a reimplementation of that node. `DynamicsCompressorNode` has a
53
+ * knee, a ratio and program-dependent behaviour the specification does not pin
54
+ * down closely enough to reproduce, and chasing it would be pretending to a
55
+ * precision that is not there. What is reproduced is the job: nothing leaves
56
+ * here above the ceiling, and it gets there by ducking rather than clipping.
57
+ *
58
+ * The gain is worked out **backwards**, which is the one thing an offline
59
+ * limiter can do that a live one cannot. Each sample's gain is the lower of
60
+ * what that sample needs and what the next sample allows plus one attack step,
61
+ * so the ramp down is *finished* when the peak lands instead of chasing it.
62
+ * That ordering is also what makes the ceiling exact rather than approximate: a
63
+ * forward one-pole never quite arrives, and clamping the residue turns the last
64
+ * fraction of a decibel back into the clipping this replaces.
65
+ *
66
+ * The forward pass is the release, and the reason a sustained loud passage does
67
+ * not pump: within one cycle the requirement relaxes at every zero crossing,
68
+ * and the bounded climb simply does not follow it.
69
+ *
70
+ * Costs one `Float32Array` the length of the render — 53MB for five minutes at
71
+ * 44.1kHz, freed on return, against a `samples` buffer of the same size that
72
+ * the caller is already holding.
73
+ */
74
+ export declare function limitPeaks(samples: Float32Array, sampleRate: number): void;
75
+ //# sourceMappingURL=mix.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mix.d.ts","sourceRoot":"","sources":["../../src/shared/mix.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH;;;;;;;;GAQG;AACH,eAAO,MAAM,kBAAkB,KAAK,CAAC;AAErC;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,eAAe,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAE1D;AAsBD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,GAAG,IAAI,CAyB1E"}
@@ -0,0 +1,115 @@
1
+ /**
2
+ * The master-bus rules, shared by live playback and offline export on both
3
+ * platforms — `web/playback/synth-host.ts`, `web/audio/soundfont-render.ts`,
4
+ * `rn/playback/sample-engine.ts` and `rn/audio/offline-render.ts`.
5
+ *
6
+ * They live here rather than in each caller because an export that mixed
7
+ * differently from playback is exactly the bug this module exists to prevent:
8
+ * the file is supposed to be a recording of what was heard, and copies of these
9
+ * numbers would drift apart the first time one was tuned.
10
+ *
11
+ * Living here is not the same as being used, which is how the web export came
12
+ * to sum its channels raw while playback trimmed them — `sqrt(n)` louder than
13
+ * what was heard, into encoders that hard-clamp. All four callers are named
14
+ * above so the next one to be added is noticed by its absence.
15
+ */
16
+ /**
17
+ * Ceiling for the safety limiter on the master bus, in dB below full scale.
18
+ *
19
+ * The headroom trim sizes the mix for how loud it is *on average*; this catches
20
+ * the peaks it cannot predict — the moments when parts happen to line up in
21
+ * phase, which sum far above the incoherent average. Just under full scale
22
+ * rather than at it, so the limiter, and not the sound card, decides the
23
+ * ceiling.
24
+ */
25
+ export const LIMITER_CEILING_DB = -1;
26
+ /**
27
+ * How far to pull the master down for `trackCount` channels summing into it.
28
+ *
29
+ * Every channel is scheduled at its own MIDI volume with nothing reconciling
30
+ * them, so the mix got louder with every track added. A nine-track arrangement
31
+ * summed to roughly two and a half times full scale and clipped through most of
32
+ * the piece; what that distortion buried first were the quiet, sustained
33
+ * voices, which is why tracks that were neither muted nor silent could not be
34
+ * heard at all.
35
+ *
36
+ * `1/sqrt(n)`, not `1/n`: separate instrument parts are not phase-correlated,
37
+ * so their powers add rather than their amplitudes. `1/n` is the law for copies
38
+ * of one signal and would leave a large arrangement far too quiet. A single
39
+ * track keeps unity — it has nothing to sum with.
40
+ */
41
+ export function headroomTrimFor(trackCount) {
42
+ return trackCount > 1 ? 1 / Math.sqrt(trackCount) : 1;
43
+ }
44
+ /**
45
+ * Attack and release for the offline limiter, in seconds, quoted against
46
+ * `LIMITER_REFERENCE_DB` of reduction.
47
+ *
48
+ * The same numbers `SynthHost` gives its `DynamicsCompressorNode`, so a peak
49
+ * ducks for about as long either way. Expressed as a slew rate rather than a
50
+ * filter coefficient, which is what makes the ceiling exact — see `limitPeaks`.
51
+ */
52
+ const LIMITER_ATTACK_SECONDS = 0.003;
53
+ const LIMITER_RELEASE_SECONDS = 0.25;
54
+ /**
55
+ * The depth the attack and release times describe.
56
+ *
57
+ * A slew rate needs a distance to be a time. Twelve decibels is deep enough to
58
+ * cover the peaks a phase-aligned tutti produces over the headroom trim, and a
59
+ * shallower reduction simply completes proportionally sooner.
60
+ */
61
+ const LIMITER_REFERENCE_DB = 12;
62
+ /**
63
+ * Holds a rendered buffer under `LIMITER_CEILING_DB`, in place.
64
+ *
65
+ * The offline counterpart of the `DynamicsCompressorNode` on the live master
66
+ * bus, and it exists for the same reason: the headroom trim sizes the mix for
67
+ * how loud it is *on average*, and the moments where parts happen to line up in
68
+ * phase sum far above that. Live those peaks meet a limiter; in an export they
69
+ * meet `encodeWav`/`encodeMp3`, which clamp — so without this the loudest
70
+ * instants of a file are hard-clipped rather than ducked.
71
+ *
72
+ * It is **not** a reimplementation of that node. `DynamicsCompressorNode` has a
73
+ * knee, a ratio and program-dependent behaviour the specification does not pin
74
+ * down closely enough to reproduce, and chasing it would be pretending to a
75
+ * precision that is not there. What is reproduced is the job: nothing leaves
76
+ * here above the ceiling, and it gets there by ducking rather than clipping.
77
+ *
78
+ * The gain is worked out **backwards**, which is the one thing an offline
79
+ * limiter can do that a live one cannot. Each sample's gain is the lower of
80
+ * what that sample needs and what the next sample allows plus one attack step,
81
+ * so the ramp down is *finished* when the peak lands instead of chasing it.
82
+ * That ordering is also what makes the ceiling exact rather than approximate: a
83
+ * forward one-pole never quite arrives, and clamping the residue turns the last
84
+ * fraction of a decibel back into the clipping this replaces.
85
+ *
86
+ * The forward pass is the release, and the reason a sustained loud passage does
87
+ * not pump: within one cycle the requirement relaxes at every zero crossing,
88
+ * and the bounded climb simply does not follow it.
89
+ *
90
+ * Costs one `Float32Array` the length of the render — 53MB for five minutes at
91
+ * 44.1kHz, freed on return, against a `samples` buffer of the same size that
92
+ * the caller is already holding.
93
+ */
94
+ export function limitPeaks(samples, sampleRate) {
95
+ const attackSlope = LIMITER_REFERENCE_DB / Math.max(1, LIMITER_ATTACK_SECONDS * sampleRate);
96
+ const releaseSlope = LIMITER_REFERENCE_DB / Math.max(1, LIMITER_RELEASE_SECONDS * sampleRate);
97
+ // Gain in dB, never above zero: a limiter only ever turns things down.
98
+ const envelope = new Float32Array(samples.length);
99
+ let carried = 0;
100
+ for (let i = samples.length - 1; i >= 0; i -= 1) {
101
+ const magnitude = Math.abs(samples[i]);
102
+ const required = magnitude > 0
103
+ ? Math.min(0, LIMITER_CEILING_DB - 20 * Math.log10(magnitude))
104
+ : 0;
105
+ carried = Math.min(required, carried + attackSlope);
106
+ envelope[i] = carried;
107
+ }
108
+ let level = envelope[0] ?? 0;
109
+ for (let i = 0; i < samples.length; i += 1) {
110
+ level = Math.min(envelope[i], level + releaseSlope);
111
+ // Exactly 1 where nothing needed doing, so an untouched mix stays bit-identical.
112
+ samples[i] = level === 0 ? samples[i] : samples[i] * 10 ** (level / 20);
113
+ }
114
+ }
115
+ //# sourceMappingURL=mix.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mix.js","sourceRoot":"","sources":["../../src/shared/mix.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,CAAC,CAAC;AAErC;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,eAAe,CAAC,UAAkB;IAChD,OAAO,UAAU,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AACxD,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,sBAAsB,GAAG,KAAK,CAAC;AACrC,MAAM,uBAAuB,GAAG,IAAI,CAAC;AAErC;;;;;;GAMG;AACH,MAAM,oBAAoB,GAAG,EAAE,CAAC;AAEhC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,MAAM,UAAU,UAAU,CAAC,OAAqB,EAAE,UAAkB;IAClE,MAAM,WAAW,GACf,oBAAoB,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,sBAAsB,GAAG,UAAU,CAAC,CAAC;IAC1E,MAAM,YAAY,GAChB,oBAAoB,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,uBAAuB,GAAG,UAAU,CAAC,CAAC;IAE3E,uEAAuE;IACvE,MAAM,QAAQ,GAAG,IAAI,YAAY,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;IAClD,IAAI,OAAO,GAAG,CAAC,CAAC;IAChB,KAAK,IAAI,CAAC,GAAG,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;QAChD,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAE,CAAC,CAAC;QACxC,MAAM,QAAQ,GACZ,SAAS,GAAG,CAAC;YACX,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,kBAAkB,GAAG,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;YAC9D,CAAC,CAAC,CAAC,CAAC;QACR,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,QAAQ,EAAE,OAAO,GAAG,WAAW,CAAC,CAAC;QACpD,QAAQ,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC;IACxB,CAAC;IAED,IAAI,KAAK,GAAG,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;IAC7B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;QAC3C,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAE,EAAE,KAAK,GAAG,YAAY,CAAC,CAAC;QACrD,iFAAiF;QACjF,OAAO,CAAC,CAAC,CAAC,GAAG,KAAK,KAAK,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAE,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAE,GAAG,EAAE,IAAI,CAAC,KAAK,GAAG,EAAE,CAAC,CAAC;IAC5E,CAAC;AACH,CAAC"}
@@ -0,0 +1,43 @@
1
+ /**
2
+ * The notes of a score, in time order, with a cursor.
3
+ *
4
+ * Replaces scheduling every note up front. The old engine handed the whole
5
+ * piece to Tone's Transport as individual events — some 11,500 of them for a
6
+ * three-minute arrangement — and every seek, edit or tempo change cancelled
7
+ * and rebuilt all of it. Here a seek is a binary search and a cursor move,
8
+ * whatever the length of the piece.
9
+ *
10
+ * Deliberately knows nothing about time in seconds: ticks are the score's own
11
+ * unit, and converting them depends on the tempo map and the playback-speed
12
+ * multiplier, both of which belong to the engine.
13
+ */
14
+ import type { PlaybackNote as ScheduledNote } from '@sudobility/music_types';
15
+ export declare class NoteQueue {
16
+ private notes;
17
+ private cursor;
18
+ load(notes: readonly ScheduledNote[]): void;
19
+ /**
20
+ * Moves the cursor to the first note at or after `tick`.
21
+ *
22
+ * Binary search rather than a scan: this runs on every seek, and a scan
23
+ * would make seeking cost more the further into the piece you go.
24
+ */
25
+ seekToTick(tick: number): void;
26
+ /**
27
+ * Every not-yet-returned note starting at or before `tick`, up to `maxCount`
28
+ * of them, advancing the cursor past what it returns.
29
+ *
30
+ * The cap bounds one pump tick's cost. On the web engine each note becomes a
31
+ * `postMessage` to the worklet sequencer, and a four-second horizon over a
32
+ * two-hundred-track score is thousands of them; whatever the cap leaves
33
+ * behind is picked up 50ms later, long before it is due.
34
+ *
35
+ * Unbounded by default, because the React Native sample engine drains its
36
+ * whole horizon in one go — it posts nothing across a thread boundary, so it
37
+ * has nothing to bound.
38
+ */
39
+ drainUntil(tick: number, maxCount?: number): ScheduledNote[];
40
+ /** True once every note has been drained — how the engine knows the piece is over. */
41
+ get exhausted(): boolean;
42
+ }
43
+ //# sourceMappingURL=note-queue.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"note-queue.d.ts","sourceRoot":"","sources":["../../src/shared/note-queue.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,KAAK,EAAE,YAAY,IAAI,aAAa,EAAE,MAAM,yBAAyB,CAAC;AAE7E,qBAAa,SAAS;IACpB,OAAO,CAAC,KAAK,CAAuB;IACpC,OAAO,CAAC,MAAM,CAAK;IAEnB,IAAI,CAAC,KAAK,EAAE,SAAS,aAAa,EAAE,GAAG,IAAI;IAK3C;;;;;OAKG;IACH,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAW9B;;;;;;;;;;;;OAYG;IACH,UAAU,CACR,IAAI,EAAE,MAAM,EACZ,QAAQ,SAA0B,GACjC,aAAa,EAAE;IAalB,sFAAsF;IACtF,IAAI,SAAS,IAAI,OAAO,CAEvB;CACF"}
@@ -0,0 +1,54 @@
1
+ export class NoteQueue {
2
+ notes = [];
3
+ cursor = 0;
4
+ load(notes) {
5
+ this.notes = [...notes].sort((a, b) => a.tick - b.tick);
6
+ this.cursor = 0;
7
+ }
8
+ /**
9
+ * Moves the cursor to the first note at or after `tick`.
10
+ *
11
+ * Binary search rather than a scan: this runs on every seek, and a scan
12
+ * would make seeking cost more the further into the piece you go.
13
+ */
14
+ seekToTick(tick) {
15
+ let lo = 0;
16
+ let hi = this.notes.length;
17
+ while (lo < hi) {
18
+ const mid = (lo + hi) >> 1;
19
+ if (this.notes[mid].tick < tick)
20
+ lo = mid + 1;
21
+ else
22
+ hi = mid;
23
+ }
24
+ this.cursor = lo;
25
+ }
26
+ /**
27
+ * Every not-yet-returned note starting at or before `tick`, up to `maxCount`
28
+ * of them, advancing the cursor past what it returns.
29
+ *
30
+ * The cap bounds one pump tick's cost. On the web engine each note becomes a
31
+ * `postMessage` to the worklet sequencer, and a four-second horizon over a
32
+ * two-hundred-track score is thousands of them; whatever the cap leaves
33
+ * behind is picked up 50ms later, long before it is due.
34
+ *
35
+ * Unbounded by default, because the React Native sample engine drains its
36
+ * whole horizon in one go — it posts nothing across a thread boundary, so it
37
+ * has nothing to bound.
38
+ */
39
+ drainUntil(tick, maxCount = Number.MAX_SAFE_INTEGER) {
40
+ const out = [];
41
+ while (out.length < maxCount &&
42
+ this.cursor < this.notes.length &&
43
+ this.notes[this.cursor].tick <= tick) {
44
+ out.push(this.notes[this.cursor]);
45
+ this.cursor += 1;
46
+ }
47
+ return out;
48
+ }
49
+ /** True once every note has been drained — how the engine knows the piece is over. */
50
+ get exhausted() {
51
+ return this.cursor >= this.notes.length;
52
+ }
53
+ }
54
+ //# sourceMappingURL=note-queue.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"note-queue.js","sourceRoot":"","sources":["../../src/shared/note-queue.ts"],"names":[],"mappings":"AAeA,MAAM,OAAO,SAAS;IACZ,KAAK,GAAoB,EAAE,CAAC;IAC5B,MAAM,GAAG,CAAC,CAAC;IAEnB,IAAI,CAAC,KAA+B;QAClC,IAAI,CAAC,KAAK,GAAG,CAAC,GAAG,KAAK,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC;QACxD,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC;IAClB,CAAC;IAED;;;;;OAKG;IACH,UAAU,CAAC,IAAY;QACrB,IAAI,EAAE,GAAG,CAAC,CAAC;QACX,IAAI,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC;QAC3B,OAAO,EAAE,GAAG,EAAE,EAAE,CAAC;YACf,MAAM,GAAG,GAAG,CAAC,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,CAAC;YAC3B,IAAI,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,GAAG,IAAI;gBAAE,EAAE,GAAG,GAAG,GAAG,CAAC,CAAC;;gBACzC,EAAE,GAAG,GAAG,CAAC;QAChB,CAAC;QACD,IAAI,CAAC,MAAM,GAAG,EAAE,CAAC;IACnB,CAAC;IAED;;;;;;;;;;;;OAYG;IACH,UAAU,CACR,IAAY,EACZ,QAAQ,GAAG,MAAM,CAAC,gBAAgB;QAElC,MAAM,GAAG,GAAoB,EAAE,CAAC;QAChC,OACE,GAAG,CAAC,MAAM,GAAG,QAAQ;YACrB,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM;YAC/B,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,IAAI,IAAI,IAAI,EACpC,CAAC;YACD,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC;YAClC,IAAI,CAAC,MAAM,IAAI,CAAC,CAAC;QACnB,CAAC;QACD,OAAO,GAAG,CAAC;IACb,CAAC;IAED,sFAAsF;IACtF,IAAI,SAAS;QACX,OAAO,IAAI,CAAC,MAAM,IAAI,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC;IAC1C,CAAC;CACF"}
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Everything live playback needs, decided here rather than in the engine.
3
+ *
4
+ * The live counterpart of `renderEvents`, and the reason music_io needs no
5
+ * musical code at all: ties are joined, pitches resolved, the measure grid's
6
+ * beats read off, the GM voice resolved and the tempo handed over as a
7
+ * conversion. The engine schedules and sounds; it decides nothing.
8
+ *
9
+ * This is `schedule.ts` from music_io, moved to where its imports already
10
+ * lived. The note traversal itself is `flattenScoreNotes`, shared with
11
+ * `renderEvents` so live and offline cannot drift over ties again.
12
+ */
13
+ import type { PlaybackPlan, PlaybackTrack, Score } from '@sudobility/music_types';
14
+ import type { AuditionVoice } from '../engine.js';
15
+ /**
16
+ * The GM voice a program addresses.
17
+ *
18
+ * The one place the kit-versus-instrument distinction is resolved, shared by
19
+ * the plan and by auditioning. A percussion track's `midiProgram` is a *kit*
20
+ * address — `gmKitAt` maps any address to the kit whose region contains it, so
21
+ * a score that arrives at a program GM defines no kit at still plays — and the
22
+ * name must be the kit's, because `gmInstrument(40)` is Violin where kit 40 is
23
+ * Brush.
24
+ */
25
+ export declare function resolveVoice(program: number, isPercussion: boolean, fallbackName?: string): AuditionVoice;
26
+ /**
27
+ * The tracks alone.
28
+ *
29
+ * Separate from `playbackPlan` because a mix change while playing must not
30
+ * rebuild every note — `PlaybackEngine.applyMix` takes only this.
31
+ */
32
+ export declare function playbackTracks(score: Score): PlaybackTrack[];
33
+ export declare function playbackPlan(score: Score): PlaybackPlan;
34
+ //# sourceMappingURL=plan.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plan.d.ts","sourceRoot":"","sources":["../../src/shared/plan.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,KAAK,EAEV,YAAY,EACZ,aAAa,EACb,KAAK,EACN,MAAM,yBAAyB,CAAC;AACjC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAalD;;;;;;;;;GASG;AACH,wBAAgB,YAAY,CAC1B,OAAO,EAAE,MAAM,EACf,YAAY,EAAE,OAAO,EACrB,YAAY,SAAK,GAChB,aAAa,CAUf;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,KAAK,GAAG,aAAa,EAAE,CAqB5D;AAqDD,wBAAgB,YAAY,CAAC,KAAK,EAAE,KAAK,GAAG,YAAY,CAgDvD"}
@@ -0,0 +1,134 @@
1
+ import { TempoMap } from '@sudobility/music_types';
2
+ import { performanceTimeline, } from '@sudobility/music_types';
3
+ import { fermataTempoMap } from '@sudobility/music_types';
4
+ import { flattenScoreNotes } from '@sudobility/music_types';
5
+ import { beatBoundaries } from '@sudobility/music_types';
6
+ import { gmInstrument } from '@sudobility/music_types';
7
+ import { gmKitAt } from '@sudobility/music_types';
8
+ import { isPercussionTrack } from '@sudobility/music_types';
9
+ /**
10
+ * The GM voice a program addresses.
11
+ *
12
+ * The one place the kit-versus-instrument distinction is resolved, shared by
13
+ * the plan and by auditioning. A percussion track's `midiProgram` is a *kit*
14
+ * address — `gmKitAt` maps any address to the kit whose region contains it, so
15
+ * a score that arrives at a program GM defines no kit at still plays — and the
16
+ * name must be the kit's, because `gmInstrument(40)` is Violin where kit 40 is
17
+ * Brush.
18
+ */
19
+ export function resolveVoice(program, isPercussion, fallbackName = '') {
20
+ if (isPercussion) {
21
+ const kit = gmKitAt(program);
22
+ return { program: kit.program, name: kit.name, isPercussion: true };
23
+ }
24
+ return {
25
+ program,
26
+ name: gmInstrument(program)?.name ?? fallbackName,
27
+ isPercussion: false,
28
+ };
29
+ }
30
+ /**
31
+ * The tracks alone.
32
+ *
33
+ * Separate from `playbackPlan` because a mix change while playing must not
34
+ * rebuild every note — `PlaybackEngine.applyMix` takes only this.
35
+ */
36
+ export function playbackTracks(score) {
37
+ return score.tracks.map(track => {
38
+ const percussion = isPercussionTrack(track);
39
+ const voice = resolveVoice(track.midiProgram, percussion, track.instrumentName);
40
+ return {
41
+ id: track.id,
42
+ midiProgram: track.midiProgram,
43
+ instrumentName: track.instrumentName,
44
+ isPercussion: percussion,
45
+ volume: track.volume,
46
+ pan: track.pan,
47
+ muted: track.muted,
48
+ solo: track.solo,
49
+ voiceProgram: voice.program,
50
+ voiceName: voice.name,
51
+ };
52
+ });
53
+ }
54
+ /**
55
+ * Every beat position across the measure grid, read off the first track —
56
+ * every track shares one grid once `rebuildMeasureTicks` has run.
57
+ */
58
+ function metronomeClicks(score) {
59
+ const track = score.tracks[0];
60
+ if (!track)
61
+ return [];
62
+ const clicks = [];
63
+ for (const measure of track.measures) {
64
+ beatBoundaries(measure.timeSignature, score.ppq).forEach((offset, i) => {
65
+ clicks.push({ tick: measure.startTick + offset, accent: i === 0 });
66
+ });
67
+ }
68
+ return clicks;
69
+ }
70
+ /**
71
+ * Lays `events` out along the timeline, repeating what the repeats repeat.
72
+ *
73
+ * Each segment copies the written events inside its source range to its own
74
+ * place in performance time. A copy gets a distinct id — the caret lights a
75
+ * note by id, and the same written note sounding twice would otherwise light
76
+ * both times at once.
77
+ *
78
+ * The identity timeline copies everything exactly once at its own tick, so a
79
+ * score without repeats produces precisely what it did before.
80
+ */
81
+ function expandAlongTimeline(events, timeline, withTick) {
82
+ if (timeline.segments.length === 0)
83
+ return events;
84
+ const expanded = [];
85
+ const passesBySource = new Map();
86
+ for (const segment of timeline.segments) {
87
+ const pass = (passesBySource.get(segment.sourceTick) ?? 0) + 1;
88
+ passesBySource.set(segment.sourceTick, pass);
89
+ const end = segment.sourceTick + segment.durationTicks;
90
+ for (const event of events) {
91
+ if (event.tick < segment.sourceTick || event.tick >= end)
92
+ continue;
93
+ const offset = event.tick - segment.sourceTick;
94
+ expanded.push(withTick(event, segment.performanceTick + offset, pass));
95
+ }
96
+ }
97
+ return expanded.sort((a, b) => a.tick - b.tick);
98
+ }
99
+ export function playbackPlan(score) {
100
+ const timeline = performanceTimeline(score);
101
+ /*
102
+ Notes are laid out in *performance* time, so a repeated bar genuinely
103
+ sounds twice. Everything that draws translates back through `timeline`
104
+ — see `sourceTickFor` — which is what keeps the score the canonical,
105
+ written thing rather than teaching the caret about repeats.
106
+ */
107
+ const notes = expandAlongTimeline(flattenScoreNotes(score), timeline, (note, tick, pass) => ({
108
+ ...note,
109
+ tick,
110
+ // Distinct per pass: the caret lights a note by id, and one written
111
+ // note sounding twice must not light both places at once.
112
+ noteId: pass === 1 ? note.noteId : `${note.noteId}#${pass}`,
113
+ }));
114
+ const clicks = expandAlongTimeline(metronomeClicks(score), timeline, (click, tick) => ({ ...click, tick }));
115
+ return {
116
+ tracks: playbackTracks(score),
117
+ notes,
118
+ clicks,
119
+ timeline,
120
+ /*
121
+ The tempo map with fermatas written into it as local slowings — see
122
+ `fermataTempoMap`. A pause is expressed as tempo rather than as longer
123
+ notes because the score tick has to stay the playback tick; the caret
124
+ builds its own map from the same function, so the two cannot disagree
125
+ about how long a hold lasts.
126
+
127
+ `TempoMap` satisfies `TempoConversion` structurally, so nothing converts
128
+ twice.
129
+ */
130
+ tempo: new TempoMap(fermataTempoMap(score), score.ppq),
131
+ durationTicks: Math.max(timeline.durationTicks, notes.reduce((n, note) => Math.max(n, note.tick + note.durTicks), 0)),
132
+ };
133
+ }
134
+ //# sourceMappingURL=plan.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plan.js","sourceRoot":"","sources":["../../src/shared/plan.ts"],"names":[],"mappings":"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"}