@motionscript/browser 0.1.0-alpha.0 → 0.1.0-alpha.10

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 (181) hide show
  1. package/CHANGELOG.md +187 -0
  2. package/NOTICE +49 -0
  3. package/README.md +96 -24
  4. package/dist/asset-bytes.d.ts +14 -5
  5. package/dist/asset-bytes.d.ts.map +1 -1
  6. package/dist/asset-bytes.js +23 -5
  7. package/dist/asset-bytes.js.map +1 -1
  8. package/dist/audio/bus-graph.d.ts +12 -3
  9. package/dist/audio/bus-graph.d.ts.map +1 -1
  10. package/dist/audio/bus-graph.js +35 -31
  11. package/dist/audio/bus-graph.js.map +1 -1
  12. package/dist/audio/effect-graph.d.ts +62 -0
  13. package/dist/audio/effect-graph.d.ts.map +1 -0
  14. package/dist/audio/effect-graph.js +234 -0
  15. package/dist/audio/effect-graph.js.map +1 -0
  16. package/dist/audio/index.d.ts.map +1 -1
  17. package/dist/audio/index.js +2 -3
  18. package/dist/audio/index.js.map +1 -1
  19. package/dist/audio/loop-region.d.ts +31 -0
  20. package/dist/audio/loop-region.d.ts.map +1 -0
  21. package/dist/audio/loop-region.js +43 -0
  22. package/dist/audio/loop-region.js.map +1 -0
  23. package/dist/audio/mixer.d.ts +4 -4
  24. package/dist/audio/mixer.d.ts.map +1 -1
  25. package/dist/audio/mixer.js +43 -44
  26. package/dist/audio/mixer.js.map +1 -1
  27. package/dist/audio/player.d.ts +15 -9
  28. package/dist/audio/player.d.ts.map +1 -1
  29. package/dist/audio/player.js +60 -69
  30. package/dist/audio/player.js.map +1 -1
  31. package/dist/browser/THIRD_PARTY_LICENSES.txt +784 -0
  32. package/dist/browser/audio.js +1 -1
  33. package/dist/browser/chunks/chunk-F3AZKPHU.js +2 -0
  34. package/dist/browser/chunks/chunk-F3AZKPHU.js.map +7 -0
  35. package/dist/browser/chunks/chunk-KLXMRKTL.js +2 -0
  36. package/dist/browser/chunks/chunk-L5UFQACH.js +163 -0
  37. package/dist/browser/chunks/chunk-L5UFQACH.js.map +7 -0
  38. package/dist/browser/chunks/mediabunny-mp3-encoder-ZCIHVVTJ.js +14 -0
  39. package/dist/browser/chunks/mediabunny-mp3-encoder-ZCIHVVTJ.js.map +7 -0
  40. package/dist/browser/index.js +52 -1490
  41. package/dist/browser/index.js.map +4 -4
  42. package/dist/browser/manifest.json +21 -4
  43. package/dist/browser/ms_renderer_bg.wasm +0 -0
  44. package/dist/concat.d.ts +33 -7
  45. package/dist/concat.d.ts.map +1 -1
  46. package/dist/concat.js +117 -72
  47. package/dist/concat.js.map +1 -1
  48. package/dist/deliver.d.ts +10 -0
  49. package/dist/deliver.d.ts.map +1 -0
  50. package/dist/deliver.js +22 -0
  51. package/dist/deliver.js.map +1 -0
  52. package/dist/dynamic-nodes.d.ts.map +1 -1
  53. package/dist/dynamic-nodes.js +9 -12
  54. package/dist/dynamic-nodes.js.map +1 -1
  55. package/dist/engine.d.ts +43 -15
  56. package/dist/engine.d.ts.map +1 -1
  57. package/dist/engine.js +58 -36
  58. package/dist/engine.js.map +1 -1
  59. package/dist/export-audio.d.ts +36 -0
  60. package/dist/export-audio.d.ts.map +1 -0
  61. package/dist/export-audio.js +56 -0
  62. package/dist/export-audio.js.map +1 -0
  63. package/dist/exporter.d.ts +134 -15
  64. package/dist/exporter.d.ts.map +1 -1
  65. package/dist/exporter.js +135 -90
  66. package/dist/exporter.js.map +1 -1
  67. package/dist/formats.d.ts +36 -0
  68. package/dist/formats.d.ts.map +1 -0
  69. package/dist/formats.js +83 -0
  70. package/dist/formats.js.map +1 -0
  71. package/dist/frame-loop.d.ts +190 -0
  72. package/dist/frame-loop.d.ts.map +1 -0
  73. package/dist/frame-loop.js +304 -0
  74. package/dist/frame-loop.js.map +1 -0
  75. package/dist/index.d.ts +12 -10
  76. package/dist/index.d.ts.map +1 -1
  77. package/dist/index.js +18 -47
  78. package/dist/index.js.map +1 -1
  79. package/dist/mix-timeline-audio.d.ts +7 -7
  80. package/dist/mix-timeline-audio.d.ts.map +1 -1
  81. package/dist/mix-timeline-audio.js +11 -14
  82. package/dist/mix-timeline-audio.js.map +1 -1
  83. package/dist/parallel/export-parallel.d.ts +32 -0
  84. package/dist/parallel/export-parallel.d.ts.map +1 -0
  85. package/dist/parallel/export-parallel.js +114 -0
  86. package/dist/parallel/export-parallel.js.map +1 -0
  87. package/dist/parallel/governor.d.ts +57 -0
  88. package/dist/parallel/governor.d.ts.map +1 -0
  89. package/dist/parallel/governor.js +79 -0
  90. package/dist/parallel/governor.js.map +1 -0
  91. package/dist/parallel/lane.d.ts +45 -0
  92. package/dist/parallel/lane.d.ts.map +1 -0
  93. package/dist/parallel/lane.js +2 -0
  94. package/dist/parallel/lane.js.map +1 -0
  95. package/dist/parallel/lanes.d.ts +20 -0
  96. package/dist/parallel/lanes.d.ts.map +1 -0
  97. package/dist/parallel/lanes.js +152 -0
  98. package/dist/parallel/lanes.js.map +1 -0
  99. package/dist/parallel/plan.d.ts +23 -0
  100. package/dist/parallel/plan.d.ts.map +1 -0
  101. package/dist/parallel/plan.js +30 -0
  102. package/dist/parallel/plan.js.map +1 -0
  103. package/dist/parallel/run-lanes.d.ts +38 -0
  104. package/dist/parallel/run-lanes.d.ts.map +1 -0
  105. package/dist/parallel/run-lanes.js +164 -0
  106. package/dist/parallel/run-lanes.js.map +1 -0
  107. package/dist/parallel/session.d.ts +9 -0
  108. package/dist/parallel/session.d.ts.map +1 -0
  109. package/dist/parallel/session.js +79 -0
  110. package/dist/parallel/session.js.map +1 -0
  111. package/dist/renderer.d.ts +22 -0
  112. package/dist/renderer.d.ts.map +1 -0
  113. package/dist/renderer.js +23 -0
  114. package/dist/renderer.js.map +1 -0
  115. package/dist/screenshot.d.ts +6 -6
  116. package/dist/screenshot.d.ts.map +1 -1
  117. package/dist/screenshot.js +4 -5
  118. package/dist/screenshot.js.map +1 -1
  119. package/dist/still.d.ts +32 -24
  120. package/dist/still.d.ts.map +1 -1
  121. package/dist/still.js +54 -54
  122. package/dist/still.js.map +1 -1
  123. package/dist/ticker.d.ts +41 -9
  124. package/dist/ticker.d.ts.map +1 -1
  125. package/dist/ticker.js +113 -41
  126. package/dist/ticker.js.map +1 -1
  127. package/package.json +11 -10
  128. package/dist/audio/filter-graph.d.ts +0 -58
  129. package/dist/audio/filter-graph.d.ts.map +0 -1
  130. package/dist/audio/filter-graph.js +0 -235
  131. package/dist/audio/filter-graph.js.map +0 -1
  132. package/dist/browser/chunks/GLTFLoader-PGXD4V5B.js +0 -2
  133. package/dist/browser/chunks/GLTFLoader-PGXD4V5B.js.map +0 -7
  134. package/dist/browser/chunks/SkeletonUtils-XYDMTGTM.js +0 -2
  135. package/dist/browser/chunks/chunk-CTLBXZSU.js +0 -4119
  136. package/dist/browser/chunks/chunk-CTLBXZSU.js.map +0 -7
  137. package/dist/browser/chunks/chunk-FQ32PDIU.js +0 -2
  138. package/dist/browser/chunks/chunk-FQ32PDIU.js.map +0 -7
  139. package/dist/browser/chunks/chunk-FY3BTKY4.js +0 -2
  140. package/dist/browser/chunks/chunk-FY3BTKY4.js.map +0 -7
  141. package/dist/browser/chunks/chunk-NSFLRI4P.js +0 -2
  142. package/dist/browser/chunks/chunk-NSFLRI4P.js.map +0 -7
  143. package/dist/browser/chunks/three.module-Z4EOVUWF.js +0 -2
  144. package/dist/browser/chunks/three.module-Z4EOVUWF.js.map +0 -7
  145. package/dist/decode/image-levels.d.ts +0 -137
  146. package/dist/decode/image-levels.d.ts.map +0 -1
  147. package/dist/decode/image-levels.js +0 -477
  148. package/dist/decode/image-levels.js.map +0 -1
  149. package/dist/decode/levels.d.ts +0 -37
  150. package/dist/decode/levels.d.ts.map +0 -1
  151. package/dist/decode/levels.js +0 -68
  152. package/dist/decode/levels.js.map +0 -1
  153. package/dist/decode/video-levels.d.ts +0 -37
  154. package/dist/decode/video-levels.d.ts.map +0 -1
  155. package/dist/decode/video-levels.js +0 -95
  156. package/dist/decode/video-levels.js.map +0 -1
  157. package/dist/getter.d.ts +0 -10
  158. package/dist/getter.d.ts.map +0 -1
  159. package/dist/getter.js +0 -38
  160. package/dist/getter.js.map +0 -1
  161. package/dist/measure-context.d.ts +0 -12
  162. package/dist/measure-context.d.ts.map +0 -1
  163. package/dist/measure-context.js +0 -12
  164. package/dist/measure-context.js.map +0 -1
  165. package/dist/output.d.ts +0 -7
  166. package/dist/output.d.ts.map +0 -1
  167. package/dist/output.js +0 -6
  168. package/dist/output.js.map +0 -1
  169. package/dist/render-context.d.ts +0 -30
  170. package/dist/render-context.d.ts.map +0 -1
  171. package/dist/render-context.js +0 -55
  172. package/dist/render-context.js.map +0 -1
  173. package/dist/storage-adapter.d.ts +0 -367
  174. package/dist/storage-adapter.d.ts.map +0 -1
  175. package/dist/storage-adapter.js +0 -1278
  176. package/dist/storage-adapter.js.map +0 -1
  177. package/dist/three/renderer.d.ts +0 -48
  178. package/dist/three/renderer.d.ts.map +0 -1
  179. package/dist/three/renderer.js +0 -177
  180. package/dist/three/renderer.js.map +0 -1
  181. /package/dist/browser/chunks/{SkeletonUtils-XYDMTGTM.js.map → chunk-KLXMRKTL.js.map} +0 -0
@@ -1,4 +1,5 @@
1
- import { buildAudioFilterGraph } from "./filter-graph";
1
+ import { busStopKey } from "@motionscript/core";
2
+ import { applyTransitions, buildAudioEffectGraph } from "./effect-graph";
2
3
  /**
3
4
  * The live chains for one audio context's buses.
4
5
  *
@@ -25,13 +26,17 @@ export class AudioBusGraph {
25
26
  * each stop after it is that bus's own parent. An empty or absent chain
26
27
  * returns the destination, which is what every clip did before buses
27
28
  * existed.
29
+ *
30
+ * `startTime` is the context time a new chain's schedule starts from, and
31
+ * `clipTime` the same instant in the stops' own seconds. A curve on a stop
32
+ * runs over its `startAt`–`endAt`, so the two are what place it.
28
33
  */
29
- inputFor(bus, startTime) {
34
+ inputFor(bus, startTime, clipTime) {
30
35
  if (!bus || bus.length === 0)
31
36
  return this.destination;
32
- return this.chainFrom(bus, 0, startTime);
37
+ return this.chainFrom(bus, 0, startTime, clipTime);
33
38
  }
34
- /** Disconnect every live bus node. Safe to call more than once. */
39
+ /** Disconnect every live bus node, so the next {@link inputFor} builds afresh. Safe to call more than once. */
35
40
  dispose() {
36
41
  for (const node of this.nodes) {
37
42
  try {
@@ -59,46 +64,45 @@ export class AudioBusGraph {
59
64
  * Recursive rather than iterative because a bus's *output* has to exist
60
65
  * before its own nodes can connect to it, and the outermost bus is the last
61
66
  * entry.
67
+ *
68
+ * Reused by everything that changes what it sounds like, down to the master,
69
+ * not by id alone: a bus edited under a playing device, or one id on two
70
+ * tracks, is a different chain, and one built for the first would play the
71
+ * second through stale levels and a curve anchored to the wrong span.
62
72
  */
63
- chainFrom(bus, index, startTime) {
73
+ chainFrom(bus, index, startTime, clipTime) {
64
74
  if (index >= bus.length)
65
75
  return this.destination;
66
76
  const stop = bus[index];
67
- const existing = this.inputs.get(stop.id);
77
+ const key = bus.slice(index).map(busStopKey).join(">");
78
+ const existing = this.inputs.get(key);
68
79
  if (existing)
69
80
  return existing;
70
- const output = this.chainFrom(bus, index + 1, startTime);
81
+ const output = this.chainFrom(bus, index + 1, startTime, clipTime);
71
82
  // A pass-through head every clip connects to, so the bus has one input
72
- // whatever its filters turn out to be.
83
+ // whatever its effects turn out to be.
73
84
  const input = this.ctx.createGain();
74
85
  input.gain.value = 1;
75
- const level = this.ctx.createGain();
76
- level.gain.value = stop.mute ? 0 : levelOf(stop);
77
- // `clipDuration` is 0 and `elapsed` 0: a bus is not a clip, so a curve on
78
- // one is authored against the timeline rather than against a source's
79
- // length. `applyParam` resolves a curve of its own duration from that.
80
- const graph = buildAudioFilterGraph(this.ctx, input, stop.filters ?? [], startTime, 0, 0);
81
- graph.output.connect(level);
82
- level.connect(output);
83
- this.nodes.push(input, level, ...graph.nodes);
86
+ // The level is a gain after the effects, as a sound's own level is, so a
87
+ // ramp on it is scheduled exactly as one in `effects` is.
88
+ const level = { type: "gain", value: stop.mute ? 0 : stop.volume };
89
+ const span = stop.endAt - stop.startAt;
90
+ const graph = buildAudioEffectGraph(this.ctx, input, [...(stop.effects ?? []), level], startTime, span, clipTime - stop.startAt);
91
+ let tail = graph.output;
92
+ if (stop.transitionIn || stop.transitionOut) {
93
+ const transition = this.ctx.createGain();
94
+ applyTransitions(transition.gain, span, stop.transitionIn, stop.transitionOut, startTime, clipTime - stop.startAt);
95
+ tail.connect(transition);
96
+ this.nodes.push(transition);
97
+ tail = transition;
98
+ }
99
+ tail.connect(output);
100
+ this.nodes.push(input, ...graph.nodes);
84
101
  this.oscillators.push(...graph.oscillators);
85
102
  for (const osc of graph.oscillators)
86
103
  osc.start(startTime);
87
- this.inputs.set(stop.id, input);
104
+ this.inputs.set(key, input);
88
105
  return input;
89
106
  }
90
107
  }
91
- /**
92
- * A bus's gain as a plain number.
93
- *
94
- * A curve-valued bus level is scheduled by the filter chain a bus carries, not
95
- * here: this node is created once for the whole bus and shared by every clip on
96
- * it, so there is no single clip start to resolve a curve against. Authoring a
97
- * bus fade as `filters: AudioFilters.gain(fadeOut(1))` puts it on the chain,
98
- * where it is scheduled against the context clock like any other.
99
- */
100
- function levelOf(stop) {
101
- const value = typeof stop.volume === "number" ? stop.volume : 1;
102
- return Number.isFinite(value) ? value : 1;
103
- }
104
108
  //# sourceMappingURL=bus-graph.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"bus-graph.js","sourceRoot":"","sources":["../../src/audio/bus-graph.ts"],"names":[],"mappings":"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","sourcesContent":["import type { AudioBusStop } from \"@motionscript/core\";\nimport { buildAudioFilterGraph } from \"./filter-graph\";\n\n/**\n * The live chains for one audio context's buses.\n *\n * Held per context rather than per clip, because that is the whole difference\n * between a bus and per-clip processing: every clip naming a bus connects into\n * the *same* nodes, so a reverb on a bus is one tail the group shares rather\n * than one tail each. Flattening the levels into per-clip gain would sound\n * right; flattening the effects would not.\n */\nexport class AudioBusGraph {\n constructor(\n private readonly ctx: BaseAudioContext,\n /** Where the last bus in a chain connects — the master, or the destination. */\n private readonly destination: AudioNode,\n ) { }\n\n /**\n * The input node a clip on `bus` should connect to, building the chain the\n * first time it is asked for.\n *\n * `bus` is source-adjacent first, so `bus[0]` is the one the clip feeds and\n * each stop after it is that bus's own parent. An empty or absent chain\n * returns the destination, which is what every clip did before buses\n * existed.\n */\n inputFor(bus: readonly AudioBusStop[] | undefined, startTime: number): AudioNode {\n if (!bus || bus.length === 0) return this.destination;\n return this.chainFrom(bus, 0, startTime);\n }\n\n /** Disconnect every live bus node. Safe to call more than once. */\n dispose(): void {\n for (const node of this.nodes) {\n try { node.disconnect(); } catch { /* already torn down */ }\n }\n for (const osc of this.oscillators) {\n try { osc.stop(); } catch { /* never started, or already stopped */ }\n }\n this.nodes.length = 0;\n this.oscillators.length = 0;\n this.inputs.clear();\n }\n\n private readonly inputs = new Map<string, AudioNode>();\n private readonly nodes: AudioNode[] = [];\n private readonly oscillators: OscillatorNode[] = [];\n\n /**\n * Build (or reuse) the chain for `bus[index]`, connected to whatever\n * `bus[index + 1]` resolves to.\n *\n * Recursive rather than iterative because a bus's *output* has to exist\n * before its own nodes can connect to it, and the outermost bus is the last\n * entry.\n */\n private chainFrom(bus: readonly AudioBusStop[], index: number, startTime: number): AudioNode {\n if (index >= bus.length) return this.destination;\n\n const stop = bus[index];\n const existing = this.inputs.get(stop.id);\n if (existing) return existing;\n\n const output = this.chainFrom(bus, index + 1, startTime);\n\n // A pass-through head every clip connects to, so the bus has one input\n // whatever its filters turn out to be.\n const input = this.ctx.createGain();\n input.gain.value = 1;\n\n const level = this.ctx.createGain();\n level.gain.value = stop.mute ? 0 : levelOf(stop);\n\n // `clipDuration` is 0 and `elapsed` 0: a bus is not a clip, so a curve on\n // one is authored against the timeline rather than against a source's\n // length. `applyParam` resolves a curve of its own duration from that.\n const graph = buildAudioFilterGraph(this.ctx, input, stop.filters ?? [], startTime, 0, 0);\n graph.output.connect(level);\n level.connect(output);\n\n this.nodes.push(input, level, ...graph.nodes);\n this.oscillators.push(...graph.oscillators);\n for (const osc of graph.oscillators) osc.start(startTime);\n\n this.inputs.set(stop.id, input);\n return input;\n }\n}\n\n/**\n * A bus's gain as a plain number.\n *\n * A curve-valued bus level is scheduled by the filter chain a bus carries, not\n * here: this node is created once for the whole bus and shared by every clip on\n * it, so there is no single clip start to resolve a curve against. Authoring a\n * bus fade as `filters: AudioFilters.gain(fadeOut(1))` puts it on the chain,\n * where it is scheduled against the context clock like any other.\n */\nfunction levelOf(stop: AudioBusStop): number {\n const value = typeof stop.volume === \"number\" ? stop.volume : 1;\n return Number.isFinite(value) ? value : 1;\n}\n"]}
1
+ {"version":3,"file":"bus-graph.js","sourceRoot":"","sources":["../../src/audio/bus-graph.ts"],"names":[],"mappings":"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","sourcesContent":["import { busStopKey, type AudioBusStop, type AudioEffect } from \"@motionscript/core\";\nimport { applyTransitions, buildAudioEffectGraph } from \"./effect-graph\";\n\n/**\n * The live chains for one audio context's buses.\n *\n * Held per context rather than per clip, because that is the whole difference\n * between a bus and per-clip processing: every clip naming a bus connects into\n * the *same* nodes, so a reverb on a bus is one tail the group shares rather\n * than one tail each. Flattening the levels into per-clip gain would sound\n * right; flattening the effects would not.\n */\nexport class AudioBusGraph {\n constructor(\n private readonly ctx: BaseAudioContext,\n /** Where the last bus in a chain connects — the master, or the destination. */\n private readonly destination: AudioNode,\n ) { }\n\n /**\n * The input node a clip on `bus` should connect to, building the chain the\n * first time it is asked for.\n *\n * `bus` is source-adjacent first, so `bus[0]` is the one the clip feeds and\n * each stop after it is that bus's own parent. An empty or absent chain\n * returns the destination, which is what every clip did before buses\n * existed.\n *\n * `startTime` is the context time a new chain's schedule starts from, and\n * `clipTime` the same instant in the stops' own seconds. A curve on a stop\n * runs over its `startAt`–`endAt`, so the two are what place it.\n */\n inputFor(bus: readonly AudioBusStop[] | undefined, startTime: number, clipTime: number): AudioNode {\n if (!bus || bus.length === 0) return this.destination;\n return this.chainFrom(bus, 0, startTime, clipTime);\n }\n\n /** Disconnect every live bus node, so the next {@link inputFor} builds afresh. Safe to call more than once. */\n dispose(): void {\n for (const node of this.nodes) {\n try { node.disconnect(); } catch { /* already torn down */ }\n }\n for (const osc of this.oscillators) {\n try { osc.stop(); } catch { /* never started, or already stopped */ }\n }\n this.nodes.length = 0;\n this.oscillators.length = 0;\n this.inputs.clear();\n }\n\n private readonly inputs = new Map<string, AudioNode>();\n private readonly nodes: AudioNode[] = [];\n private readonly oscillators: OscillatorNode[] = [];\n\n /**\n * Build (or reuse) the chain for `bus[index]`, connected to whatever\n * `bus[index + 1]` resolves to.\n *\n * Recursive rather than iterative because a bus's *output* has to exist\n * before its own nodes can connect to it, and the outermost bus is the last\n * entry.\n *\n * Reused by everything that changes what it sounds like, down to the master,\n * not by id alone: a bus edited under a playing device, or one id on two\n * tracks, is a different chain, and one built for the first would play the\n * second through stale levels and a curve anchored to the wrong span.\n */\n private chainFrom(bus: readonly AudioBusStop[], index: number, startTime: number, clipTime: number): AudioNode {\n if (index >= bus.length) return this.destination;\n\n const stop = bus[index];\n const key = bus.slice(index).map(busStopKey).join(\">\");\n const existing = this.inputs.get(key);\n if (existing) return existing;\n\n const output = this.chainFrom(bus, index + 1, startTime, clipTime);\n\n // A pass-through head every clip connects to, so the bus has one input\n // whatever its effects turn out to be.\n const input = this.ctx.createGain();\n input.gain.value = 1;\n\n // The level is a gain after the effects, as a sound's own level is, so a\n // ramp on it is scheduled exactly as one in `effects` is.\n const level: AudioEffect = { type: \"gain\", value: stop.mute ? 0 : stop.volume };\n const span = stop.endAt - stop.startAt;\n const graph = buildAudioEffectGraph(\n this.ctx, input, [...(stop.effects ?? []), level], startTime, span, clipTime - stop.startAt,\n );\n let tail = graph.output;\n if (stop.transitionIn || stop.transitionOut) {\n const transition = this.ctx.createGain();\n applyTransitions(transition.gain, span, stop.transitionIn, stop.transitionOut, startTime, clipTime - stop.startAt);\n tail.connect(transition);\n this.nodes.push(transition);\n tail = transition;\n }\n tail.connect(output);\n\n this.nodes.push(input, ...graph.nodes);\n this.oscillators.push(...graph.oscillators);\n for (const osc of graph.oscillators) osc.start(startTime);\n\n this.inputs.set(key, input);\n return input;\n }\n}\n"]}
@@ -0,0 +1,62 @@
1
+ import { type AudioEffect, type AudioRequest, type AudioTransition, type Ramp } from "@motionscript/core";
2
+ /**
3
+ * The Web Audio graph for one source's effect chain.
4
+ *
5
+ * `output` is the last node, to connect downstream; `nodes` are every node to
6
+ * `disconnect()` on teardown; `oscillators` are tremolo LFOs the caller starts
7
+ * and stops alongside the source.
8
+ */
9
+ export interface AudioEffectGraph {
10
+ output: AudioNode;
11
+ nodes: AudioNode[];
12
+ oscillators: OscillatorNode[];
13
+ }
14
+ /**
15
+ * Builds the Web Audio graph for a clip's effect chain, after an
16
+ * already-created `source`. Shared by live playback ({@link WebAudioSink}) and
17
+ * offline export (`mixAudio`) so both sound identical.
18
+ *
19
+ * `startTime` is the context time the source starts; `clipDuration` the clip
20
+ * seconds ramps run over (a clip's, or a bus's span); `elapsed` the clip seconds
21
+ * already played at a mid-clip seek (0 for a clean start, negative before the
22
+ * span begins).
23
+ */
24
+ export declare function buildAudioEffectGraph(ctx: BaseAudioContext, source: AudioNode, effects: readonly AudioEffect[], startTime?: number, clipDuration?: number, elapsed?: number): AudioEffectGraph;
25
+ /**
26
+ * Schedule `transitionIn`/`transitionOut` onto a gain over a span `length`
27
+ * seconds long, baked like an eased ramp. Between the two it holds at 1.
28
+ */
29
+ export declare function applyTransitions(gain: AudioParam, length: number, transitionIn: AudioTransition | undefined, transitionOut: AudioTransition | undefined, startTime: number, elapsed: number): void;
30
+ /** How one request plays its source: the rate, and the lengths its ramps and transitions run over. */
31
+ export interface Playback {
32
+ /** The constant `playbackRate`; 1 under a ramp, which is scheduled instead. */
33
+ rate: number;
34
+ speedRamp?: Ramp;
35
+ /** Source seconds a speed ramp is placed on. */
36
+ sourceLength: number;
37
+ /** Clip seconds the volume and effect ramps run over. */
38
+ clipDuration: number;
39
+ /** What the transitions run over: the clip's length, unknown for an open one. */
40
+ transitionLength: number;
41
+ }
42
+ export declare function playbackOf(req: AudioRequest, bufferDuration: number): Playback;
43
+ /** Source seconds after `trimStart` that `clipElapsed` clip seconds have played. */
44
+ export declare function sourceOffsetOf(playback: Playback, clipElapsed: number): number;
45
+ /**
46
+ * A request's level, then its transitions, after `from`: the gains every
47
+ * request plays through on its way to its bus.
48
+ */
49
+ export declare function buildLevel(ctx: BaseAudioContext, from: AudioNode, req: AudioRequest, playback: Playback, startTime: number, elapsed: number): {
50
+ output: AudioNode;
51
+ nodes: AudioNode[];
52
+ };
53
+ /**
54
+ * Schedule a speed ramp onto an `AudioBufferSourceNode.playbackRate`.
55
+ *
56
+ * The ramp runs over **source** seconds and Web Audio schedules in context
57
+ * seconds, so this walks clip time in small steps, converts each to a source
58
+ * offset through the integral inverse, and lays down the rate there. `elapsed`
59
+ * is clip seconds already played at a mid-clip seek.
60
+ */
61
+ export declare function applySpeedToPlaybackRate(rate: AudioParam, speed: Ramp | undefined, sourceLength: number, clipDuration: number, startTime: number, elapsed: number): void;
62
+ //# sourceMappingURL=effect-graph.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"effect-graph.d.ts","sourceRoot":"","sources":["../../src/audio/effect-graph.ts"],"names":[],"mappings":"AAAA,OAAO,EASH,KAAK,WAAW,EAChB,KAAK,YAAY,EACjB,KAAK,eAAe,EAEpB,KAAK,IAAI,EACZ,MAAM,oBAAoB,CAAC;AAE5B;;;;;;GAMG;AACH,MAAM,WAAW,gBAAgB;IAC7B,MAAM,EAAE,SAAS,CAAC;IAClB,KAAK,EAAE,SAAS,EAAE,CAAC;IACnB,WAAW,EAAE,cAAc,EAAE,CAAC;CACjC;AA2ED;;;;;;;;;GASG;AACH,wBAAgB,qBAAqB,CACjC,GAAG,EAAE,gBAAgB,EACrB,MAAM,EAAE,SAAS,EACjB,OAAO,EAAE,SAAS,WAAW,EAAE,EAC/B,SAAS,GAAE,MAAwB,EACnC,YAAY,GAAE,MAAU,EACxB,OAAO,GAAE,MAAU,GACpB,gBAAgB,CAiFlB;AAUD;;;GAGG;AACH,wBAAgB,gBAAgB,CAC5B,IAAI,EAAE,UAAU,EAChB,MAAM,EAAE,MAAM,EACd,YAAY,EAAE,eAAe,GAAG,SAAS,EACzC,aAAa,EAAE,eAAe,GAAG,SAAS,EAC1C,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,GAChB,IAAI,CAoBN;AAED,sGAAsG;AACtG,MAAM,WAAW,QAAQ;IACrB,+EAA+E;IAC/E,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,IAAI,CAAC;IACjB,gDAAgD;IAChD,YAAY,EAAE,MAAM,CAAC;IACrB,yDAAyD;IACzD,YAAY,EAAE,MAAM,CAAC;IACrB,iFAAiF;IACjF,gBAAgB,EAAE,MAAM,CAAC;CAC5B;AAED,wBAAgB,UAAU,CAAC,GAAG,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,GAAG,QAAQ,CAS9E;AAED,oFAAoF;AACpF,wBAAgB,cAAc,CAAC,QAAQ,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,GAAG,MAAM,CAI9E;AAED;;;GAGG;AACH,wBAAgB,UAAU,CACtB,GAAG,EAAE,gBAAgB,EACrB,IAAI,EAAE,SAAS,EACf,GAAG,EAAE,YAAY,EACjB,QAAQ,EAAE,QAAQ,EAClB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,GAChB;IAAE,MAAM,EAAE,SAAS,CAAC;IAAC,KAAK,EAAE,SAAS,EAAE,CAAA;CAAE,CAS3C;AAED;;;;;;;GAOG;AACH,wBAAgB,wBAAwB,CACpC,IAAI,EAAE,UAAU,EAChB,KAAK,EAAE,IAAI,GAAG,SAAS,EACvB,YAAY,EAAE,MAAM,EACpB,YAAY,EAAE,MAAM,EACpB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,GAChB,IAAI,CAaN"}
@@ -0,0 +1,234 @@
1
+ import { clipTimeAtSource, isRamp, resolveRamp, sampleRamp, sourceTimeAtClip, staticValue, transitionGainAt, transitionSpans, } from "@motionscript/core";
2
+ /** Sample points per second an eased segment or a transition is baked into: about every 5 ms. */
3
+ const CURVE_SAMPLES_PER_SECOND = 200;
4
+ /**
5
+ * Apply a {@link Param} to a Web Audio {@link AudioParam}: a number once, a
6
+ * ramp as a schedule — linear segments as `linearRampToValueAtTime`, eased
7
+ * ones baked into `setValueCurveAtTime`.
8
+ *
9
+ * `elapsed` is the clip seconds already played when a mid-clip seek starts the
10
+ * source, so "now" reads the ramp at the clip's current position. `clamp` keeps
11
+ * every scheduled value in a param's valid range.
12
+ */
13
+ function applyParam(audioParam, param, startTime, clipDuration, elapsed, clamp = (v) => v) {
14
+ // The platform throws on a non-finite value.
15
+ const finite = (v) => {
16
+ const c = clamp(v);
17
+ return Number.isFinite(c) ? c : audioParam.value;
18
+ };
19
+ if (!isRamp(param)) {
20
+ audioParam.value = finite(param);
21
+ return;
22
+ }
23
+ if (!Number.isFinite(clipDuration) || clipDuration <= 0) {
24
+ audioParam.value = finite(staticValue(param, audioParam.value));
25
+ return;
26
+ }
27
+ const keys = resolveRamp(param, clipDuration);
28
+ if (keys.length === 0)
29
+ return;
30
+ const base = startTime - elapsed;
31
+ audioParam.setValueAtTime(finite(sampleRamp(param, Math.max(0, elapsed), clipDuration, audioParam.value)), startTime);
32
+ for (let i = 1; i < keys.length; i++) {
33
+ const previous = keys[i - 1];
34
+ const key = keys[i];
35
+ const segStart = base + previous.time;
36
+ const segEnd = base + key.time;
37
+ if (segEnd <= startTime)
38
+ continue;
39
+ const span = key.time - previous.time;
40
+ if (span <= 0) {
41
+ audioParam.setValueAtTime(finite(key.value), Math.max(segEnd, startTime));
42
+ continue;
43
+ }
44
+ // A segment already under way when the source starts carries on from
45
+ // the value set at `startTime` rather than restarting from its own.
46
+ const done = Math.max(0, (startTime - segStart) / span);
47
+ const curveStart = Math.max(segStart, startTime);
48
+ if (key.linear) {
49
+ if (done === 0)
50
+ audioParam.setValueAtTime(finite(previous.value), curveStart);
51
+ audioParam.linearRampToValueAtTime(finite(key.value), segEnd);
52
+ }
53
+ else {
54
+ const curveDur = segEnd - curveStart;
55
+ const count = Math.max(2, Math.ceil(curveDur * CURVE_SAMPLES_PER_SECOND));
56
+ const samples = new Float32Array(count);
57
+ for (let n = 0; n < count; n++) {
58
+ const local = done + (1 - done) * (n / (count - 1));
59
+ samples[n] = finite(previous.value + (key.value - previous.value) * key.ease(local));
60
+ }
61
+ audioParam.setValueCurveAtTime(samples, curveStart, curveDur);
62
+ }
63
+ }
64
+ }
65
+ /**
66
+ * Builds the Web Audio graph for a clip's effect chain, after an
67
+ * already-created `source`. Shared by live playback ({@link WebAudioSink}) and
68
+ * offline export (`mixAudio`) so both sound identical.
69
+ *
70
+ * `startTime` is the context time the source starts; `clipDuration` the clip
71
+ * seconds ramps run over (a clip's, or a bus's span); `elapsed` the clip seconds
72
+ * already played at a mid-clip seek (0 for a clean start, negative before the
73
+ * span begins).
74
+ */
75
+ export function buildAudioEffectGraph(ctx, source, effects, startTime = ctx.currentTime, clipDuration = 0, elapsed = 0) {
76
+ const nodes = [];
77
+ const oscillators = [];
78
+ let cursor = source;
79
+ const series = (node) => {
80
+ cursor.connect(node);
81
+ nodes.push(node);
82
+ cursor = node;
83
+ };
84
+ const apply = (p, v, clamp) => applyParam(p, v, startTime, clipDuration, elapsed, clamp);
85
+ for (const effect of effects) {
86
+ switch (effect.type) {
87
+ case "gain": {
88
+ const gain = ctx.createGain();
89
+ apply(gain.gain, effect.value);
90
+ series(gain);
91
+ break;
92
+ }
93
+ case "highpass":
94
+ case "lowpass": {
95
+ const biquad = ctx.createBiquadFilter();
96
+ biquad.type = effect.type;
97
+ apply(biquad.frequency, effect.frequency);
98
+ if (effect.q !== undefined)
99
+ apply(biquad.Q, effect.q);
100
+ series(biquad);
101
+ break;
102
+ }
103
+ case "tremolo": {
104
+ // A carrier gain an LFO swings: with depth d the carrier sits at
105
+ // 1 - d/2 and the swing is d/2.
106
+ const carrier = ctx.createGain();
107
+ const lfo = ctx.createOscillator();
108
+ const lfoDepth = ctx.createGain();
109
+ apply(carrier.gain, effect.depth, (d) => 1 - clamp01(d) / 2);
110
+ apply(lfoDepth.gain, effect.depth, (d) => clamp01(d) / 2);
111
+ apply(lfo.frequency, effect.rate);
112
+ lfo.connect(lfoDepth);
113
+ lfoDepth.connect(carrier.gain);
114
+ nodes.push(lfoDepth);
115
+ oscillators.push(lfo);
116
+ series(carrier);
117
+ break;
118
+ }
119
+ case "echo": {
120
+ const split = cursor;
121
+ const merge = ctx.createGain();
122
+ const dry = ctx.createGain();
123
+ apply(dry.gain, effect.mix ?? 0.5, (m) => 1 - clamp01(m));
124
+ split.connect(dry);
125
+ dry.connect(merge);
126
+ // The delay line is sized once, so it has to hold the ramp's longest delay.
127
+ const delay = ctx.createDelay(Math.max(1, maxOf(effect.delay) + 1));
128
+ apply(delay.delayTime, effect.delay);
129
+ const feedbackGain = ctx.createGain();
130
+ apply(feedbackGain.gain, effect.feedback, (f) => Math.max(0, Math.min(0.99, f)));
131
+ const wet = ctx.createGain();
132
+ apply(wet.gain, effect.mix ?? 0.5, (m) => clamp01(m));
133
+ split.connect(delay);
134
+ delay.connect(feedbackGain);
135
+ feedbackGain.connect(delay);
136
+ delay.connect(wet);
137
+ wet.connect(merge);
138
+ nodes.push(dry, delay, feedbackGain, wet, merge);
139
+ cursor = merge;
140
+ break;
141
+ }
142
+ }
143
+ }
144
+ return { output: cursor, nodes, oscillators };
145
+ }
146
+ function clamp01(v) {
147
+ return Math.max(0, Math.min(1, v));
148
+ }
149
+ function maxOf(p) {
150
+ return isRamp(p) ? Math.max(0, ...p.keys.map((k) => k.value)) : p;
151
+ }
152
+ /**
153
+ * Schedule `transitionIn`/`transitionOut` onto a gain over a span `length`
154
+ * seconds long, baked like an eased ramp. Between the two it holds at 1.
155
+ */
156
+ export function applyTransitions(gain, length, transitionIn, transitionOut, startTime, elapsed) {
157
+ if (!transitionIn && !transitionOut)
158
+ return;
159
+ const seek = Math.max(0, elapsed);
160
+ const at = (t) => transitionGainAt(t, length, transitionIn, transitionOut);
161
+ gain.setValueAtTime(at(seek), startTime);
162
+ const spans = transitionSpans(length, transitionIn, transitionOut);
163
+ const windows = [];
164
+ if (spans.in > 0)
165
+ windows.push([0, spans.in]);
166
+ if (spans.out > 0 && Number.isFinite(length))
167
+ windows.push([length - spans.out, length]);
168
+ const base = startTime - elapsed;
169
+ for (const [from, to] of windows) {
170
+ const begin = Math.max(from, seek);
171
+ const duration = to - begin;
172
+ if (duration <= 0)
173
+ continue;
174
+ const count = Math.max(2, Math.ceil(duration * CURVE_SAMPLES_PER_SECOND));
175
+ const samples = new Float32Array(count);
176
+ for (let n = 0; n < count; n++)
177
+ samples[n] = at(begin + duration * (n / (count - 1)));
178
+ gain.setValueCurveAtTime(samples, base + begin, duration);
179
+ }
180
+ }
181
+ export function playbackOf(req, bufferDuration) {
182
+ const speed = req.speed ?? 1;
183
+ const speedRamp = isRamp(speed) ? speed : undefined;
184
+ const rate = speedRamp || !(Number.isFinite(speed) && speed > 0) ? 1 : speed;
185
+ // Placed on the same length core sized the request with, not the decoded buffer.
186
+ const sourceLength = req.sourceLength ?? Math.max(0, bufferDuration - req.trimStart);
187
+ const bounded = Number.isFinite(req.endAt);
188
+ const clipDuration = bounded ? Math.max(0, req.endAt - req.startAt) : clipTimeAtSource(speed, sourceLength);
189
+ return { rate, speedRamp, sourceLength, clipDuration, transitionLength: bounded ? clipDuration : Infinity };
190
+ }
191
+ /** Source seconds after `trimStart` that `clipElapsed` clip seconds have played. */
192
+ export function sourceOffsetOf(playback, clipElapsed) {
193
+ return playback.speedRamp
194
+ ? sourceTimeAtClip(playback.speedRamp, playback.sourceLength, clipElapsed)
195
+ : clipElapsed * playback.rate;
196
+ }
197
+ /**
198
+ * A request's level, then its transitions, after `from`: the gains every
199
+ * request plays through on its way to its bus.
200
+ */
201
+ export function buildLevel(ctx, from, req, playback, startTime, elapsed) {
202
+ const level = ctx.createGain();
203
+ applyParam(level.gain, req.volume ?? 1, startTime, playback.clipDuration, elapsed);
204
+ from.connect(level);
205
+ if (!req.transitionIn && !req.transitionOut)
206
+ return { output: level, nodes: [level] };
207
+ const transition = ctx.createGain();
208
+ applyTransitions(transition.gain, playback.transitionLength, req.transitionIn, req.transitionOut, startTime, elapsed);
209
+ level.connect(transition);
210
+ return { output: transition, nodes: [level, transition] };
211
+ }
212
+ /**
213
+ * Schedule a speed ramp onto an `AudioBufferSourceNode.playbackRate`.
214
+ *
215
+ * The ramp runs over **source** seconds and Web Audio schedules in context
216
+ * seconds, so this walks clip time in small steps, converts each to a source
217
+ * offset through the integral inverse, and lays down the rate there. `elapsed`
218
+ * is clip seconds already played at a mid-clip seek.
219
+ */
220
+ export function applySpeedToPlaybackRate(rate, speed, sourceLength, clipDuration, startTime, elapsed) {
221
+ if (!speed)
222
+ return;
223
+ const step = 1 / CURVE_SAMPLES_PER_SECOND;
224
+ const seekAt = Math.max(0, elapsed);
225
+ const valueAt = (clipElapsed) => Math.max(1e-4, sampleRamp(speed, sourceTimeAtClip(speed, sourceLength, clipElapsed), sourceLength, 1));
226
+ rate.setValueAtTime(valueAt(seekAt), startTime);
227
+ for (let s = seekAt + step; s <= clipDuration + step; s += step) {
228
+ const clamped = Math.min(s, clipDuration);
229
+ rate.linearRampToValueAtTime(valueAt(clamped), startTime + (clamped - seekAt));
230
+ if (clamped >= clipDuration)
231
+ break;
232
+ }
233
+ }
234
+ //# sourceMappingURL=effect-graph.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"effect-graph.js","sourceRoot":"","sources":["../../src/audio/effect-graph.ts"],"names":[],"mappings":"AAAA,OAAO,EACH,gBAAgB,EAChB,MAAM,EACN,WAAW,EACX,UAAU,EACV,gBAAgB,EAChB,WAAW,EACX,gBAAgB,EAChB,eAAe,GAMlB,MAAM,oBAAoB,CAAC;AAe5B,iGAAiG;AACjG,MAAM,wBAAwB,GAAG,GAAG,CAAC;AAErC;;;;;;;;GAQG;AACH,SAAS,UAAU,CACf,UAAsB,EACtB,KAAY,EACZ,SAAiB,EACjB,YAAoB,EACpB,OAAe,EACf,QAA+B,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IAEvC,6CAA6C;IAC7C,MAAM,MAAM,GAAG,CAAC,CAAS,EAAU,EAAE;QACjC,MAAM,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;QACnB,OAAO,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,KAAK,CAAC;IACrD,CAAC,CAAC;IAEF,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;QACjB,UAAU,CAAC,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC;QACjC,OAAO;IACX,CAAC;IACD,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,YAAY,CAAC,IAAI,YAAY,IAAI,CAAC,EAAE,CAAC;QACtD,UAAU,CAAC,KAAK,GAAG,MAAM,CAAC,WAAW,CAAC,KAAK,EAAE,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC;QAChE,OAAO;IACX,CAAC;IAED,MAAM,IAAI,GAAG,WAAW,CAAC,KAAK,EAAE,YAAY,CAAC,CAAC;IAC9C,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO;IAC9B,MAAM,IAAI,GAAG,SAAS,GAAG,OAAO,CAAC;IACjC,UAAU,CAAC,cAAc,CAAC,MAAM,CAAC,UAAU,CAAC,KAAK,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,OAAO,CAAC,EAAE,YAAY,EAAE,UAAU,CAAC,KAAK,CAAC,CAAC,EAAE,SAAS,CAAC,CAAC;IAEtH,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACnC,MAAM,QAAQ,GAAG,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;QAC7B,MAAM,GAAG,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;QACpB,MAAM,QAAQ,GAAG,IAAI,GAAG,QAAQ,CAAC,IAAI,CAAC;QACtC,MAAM,MAAM,GAAG,IAAI,GAAG,GAAG,CAAC,IAAI,CAAC;QAC/B,IAAI,MAAM,IAAI,SAAS;YAAE,SAAS;QAElC,MAAM,IAAI,GAAG,GAAG,CAAC,IAAI,GAAG,QAAQ,CAAC,IAAI,CAAC;QACtC,IAAI,IAAI,IAAI,CAAC,EAAE,CAAC;YACZ,UAAU,CAAC,cAAc,CAAC,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC,CAAC;YAC1E,SAAS;QACb,CAAC;QAED,qEAAqE;QACrE,oEAAoE;QACpE,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,SAAS,GAAG,QAAQ,CAAC,GAAG,IAAI,CAAC,CAAC;QACxD,MAAM,UAAU,GAAG,IAAI,CAAC,GAAG,CAAC,QAAQ,EAAE,SAAS,CAAC,CAAC;QACjD,IAAI,GAAG,CAAC,MAAM,EAAE,CAAC;YACb,IAAI,IAAI,KAAK,CAAC;gBAAE,UAAU,CAAC,cAAc,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,UAAU,CAAC,CAAC;YAC9E,UAAU,CAAC,uBAAuB,CAAC,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC,CAAC;QAClE,CAAC;aAAM,CAAC;YACJ,MAAM,QAAQ,GAAG,MAAM,GAAG,UAAU,CAAC;YACrC,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,IAAI,CAAC,QAAQ,GAAG,wBAAwB,CAAC,CAAC,CAAC;YAC1E,MAAM,OAAO,GAAG,IAAI,YAAY,CAAC,KAAK,CAAC,CAAC;YACxC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,EAAE,CAAC,EAAE,EAAE,CAAC;gBAC7B,MAAM,KAAK,GAAG,IAAI,GAAG,CAAC,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC;gBACpD,OAAO,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,QAAQ,CAAC,KAAK,GAAG,CAAC,GAAG,CAAC,KAAK,GAAG,QAAQ,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC;YACzF,CAAC;YACD,UAAU,CAAC,mBAAmB,CAAC,OAAO,EAAE,UAAU,EAAE,QAAQ,CAAC,CAAC;QAClE,CAAC;IACL,CAAC;AACL,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,qBAAqB,CACjC,GAAqB,EACrB,MAAiB,EACjB,OAA+B,EAC/B,YAAoB,GAAG,CAAC,WAAW,EACnC,eAAuB,CAAC,EACxB,UAAkB,CAAC;IAEnB,MAAM,KAAK,GAAgB,EAAE,CAAC;IAC9B,MAAM,WAAW,GAAqB,EAAE,CAAC;IACzC,IAAI,MAAM,GAAc,MAAM,CAAC;IAE/B,MAAM,MAAM,GAAG,CAAC,IAAe,EAAQ,EAAE;QACrC,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;QACrB,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACjB,MAAM,GAAG,IAAI,CAAC;IAClB,CAAC,CAAC;IAEF,MAAM,KAAK,GAAG,CAAC,CAAa,EAAE,CAAQ,EAAE,KAA6B,EAAQ,EAAE,CAC3E,UAAU,CAAC,CAAC,EAAE,CAAC,EAAE,SAAS,EAAE,YAAY,EAAE,OAAO,EAAE,KAAK,CAAC,CAAC;IAE9D,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;QAC3B,QAAQ,MAAM,CAAC,IAAI,EAAE,CAAC;YAClB,KAAK,MAAM,CAAC,CAAC,CAAC;gBACV,MAAM,IAAI,GAAG,GAAG,CAAC,UAAU,EAAE,CAAC;gBAC9B,KAAK,CAAC,IAAI,CAAC,IAAI,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC;gBAC/B,MAAM,CAAC,IAAI,CAAC,CAAC;gBACb,MAAM;YACV,CAAC;YACD,KAAK,UAAU,CAAC;YAChB,KAAK,SAAS,CAAC,CAAC,CAAC;gBACb,MAAM,MAAM,GAAG,GAAG,CAAC,kBAAkB,EAAE,CAAC;gBACxC,MAAM,CAAC,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC;gBAC1B,KAAK,CAAC,MAAM,CAAC,SAAS,EAAE,MAAM,CAAC,SAAS,CAAC,CAAC;gBAC1C,IAAI,MAAM,CAAC,CAAC,KAAK,SAAS;oBAAE,KAAK,CAAC,MAAM,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC;gBACtD,MAAM,CAAC,MAAM,CAAC,CAAC;gBACf,MAAM;YACV,CAAC;YACD,KAAK,SAAS,CAAC,CAAC,CAAC;gBACb,iEAAiE;gBACjE,gCAAgC;gBAChC,MAAM,OAAO,GAAG,GAAG,CAAC,UAAU,EAAE,CAAC;gBACjC,MAAM,GAAG,GAAG,GAAG,CAAC,gBAAgB,EAAE,CAAC;gBACnC,MAAM,QAAQ,GAAG,GAAG,CAAC,UAAU,EAAE,CAAC;gBAElC,KAAK,CAAC,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;gBAC7D,KAAK,CAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;gBAC1D,KAAK,CAAC,GAAG,CAAC,SAAS,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC;gBAElC,GAAG,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;gBACtB,QAAQ,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;gBAE/B,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;gBACrB,WAAW,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;gBACtB,MAAM,CAAC,OAAO,CAAC,CAAC;gBAChB,MAAM;YACV,CAAC;YACD,KAAK,MAAM,CAAC,CAAC,CAAC;gBACV,MAAM,KAAK,GAAG,MAAM,CAAC;gBACrB,MAAM,KAAK,GAAG,GAAG,CAAC,UAAU,EAAE,CAAC;gBAE/B,MAAM,GAAG,GAAG,GAAG,CAAC,UAAU,EAAE,CAAC;gBAC7B,KAAK,CAAC,GAAG,CAAC,IAAI,EAAE,MAAM,CAAC,GAAG,IAAI,GAAG,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC;gBAC1D,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;gBACnB,GAAG,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;gBAEnB,4EAA4E;gBAC5E,MAAM,KAAK,GAAG,GAAG,CAAC,WAAW,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;gBACpE,KAAK,CAAC,KAAK,CAAC,SAAS,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC;gBACrC,MAAM,YAAY,GAAG,GAAG,CAAC,UAAU,EAAE,CAAC;gBACtC,KAAK,CAAC,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,QAAQ,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;gBACjF,MAAM,GAAG,GAAG,GAAG,CAAC,UAAU,EAAE,CAAC;gBAC7B,KAAK,CAAC,GAAG,CAAC,IAAI,EAAE,MAAM,CAAC,GAAG,IAAI,GAAG,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC;gBAEtD,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;gBACrB,KAAK,CAAC,OAAO,CAAC,YAAY,CAAC,CAAC;gBAC5B,YAAY,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;gBAC5B,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;gBACnB,GAAG,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;gBAEnB,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,KAAK,EAAE,YAAY,EAAE,GAAG,EAAE,KAAK,CAAC,CAAC;gBACjD,MAAM,GAAG,KAAK,CAAC;gBACf,MAAM;YACV,CAAC;QACL,CAAC;IACL,CAAC;IAED,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,WAAW,EAAE,CAAC;AAClD,CAAC;AAED,SAAS,OAAO,CAAC,CAAS;IACtB,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;AACvC,CAAC;AAED,SAAS,KAAK,CAAC,CAAQ;IACnB,OAAO,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AACtE,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,gBAAgB,CAC5B,IAAgB,EAChB,MAAc,EACd,YAAyC,EACzC,aAA0C,EAC1C,SAAiB,EACjB,OAAe;IAEf,IAAI,CAAC,YAAY,IAAI,CAAC,aAAa;QAAE,OAAO;IAC5C,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC;IAClC,MAAM,EAAE,GAAG,CAAC,CAAS,EAAU,EAAE,CAAC,gBAAgB,CAAC,CAAC,EAAE,MAAM,EAAE,YAAY,EAAE,aAAa,CAAC,CAAC;IAC3F,IAAI,CAAC,cAAc,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,SAAS,CAAC,CAAC;IAEzC,MAAM,KAAK,GAAG,eAAe,CAAC,MAAM,EAAE,YAAY,EAAE,aAAa,CAAC,CAAC;IACnE,MAAM,OAAO,GAAuB,EAAE,CAAC;IACvC,IAAI,KAAK,CAAC,EAAE,GAAG,CAAC;QAAE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC;IAC9C,IAAI,KAAK,CAAC,GAAG,GAAG,CAAC,IAAI,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC;QAAE,OAAO,CAAC,IAAI,CAAC,CAAC,MAAM,GAAG,KAAK,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC,CAAC;IACzF,MAAM,IAAI,GAAG,SAAS,GAAG,OAAO,CAAC;IACjC,KAAK,MAAM,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,OAAO,EAAE,CAAC;QAC/B,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;QACnC,MAAM,QAAQ,GAAG,EAAE,GAAG,KAAK,CAAC;QAC5B,IAAI,QAAQ,IAAI,CAAC;YAAE,SAAS;QAC5B,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,IAAI,CAAC,QAAQ,GAAG,wBAAwB,CAAC,CAAC,CAAC;QAC1E,MAAM,OAAO,GAAG,IAAI,YAAY,CAAC,KAAK,CAAC,CAAC;QACxC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,EAAE,CAAC,EAAE;YAAE,OAAO,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC,KAAK,GAAG,QAAQ,GAAG,CAAC,CAAC,GAAG,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;QACtF,IAAI,CAAC,mBAAmB,CAAC,OAAO,EAAE,IAAI,GAAG,KAAK,EAAE,QAAQ,CAAC,CAAC;IAC9D,CAAC;AACL,CAAC;AAeD,MAAM,UAAU,UAAU,CAAC,GAAiB,EAAE,cAAsB;IAChE,MAAM,KAAK,GAAG,GAAG,CAAC,KAAK,IAAI,CAAC,CAAC;IAC7B,MAAM,SAAS,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC;IACpD,MAAM,IAAI,GAAG,SAAS,IAAI,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAK,KAAgB,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAE,KAAgB,CAAC;IACrG,iFAAiF;IACjF,MAAM,YAAY,GAAG,GAAG,CAAC,YAAY,IAAI,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,cAAc,GAAG,GAAG,CAAC,SAAS,CAAC,CAAC;IACrF,MAAM,OAAO,GAAG,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;IAC3C,MAAM,YAAY,GAAG,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,GAAG,CAAC,KAAK,GAAG,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,gBAAgB,CAAC,KAAK,EAAE,YAAY,CAAC,CAAC;IAC5G,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,YAAY,EAAE,YAAY,EAAE,gBAAgB,EAAE,OAAO,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC;AAChH,CAAC;AAED,oFAAoF;AACpF,MAAM,UAAU,cAAc,CAAC,QAAkB,EAAE,WAAmB;IAClE,OAAO,QAAQ,CAAC,SAAS;QACrB,CAAC,CAAC,gBAAgB,CAAC,QAAQ,CAAC,SAAS,EAAE,QAAQ,CAAC,YAAY,EAAE,WAAW,CAAC;QAC1E,CAAC,CAAC,WAAW,GAAG,QAAQ,CAAC,IAAI,CAAC;AACtC,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,UAAU,CACtB,GAAqB,EACrB,IAAe,EACf,GAAiB,EACjB,QAAkB,EAClB,SAAiB,EACjB,OAAe;IAEf,MAAM,KAAK,GAAG,GAAG,CAAC,UAAU,EAAE,CAAC;IAC/B,UAAU,CAAC,KAAK,CAAC,IAAI,EAAE,GAAG,CAAC,MAAM,IAAI,CAAC,EAAE,SAAS,EAAE,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC,CAAC;IACnF,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;IACpB,IAAI,CAAC,GAAG,CAAC,YAAY,IAAI,CAAC,GAAG,CAAC,aAAa;QAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,KAAK,CAAC,EAAE,CAAC;IACtF,MAAM,UAAU,GAAG,GAAG,CAAC,UAAU,EAAE,CAAC;IACpC,gBAAgB,CAAC,UAAU,CAAC,IAAI,EAAE,QAAQ,CAAC,gBAAgB,EAAE,GAAG,CAAC,YAAY,EAAE,GAAG,CAAC,aAAa,EAAE,SAAS,EAAE,OAAO,CAAC,CAAC;IACtH,KAAK,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;IAC1B,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,KAAK,EAAE,CAAC,KAAK,EAAE,UAAU,CAAC,EAAE,CAAC;AAC9D,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,wBAAwB,CACpC,IAAgB,EAChB,KAAuB,EACvB,YAAoB,EACpB,YAAoB,EACpB,SAAiB,EACjB,OAAe;IAEf,IAAI,CAAC,KAAK;QAAE,OAAO;IACnB,MAAM,IAAI,GAAG,CAAC,GAAG,wBAAwB,CAAC;IAC1C,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC;IACpC,MAAM,OAAO,GAAG,CAAC,WAAmB,EAAU,EAAE,CAC5C,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,UAAU,CAAC,KAAK,EAAE,gBAAgB,CAAC,KAAK,EAAE,YAAY,EAAE,WAAW,CAAC,EAAE,YAAY,EAAE,CAAC,CAAC,CAAC,CAAC;IAC3G,IAAI,CAAC,cAAc,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,SAAS,CAAC,CAAC;IAEhD,KAAK,IAAI,CAAC,GAAG,MAAM,GAAG,IAAI,EAAE,CAAC,IAAI,YAAY,GAAG,IAAI,EAAE,CAAC,IAAI,IAAI,EAAE,CAAC;QAC9D,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,YAAY,CAAC,CAAC;QAC1C,IAAI,CAAC,uBAAuB,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,SAAS,GAAG,CAAC,OAAO,GAAG,MAAM,CAAC,CAAC,CAAC;QAC/E,IAAI,OAAO,IAAI,YAAY;YAAE,MAAM;IACvC,CAAC;AACL,CAAC","sourcesContent":["import {\n clipTimeAtSource,\n isRamp,\n resolveRamp,\n sampleRamp,\n sourceTimeAtClip,\n staticValue,\n transitionGainAt,\n transitionSpans,\n type AudioEffect,\n type AudioRequest,\n type AudioTransition,\n type Param,\n type Ramp,\n} from \"@motionscript/core\";\n\n/**\n * The Web Audio graph for one source's effect chain.\n *\n * `output` is the last node, to connect downstream; `nodes` are every node to\n * `disconnect()` on teardown; `oscillators` are tremolo LFOs the caller starts\n * and stops alongside the source.\n */\nexport interface AudioEffectGraph {\n output: AudioNode;\n nodes: AudioNode[];\n oscillators: OscillatorNode[];\n}\n\n/** Sample points per second an eased segment or a transition is baked into: about every 5 ms. */\nconst CURVE_SAMPLES_PER_SECOND = 200;\n\n/**\n * Apply a {@link Param} to a Web Audio {@link AudioParam}: a number once, a\n * ramp as a schedule — linear segments as `linearRampToValueAtTime`, eased\n * ones baked into `setValueCurveAtTime`.\n *\n * `elapsed` is the clip seconds already played when a mid-clip seek starts the\n * source, so \"now\" reads the ramp at the clip's current position. `clamp` keeps\n * every scheduled value in a param's valid range.\n */\nfunction applyParam(\n audioParam: AudioParam,\n param: Param,\n startTime: number,\n clipDuration: number,\n elapsed: number,\n clamp: (v: number) => number = (v) => v,\n): void {\n // The platform throws on a non-finite value.\n const finite = (v: number): number => {\n const c = clamp(v);\n return Number.isFinite(c) ? c : audioParam.value;\n };\n\n if (!isRamp(param)) {\n audioParam.value = finite(param);\n return;\n }\n if (!Number.isFinite(clipDuration) || clipDuration <= 0) {\n audioParam.value = finite(staticValue(param, audioParam.value));\n return;\n }\n\n const keys = resolveRamp(param, clipDuration);\n if (keys.length === 0) return;\n const base = startTime - elapsed;\n audioParam.setValueAtTime(finite(sampleRamp(param, Math.max(0, elapsed), clipDuration, audioParam.value)), startTime);\n\n for (let i = 1; i < keys.length; i++) {\n const previous = keys[i - 1];\n const key = keys[i];\n const segStart = base + previous.time;\n const segEnd = base + key.time;\n if (segEnd <= startTime) continue;\n\n const span = key.time - previous.time;\n if (span <= 0) {\n audioParam.setValueAtTime(finite(key.value), Math.max(segEnd, startTime));\n continue;\n }\n\n // A segment already under way when the source starts carries on from\n // the value set at `startTime` rather than restarting from its own.\n const done = Math.max(0, (startTime - segStart) / span);\n const curveStart = Math.max(segStart, startTime);\n if (key.linear) {\n if (done === 0) audioParam.setValueAtTime(finite(previous.value), curveStart);\n audioParam.linearRampToValueAtTime(finite(key.value), segEnd);\n } else {\n const curveDur = segEnd - curveStart;\n const count = Math.max(2, Math.ceil(curveDur * CURVE_SAMPLES_PER_SECOND));\n const samples = new Float32Array(count);\n for (let n = 0; n < count; n++) {\n const local = done + (1 - done) * (n / (count - 1));\n samples[n] = finite(previous.value + (key.value - previous.value) * key.ease(local));\n }\n audioParam.setValueCurveAtTime(samples, curveStart, curveDur);\n }\n }\n}\n\n/**\n * Builds the Web Audio graph for a clip's effect chain, after an\n * already-created `source`. Shared by live playback ({@link WebAudioSink}) and\n * offline export (`mixAudio`) so both sound identical.\n *\n * `startTime` is the context time the source starts; `clipDuration` the clip\n * seconds ramps run over (a clip's, or a bus's span); `elapsed` the clip seconds\n * already played at a mid-clip seek (0 for a clean start, negative before the\n * span begins).\n */\nexport function buildAudioEffectGraph(\n ctx: BaseAudioContext,\n source: AudioNode,\n effects: readonly AudioEffect[],\n startTime: number = ctx.currentTime,\n clipDuration: number = 0,\n elapsed: number = 0,\n): AudioEffectGraph {\n const nodes: AudioNode[] = [];\n const oscillators: OscillatorNode[] = [];\n let cursor: AudioNode = source;\n\n const series = (node: AudioNode): void => {\n cursor.connect(node);\n nodes.push(node);\n cursor = node;\n };\n\n const apply = (p: AudioParam, v: Param, clamp?: (n: number) => number): void =>\n applyParam(p, v, startTime, clipDuration, elapsed, clamp);\n\n for (const effect of effects) {\n switch (effect.type) {\n case \"gain\": {\n const gain = ctx.createGain();\n apply(gain.gain, effect.value);\n series(gain);\n break;\n }\n case \"highpass\":\n case \"lowpass\": {\n const biquad = ctx.createBiquadFilter();\n biquad.type = effect.type;\n apply(biquad.frequency, effect.frequency);\n if (effect.q !== undefined) apply(biquad.Q, effect.q);\n series(biquad);\n break;\n }\n case \"tremolo\": {\n // A carrier gain an LFO swings: with depth d the carrier sits at\n // 1 - d/2 and the swing is d/2.\n const carrier = ctx.createGain();\n const lfo = ctx.createOscillator();\n const lfoDepth = ctx.createGain();\n\n apply(carrier.gain, effect.depth, (d) => 1 - clamp01(d) / 2);\n apply(lfoDepth.gain, effect.depth, (d) => clamp01(d) / 2);\n apply(lfo.frequency, effect.rate);\n\n lfo.connect(lfoDepth);\n lfoDepth.connect(carrier.gain);\n\n nodes.push(lfoDepth);\n oscillators.push(lfo);\n series(carrier);\n break;\n }\n case \"echo\": {\n const split = cursor;\n const merge = ctx.createGain();\n\n const dry = ctx.createGain();\n apply(dry.gain, effect.mix ?? 0.5, (m) => 1 - clamp01(m));\n split.connect(dry);\n dry.connect(merge);\n\n // The delay line is sized once, so it has to hold the ramp's longest delay.\n const delay = ctx.createDelay(Math.max(1, maxOf(effect.delay) + 1));\n apply(delay.delayTime, effect.delay);\n const feedbackGain = ctx.createGain();\n apply(feedbackGain.gain, effect.feedback, (f) => Math.max(0, Math.min(0.99, f)));\n const wet = ctx.createGain();\n apply(wet.gain, effect.mix ?? 0.5, (m) => clamp01(m));\n\n split.connect(delay);\n delay.connect(feedbackGain);\n feedbackGain.connect(delay);\n delay.connect(wet);\n wet.connect(merge);\n\n nodes.push(dry, delay, feedbackGain, wet, merge);\n cursor = merge;\n break;\n }\n }\n }\n\n return { output: cursor, nodes, oscillators };\n}\n\nfunction clamp01(v: number): number {\n return Math.max(0, Math.min(1, v));\n}\n\nfunction maxOf(p: Param): number {\n return isRamp(p) ? Math.max(0, ...p.keys.map((k) => k.value)) : p;\n}\n\n/**\n * Schedule `transitionIn`/`transitionOut` onto a gain over a span `length`\n * seconds long, baked like an eased ramp. Between the two it holds at 1.\n */\nexport function applyTransitions(\n gain: AudioParam,\n length: number,\n transitionIn: AudioTransition | undefined,\n transitionOut: AudioTransition | undefined,\n startTime: number,\n elapsed: number,\n): void {\n if (!transitionIn && !transitionOut) return;\n const seek = Math.max(0, elapsed);\n const at = (t: number): number => transitionGainAt(t, length, transitionIn, transitionOut);\n gain.setValueAtTime(at(seek), startTime);\n\n const spans = transitionSpans(length, transitionIn, transitionOut);\n const windows: [number, number][] = [];\n if (spans.in > 0) windows.push([0, spans.in]);\n if (spans.out > 0 && Number.isFinite(length)) windows.push([length - spans.out, length]);\n const base = startTime - elapsed;\n for (const [from, to] of windows) {\n const begin = Math.max(from, seek);\n const duration = to - begin;\n if (duration <= 0) continue;\n const count = Math.max(2, Math.ceil(duration * CURVE_SAMPLES_PER_SECOND));\n const samples = new Float32Array(count);\n for (let n = 0; n < count; n++) samples[n] = at(begin + duration * (n / (count - 1)));\n gain.setValueCurveAtTime(samples, base + begin, duration);\n }\n}\n\n/** How one request plays its source: the rate, and the lengths its ramps and transitions run over. */\nexport interface Playback {\n /** The constant `playbackRate`; 1 under a ramp, which is scheduled instead. */\n rate: number;\n speedRamp?: Ramp;\n /** Source seconds a speed ramp is placed on. */\n sourceLength: number;\n /** Clip seconds the volume and effect ramps run over. */\n clipDuration: number;\n /** What the transitions run over: the clip's length, unknown for an open one. */\n transitionLength: number;\n}\n\nexport function playbackOf(req: AudioRequest, bufferDuration: number): Playback {\n const speed = req.speed ?? 1;\n const speedRamp = isRamp(speed) ? speed : undefined;\n const rate = speedRamp || !(Number.isFinite(speed) && (speed as number) > 0) ? 1 : (speed as number);\n // Placed on the same length core sized the request with, not the decoded buffer.\n const sourceLength = req.sourceLength ?? Math.max(0, bufferDuration - req.trimStart);\n const bounded = Number.isFinite(req.endAt);\n const clipDuration = bounded ? Math.max(0, req.endAt - req.startAt) : clipTimeAtSource(speed, sourceLength);\n return { rate, speedRamp, sourceLength, clipDuration, transitionLength: bounded ? clipDuration : Infinity };\n}\n\n/** Source seconds after `trimStart` that `clipElapsed` clip seconds have played. */\nexport function sourceOffsetOf(playback: Playback, clipElapsed: number): number {\n return playback.speedRamp\n ? sourceTimeAtClip(playback.speedRamp, playback.sourceLength, clipElapsed)\n : clipElapsed * playback.rate;\n}\n\n/**\n * A request's level, then its transitions, after `from`: the gains every\n * request plays through on its way to its bus.\n */\nexport function buildLevel(\n ctx: BaseAudioContext,\n from: AudioNode,\n req: AudioRequest,\n playback: Playback,\n startTime: number,\n elapsed: number,\n): { output: AudioNode; nodes: AudioNode[] } {\n const level = ctx.createGain();\n applyParam(level.gain, req.volume ?? 1, startTime, playback.clipDuration, elapsed);\n from.connect(level);\n if (!req.transitionIn && !req.transitionOut) return { output: level, nodes: [level] };\n const transition = ctx.createGain();\n applyTransitions(transition.gain, playback.transitionLength, req.transitionIn, req.transitionOut, startTime, elapsed);\n level.connect(transition);\n return { output: transition, nodes: [level, transition] };\n}\n\n/**\n * Schedule a speed ramp onto an `AudioBufferSourceNode.playbackRate`.\n *\n * The ramp runs over **source** seconds and Web Audio schedules in context\n * seconds, so this walks clip time in small steps, converts each to a source\n * offset through the integral inverse, and lays down the rate there. `elapsed`\n * is clip seconds already played at a mid-clip seek.\n */\nexport function applySpeedToPlaybackRate(\n rate: AudioParam,\n speed: Ramp | undefined,\n sourceLength: number,\n clipDuration: number,\n startTime: number,\n elapsed: number,\n): void {\n if (!speed) return;\n const step = 1 / CURVE_SAMPLES_PER_SECOND;\n const seekAt = Math.max(0, elapsed);\n const valueAt = (clipElapsed: number): number =>\n Math.max(1e-4, sampleRamp(speed, sourceTimeAtClip(speed, sourceLength, clipElapsed), sourceLength, 1));\n rate.setValueAtTime(valueAt(seekAt), startTime);\n\n for (let s = seekAt + step; s <= clipDuration + step; s += step) {\n const clamped = Math.min(s, clipDuration);\n rate.linearRampToValueAtTime(valueAt(clamped), startTime + (clamped - seekAt));\n if (clamped >= clipDuration) break;\n }\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/audio/index.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,aAAa,EAAE,mBAAmB,EAAE,MAAM,SAAS,CAAC;AAClF,YAAY,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAE/C,OAAO,EAAE,YAAY,EAAE,YAAY,IAAI,cAAc,EAAE,MAAM,UAAU,CAAC;AAIxE,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/audio/index.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,aAAa,EAAE,mBAAmB,EAAE,MAAM,SAAS,CAAC;AAClF,YAAY,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAE/C,OAAO,EAAE,YAAY,EAAE,YAAY,IAAI,cAAc,EAAE,MAAM,UAAU,CAAC;AAIxE,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC"}
@@ -1,9 +1,8 @@
1
1
  // @motionscript/browser/audio — stacked-audio playback and mixdown, with no
2
2
  // renderer attached.
3
3
  //
4
- // A separate entry point from the package barrel on purpose: that barrel loads
5
- // the CanvasKit/Skia backend at module scope, and nothing here draws anything.
6
- // A host building an audio timeline should not pay for the WASM module.
4
+ // A separate entry point from the package barrel on purpose: nothing here draws,
5
+ // so a host building an audio timeline should not load the renderer.
7
6
  export { mixAudio, encodeWav, WebAudioMixer, DEFAULT_SAMPLE_RATE } from "./mixer";
8
7
  export { WebAudioSink, WebAudioSink as WebAudioPlayer } from "./player";
9
8
  // The bus layer both of those share. Exported so a host writing its own device
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/audio/index.ts"],"names":[],"mappings":"AAAA,4EAA4E;AAC5E,qBAAqB;AACrB,EAAE;AACF,+EAA+E;AAC/E,+EAA+E;AAC/E,wEAAwE;AAExE,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,aAAa,EAAE,mBAAmB,EAAE,MAAM,SAAS,CAAC;AAGlF,OAAO,EAAE,YAAY,EAAE,YAAY,IAAI,cAAc,EAAE,MAAM,UAAU,CAAC;AAExE,+EAA+E;AAC/E,yEAAyE;AACzE,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC","sourcesContent":["// @motionscript/browser/audio — stacked-audio playback and mixdown, with no\n// renderer attached.\n//\n// A separate entry point from the package barrel on purpose: that barrel loads\n// the CanvasKit/Skia backend at module scope, and nothing here draws anything.\n// A host building an audio timeline should not pay for the WASM module.\n\nexport { mixAudio, encodeWav, WebAudioMixer, DEFAULT_SAMPLE_RATE } from \"./mixer\";\nexport type { MixAudioOptions } from \"./mixer\";\n\nexport { WebAudioSink, WebAudioSink as WebAudioPlayer } from \"./player\";\n\n// The bus layer both of those share. Exported so a host writing its own device\n// or mixer builds the same chains, rather than a second set that drifts.\nexport { AudioBusGraph } from \"./bus-graph\";\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/audio/index.ts"],"names":[],"mappings":"AAAA,4EAA4E;AAC5E,qBAAqB;AACrB,EAAE;AACF,iFAAiF;AACjF,qEAAqE;AAErE,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,aAAa,EAAE,mBAAmB,EAAE,MAAM,SAAS,CAAC;AAGlF,OAAO,EAAE,YAAY,EAAE,YAAY,IAAI,cAAc,EAAE,MAAM,UAAU,CAAC;AAExE,+EAA+E;AAC/E,yEAAyE;AACzE,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC","sourcesContent":["// @motionscript/browser/audio — stacked-audio playback and mixdown, with no\n// renderer attached.\n//\n// A separate entry point from the package barrel on purpose: nothing here draws,\n// so a host building an audio timeline should not load the renderer.\n\nexport { mixAudio, encodeWav, WebAudioMixer, DEFAULT_SAMPLE_RATE } from \"./mixer\";\nexport type { MixAudioOptions } from \"./mixer\";\n\nexport { WebAudioSink, WebAudioSink as WebAudioPlayer } from \"./player\";\n\n// The bus layer both of those share. Exported so a host writing its own device\n// or mixer builds the same chains, rather than a second set that drifts.\nexport { AudioBusGraph } from \"./bus-graph\";\n"]}
@@ -0,0 +1,31 @@
1
+ import type { AudioRequest } from "@motionscript/core";
2
+ /** The span of a decoded buffer a looping request repeats, in source seconds. */
3
+ export interface LoopRegion {
4
+ start: number;
5
+ end: number;
6
+ }
7
+ /**
8
+ * The region a looping request repeats: from its in-point to its out-point, or to
9
+ * the end of the buffer when it has none. `null` for a request that does not loop,
10
+ * and for a degenerate region (an out-point at or before the in-point), where the
11
+ * device falls back to wrapping the whole buffer.
12
+ *
13
+ * Shared by the live device and the offline mixer so a preview and an export loop
14
+ * the same samples.
15
+ */
16
+ export declare function loopRegionOf(request: AudioRequest, bufferDuration: number): LoopRegion | null;
17
+ /**
18
+ * Configure `source` to play `request`'s loop: on or off, and over the trimmed
19
+ * region rather than the whole buffer (`loopStart`/`loopEnd` default to the
20
+ * whole buffer, which is how a trim used to be lost).
21
+ */
22
+ export declare function applyLoop(source: AudioBufferSourceNode, request: AudioRequest): LoopRegion | null;
23
+ /**
24
+ * Fold a source offset into a loop region.
25
+ *
26
+ * A seek into the middle of a looping clip lands at `trimStart + elapsed`, which
27
+ * can be many passes past the region's end; the device would not wrap an offset
28
+ * that starts outside the region, so it is wrapped here.
29
+ */
30
+ export declare function wrapIntoRegion(region: LoopRegion | null, offset: number): number;
31
+ //# sourceMappingURL=loop-region.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"loop-region.d.ts","sourceRoot":"","sources":["../../src/audio/loop-region.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAEvD,iFAAiF;AACjF,MAAM,WAAW,UAAU;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;CACf;AAED;;;;;;;;GAQG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI,CAK7F;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,MAAM,EAAE,qBAAqB,EAAE,OAAO,EAAE,YAAY,GAAG,UAAU,GAAG,IAAI,CAQjG;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,UAAU,GAAG,IAAI,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAGhF"}
@@ -0,0 +1,43 @@
1
+ /**
2
+ * The region a looping request repeats: from its in-point to its out-point, or to
3
+ * the end of the buffer when it has none. `null` for a request that does not loop,
4
+ * and for a degenerate region (an out-point at or before the in-point), where the
5
+ * device falls back to wrapping the whole buffer.
6
+ *
7
+ * Shared by the live device and the offline mixer so a preview and an export loop
8
+ * the same samples.
9
+ */
10
+ export function loopRegionOf(request, bufferDuration) {
11
+ if (!request.loop)
12
+ return null;
13
+ const start = Math.min(Math.max(0, request.trimStart), bufferDuration);
14
+ const end = Math.min(request.trimEnd ?? bufferDuration, bufferDuration);
15
+ return end > start ? { start, end } : null;
16
+ }
17
+ /**
18
+ * Configure `source` to play `request`'s loop: on or off, and over the trimmed
19
+ * region rather than the whole buffer (`loopStart`/`loopEnd` default to the
20
+ * whole buffer, which is how a trim used to be lost).
21
+ */
22
+ export function applyLoop(source, request) {
23
+ source.loop = request.loop;
24
+ const region = source.buffer ? loopRegionOf(request, source.buffer.duration) : null;
25
+ if (region) {
26
+ source.loopStart = region.start;
27
+ source.loopEnd = region.end;
28
+ }
29
+ return region;
30
+ }
31
+ /**
32
+ * Fold a source offset into a loop region.
33
+ *
34
+ * A seek into the middle of a looping clip lands at `trimStart + elapsed`, which
35
+ * can be many passes past the region's end; the device would not wrap an offset
36
+ * that starts outside the region, so it is wrapped here.
37
+ */
38
+ export function wrapIntoRegion(region, offset) {
39
+ if (!region || offset < region.end)
40
+ return offset;
41
+ return region.start + ((offset - region.start) % (region.end - region.start));
42
+ }
43
+ //# sourceMappingURL=loop-region.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"loop-region.js","sourceRoot":"","sources":["../../src/audio/loop-region.ts"],"names":[],"mappings":"AAQA;;;;;;;;GAQG;AACH,MAAM,UAAU,YAAY,CAAC,OAAqB,EAAE,cAAsB;IACtE,IAAI,CAAC,OAAO,CAAC,IAAI;QAAE,OAAO,IAAI,CAAC;IAC/B,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,OAAO,CAAC,SAAS,CAAC,EAAE,cAAc,CAAC,CAAC;IACvE,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,OAAO,IAAI,cAAc,EAAE,cAAc,CAAC,CAAC;IACxE,OAAO,GAAG,GAAG,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;AAC/C,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,SAAS,CAAC,MAA6B,EAAE,OAAqB;IAC1E,MAAM,CAAC,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IAC3B,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,YAAY,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IACpF,IAAI,MAAM,EAAE,CAAC;QACT,MAAM,CAAC,SAAS,GAAG,MAAM,CAAC,KAAK,CAAC;QAChC,MAAM,CAAC,OAAO,GAAG,MAAM,CAAC,GAAG,CAAC;IAChC,CAAC;IACD,OAAO,MAAM,CAAC;AAClB,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,cAAc,CAAC,MAAyB,EAAE,MAAc;IACpE,IAAI,CAAC,MAAM,IAAI,MAAM,GAAG,MAAM,CAAC,GAAG;QAAE,OAAO,MAAM,CAAC;IAClD,OAAO,MAAM,CAAC,KAAK,GAAG,CAAC,CAAC,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;AAClF,CAAC","sourcesContent":["import type { AudioRequest } from \"@motionscript/core\";\n\n/** The span of a decoded buffer a looping request repeats, in source seconds. */\nexport interface LoopRegion {\n start: number;\n end: number;\n}\n\n/**\n * The region a looping request repeats: from its in-point to its out-point, or to\n * the end of the buffer when it has none. `null` for a request that does not loop,\n * and for a degenerate region (an out-point at or before the in-point), where the\n * device falls back to wrapping the whole buffer.\n *\n * Shared by the live device and the offline mixer so a preview and an export loop\n * the same samples.\n */\nexport function loopRegionOf(request: AudioRequest, bufferDuration: number): LoopRegion | null {\n if (!request.loop) return null;\n const start = Math.min(Math.max(0, request.trimStart), bufferDuration);\n const end = Math.min(request.trimEnd ?? bufferDuration, bufferDuration);\n return end > start ? { start, end } : null;\n}\n\n/**\n * Configure `source` to play `request`'s loop: on or off, and over the trimmed\n * region rather than the whole buffer (`loopStart`/`loopEnd` default to the\n * whole buffer, which is how a trim used to be lost).\n */\nexport function applyLoop(source: AudioBufferSourceNode, request: AudioRequest): LoopRegion | null {\n source.loop = request.loop;\n const region = source.buffer ? loopRegionOf(request, source.buffer.duration) : null;\n if (region) {\n source.loopStart = region.start;\n source.loopEnd = region.end;\n }\n return region;\n}\n\n/**\n * Fold a source offset into a loop region.\n *\n * A seek into the middle of a looping clip lands at `trimStart + elapsed`, which\n * can be many passes past the region's end; the device would not wrap an offset\n * that starts outside the region, so it is wrapped here.\n */\nexport function wrapIntoRegion(region: LoopRegion | null, offset: number): number {\n if (!region || offset < region.end) return offset;\n return region.start + ((offset - region.start) % (region.end - region.start));\n}\n"]}