@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 @@
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,GAAG,CAAC,CAAC;IACjB,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,IAAI,CAAC,YAAY,GAAG,IAAI,CAAC;QACzB,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,qFAAqF;IACrF,IAAI,YAAY;QACd,OAAO,IAAI,CAAC,YAAY,CAAC;IAC3B,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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"}
@@ -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"}