@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
package/dist/ticker.d.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import { Ticker } from "@motionscript/core";
2
2
  /**
3
- * Ticker backed by a Web Audio AudioContext. Scene time advances in
4
- * lock-step with `audioContext.currentTime`, so any AudioBufferSourceNodes
5
- * scheduled on the same context play perfectly in sync.
3
+ * Ticker backed by a Web Audio AudioContext. Clip time follows
4
+ * `audioContext.currentTime` (within a few ms, see `startLoop`), so any
5
+ * AudioBufferSourceNodes scheduled on the same context play in sync.
6
6
  *
7
7
  * When paused we suspend the context (audio stops, currentTime freezes);
8
8
  * when playing we resume and a rAF loop publishes ticks at the target fps.
@@ -12,29 +12,61 @@ import { Ticker } from "@motionscript/core";
12
12
  * graph to schedule against even on scenes that contain no audio nodes.
13
13
  */
14
14
  export declare class WebTicker extends Ticker {
15
- private context;
16
- private ownsContext;
15
+ private readonly contextOf;
16
+ private readonly ownsContext;
17
+ private _context;
17
18
  private fps;
18
19
  private playStartCtxTime;
19
20
  private seekOffset;
20
21
  private speed;
21
22
  private reverse;
22
23
  private rafId;
24
+ /** Bumped by every `seek`, so the loop can tell that a listener or tick callback moved the playhead under it. */
25
+ private seekEpoch;
23
26
  private silentSource;
27
+ /**
28
+ * `context` may be a getter, called on the first `play`: a session hands
29
+ * over its audio sink's, which is created on first use, so a session that
30
+ * never plays never opens the audio device.
31
+ */
24
32
  constructor(opts: {
25
- context?: AudioContext;
33
+ context?: AudioContext | (() => AudioContext);
26
34
  fps: number;
27
35
  });
28
36
  getContext(): AudioContext;
37
+ private get context();
29
38
  seek(t: number): void;
30
- /** Resumes the (possibly suspended) context, anchors elapsed-time tracking to the current `context.currentTime`, and starts the silent ticker + rAF tick loop. */
31
- play(speed?: number, reverse?: boolean): Promise<void>;
39
+ /**
40
+ * Anchors elapsed-time tracking to the current `context.currentTime`, starts the silent ticker
41
+ * + rAF tick loop and asks a suspended context to resume.
42
+ *
43
+ * Synchronous, as {@link Ticker.play} is: a `pause` or `seek` in the same turn must find a
44
+ * running clock to stop. A suspended context's `currentTime` is frozen, so the clock holds at
45
+ * the anchor until the resume lands — outside a user gesture that is never.
46
+ */
47
+ play(speed?: number, reverse?: boolean): void;
32
48
  /** Stops the tick loop and silent ticker, then suspends the context — freezing `currentTime` so elapsed-time math stays correct across the pause. */
33
49
  pause(): void;
34
50
  /** (Re)starts the silent buffer source covering the remaining timeline — keeps the AudioContext active and gives a stable graph for other AudioBufferSourceNodes to schedule against, per the class-level note. */
35
51
  private startSilentTicker;
36
52
  private stopSilentTicker;
37
- /** Drives ticks off `requestAnimationFrame`, deriving scene time from elapsed `context.currentTime` (so it stays sample-accurate) and only firing `tick()` when a full frame interval has elapsed. */
53
+ /** Where the playhead is now, from elapsed `context.currentTime` (so it stays sample-accurate), and whether that is the end of the run. */
54
+ private position;
55
+ /**
56
+ * Drives ticks off `requestAnimationFrame`, firing `tick()` when the playhead reaches another
57
+ * project frame — and once more at the end of the run, whether or not it landed on a boundary.
58
+ *
59
+ * `context.currentTime` moves in whole render quanta handed over in bursts: at 48 kHz on a
60
+ * 60 Hz display it advances 8, 10.67, 18.67 or 21.33 ms between animation frames, and a
61
+ * playhead read straight off it skipped up to a third of the frames. So the playhead advances
62
+ * by each animation frame's own interval, pulled 5% of the way to the audio clock every frame
63
+ * and snapped to it when they disagree by more than 50 ms (a stall, a seek). It stays within
64
+ * a few ms of the audio and moves one frame per frame.
65
+ *
66
+ * A listener may `seek` (a host looping wraps to 0 when it hears the end), so the end is
67
+ * re-read after the listeners and the tick callbacks have run: a run that was seeked out of
68
+ * its end carries on, and one that was not pauses itself.
69
+ */
38
70
  private startLoop;
39
71
  private stopLoop;
40
72
  dispose(): void;
@@ -1 +1 @@
1
- {"version":3,"file":"ticker.d.ts","sourceRoot":"","sources":["../src/ticker.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAE5C;;;;;;;;;;;GAWG;AACH,qBAAa,SAAU,SAAQ,MAAM;IACjC,OAAO,CAAC,OAAO,CAAe;IAC9B,OAAO,CAAC,WAAW,CAAU;IAC7B,OAAO,CAAC,GAAG,CAAS;IAEpB,OAAO,CAAC,gBAAgB,CAAa;IACrC,OAAO,CAAC,UAAU,CAAa;IAC/B,OAAO,CAAC,KAAK,CAAa;IAC1B,OAAO,CAAC,OAAO,CAAkB;IAEjC,OAAO,CAAC,KAAK,CAAuB;IACpC,OAAO,CAAC,YAAY,CAAsC;gBAE9C,IAAI,EAAE;QAAE,OAAO,CAAC,EAAE,YAAY,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE;IAOzD,UAAU,IAAI,YAAY;IAI1B,IAAI,CAAC,CAAC,EAAE,MAAM,GAAG,IAAI;IAOrB,kKAAkK;IAC5J,IAAI,CAAC,KAAK,GAAE,MAAU,EAAE,OAAO,GAAE,OAAe,GAAG,OAAO,CAAC,IAAI,CAAC;IAetE,qJAAqJ;IACrJ,KAAK,IAAI,IAAI;IAUb,mNAAmN;IACnN,OAAO,CAAC,iBAAiB;IAYzB,OAAO,CAAC,gBAAgB;IAaxB,sMAAsM;IACtM,OAAO,CAAC,SAAS;IA2CjB,OAAO,CAAC,QAAQ;IAOhB,OAAO,IAAI,IAAI;CAQlB"}
1
+ {"version":3,"file":"ticker.d.ts","sourceRoot":"","sources":["../src/ticker.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAE5C;;;;;;;;;;;GAWG;AACH,qBAAa,SAAU,SAAQ,MAAM;IACjC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAqB;IAC/C,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAU;IACtC,OAAO,CAAC,QAAQ,CAA6B;IAC7C,OAAO,CAAC,GAAG,CAAS;IAEpB,OAAO,CAAC,gBAAgB,CAAa;IACrC,OAAO,CAAC,UAAU,CAAa;IAC/B,OAAO,CAAC,KAAK,CAAa;IAC1B,OAAO,CAAC,OAAO,CAAkB;IAEjC,OAAO,CAAC,KAAK,CAAuB;IACpC,iHAAiH;IACjH,OAAO,CAAC,SAAS,CAAa;IAC9B,OAAO,CAAC,YAAY,CAAsC;IAE1D;;;;OAIG;gBACS,IAAI,EAAE;QAAE,OAAO,CAAC,EAAE,YAAY,GAAG,CAAC,MAAM,YAAY,CAAC,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE;IAQhF,UAAU,IAAI,YAAY;IAK1B,OAAO,KAAK,OAAO,GAElB;IAED,IAAI,CAAC,CAAC,EAAE,MAAM,GAAG,IAAI;IASrB;;;;;;;OAOG;IACH,IAAI,CAAC,KAAK,GAAE,MAAU,EAAE,OAAO,GAAE,OAAe,GAAG,IAAI;IAwBvD,qJAAqJ;IACrJ,KAAK,IAAI,IAAI;IAUb,mNAAmN;IACnN,OAAO,CAAC,iBAAiB;IAYzB,OAAO,CAAC,gBAAgB;IAaxB,2IAA2I;IAC3I,OAAO,CAAC,QAAQ;IAShB;;;;;;;;;;;;;;OAcG;IACH,OAAO,CAAC,SAAS;IA6DjB,OAAO,CAAC,QAAQ;IAOhB,OAAO,IAAI,IAAI;CAQlB"}
package/dist/ticker.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import { Ticker } from "@motionscript/core";
2
2
  /**
3
- * Ticker backed by a Web Audio AudioContext. Scene time advances in
4
- * lock-step with `audioContext.currentTime`, so any AudioBufferSourceNodes
5
- * scheduled on the same context play perfectly in sync.
3
+ * Ticker backed by a Web Audio AudioContext. Clip time follows
4
+ * `audioContext.currentTime` (within a few ms, see `startLoop`), so any
5
+ * AudioBufferSourceNodes scheduled on the same context play in sync.
6
6
  *
7
7
  * When paused we suspend the context (audio stops, currentTime freezes);
8
8
  * when playing we resume and a rAF loop publishes ticks at the target fps.
@@ -12,37 +12,67 @@ import { Ticker } from "@motionscript/core";
12
12
  * graph to schedule against even on scenes that contain no audio nodes.
13
13
  */
14
14
  export class WebTicker extends Ticker {
15
- context;
15
+ contextOf;
16
16
  ownsContext;
17
+ _context = null;
17
18
  fps;
18
19
  playStartCtxTime = 0;
19
20
  seekOffset = 0;
20
21
  speed = 1;
21
22
  reverse = false;
22
23
  rafId = null;
24
+ /** Bumped by every `seek`, so the loop can tell that a listener or tick callback moved the playhead under it. */
25
+ seekEpoch = 0;
23
26
  silentSource = null;
27
+ /**
28
+ * `context` may be a getter, called on the first `play`: a session hands
29
+ * over its audio sink's, which is created on first use, so a session that
30
+ * never plays never opens the audio device.
31
+ */
24
32
  constructor(opts) {
25
33
  super();
26
- this.context = opts.context ?? new AudioContext();
27
- this.ownsContext = !opts.context;
34
+ const { context } = opts;
35
+ this.contextOf = typeof context === "function" ? context : () => context ?? new AudioContext();
36
+ this.ownsContext = !context;
28
37
  this.fps = opts.fps;
29
38
  }
30
39
  getContext() {
31
- return this.context;
40
+ this._context ??= this.contextOf();
41
+ return this._context;
42
+ }
43
+ get context() {
44
+ return this.getContext();
32
45
  }
33
46
  seek(t) {
34
47
  const clamped = Math.max(0, Math.min(t, this.duration));
35
48
  this.seekOffset = clamped;
36
- this.playStartCtxTime = this.context.currentTime;
49
+ // The clock is read only while playing; `play` anchors it afresh.
50
+ if (this.isPlaying)
51
+ this.playStartCtxTime = this.context.currentTime;
52
+ this.seekEpoch++;
37
53
  this.setCurrentTime(clamped);
38
54
  }
39
- /** Resumes the (possibly suspended) context, anchors elapsed-time tracking to the current `context.currentTime`, and starts the silent ticker + rAF tick loop. */
40
- async play(speed = 1, reverse = false) {
55
+ /**
56
+ * Anchors elapsed-time tracking to the current `context.currentTime`, starts the silent ticker
57
+ * + rAF tick loop and asks a suspended context to resume.
58
+ *
59
+ * Synchronous, as {@link Ticker.play} is: a `pause` or `seek` in the same turn must find a
60
+ * running clock to stop. A suspended context's `currentTime` is frozen, so the clock holds at
61
+ * the anchor until the resume lands — outside a user gesture that is never.
62
+ */
63
+ play(speed = 1, reverse = false) {
41
64
  this.speed = speed;
42
65
  this.reverse = reverse;
43
66
  this.seekOffset = this.currentTime;
44
67
  if (this.context.state === "suspended") {
45
- await this.context.resume();
68
+ this.context.resume().then(() => {
69
+ // A pause that arrived while the resume was pending could not suspend it.
70
+ if (!this.isPlaying && this.context.state === "running")
71
+ void this.context.suspend();
72
+ }, (error) => {
73
+ console.error("[WebTicker] The audio context could not resume:", error);
74
+ this.pause();
75
+ });
46
76
  }
47
77
  this.playStartCtxTime = this.context.currentTime;
48
78
  this.startSilentTicker();
@@ -54,8 +84,8 @@ export class WebTicker extends Ticker {
54
84
  this.seekOffset = this.currentTime;
55
85
  this.stopLoop();
56
86
  this.stopSilentTicker();
57
- if (this.context.state === "running") {
58
- this.context.suspend();
87
+ if (this._context?.state === "running") {
88
+ this._context.suspend();
59
89
  }
60
90
  super.pause();
61
91
  }
@@ -84,43 +114,85 @@ export class WebTicker extends Ticker {
84
114
  this.silentSource = null;
85
115
  }
86
116
  }
87
- /** Drives ticks off `requestAnimationFrame`, deriving scene time from elapsed `context.currentTime` (so it stays sample-accurate) and only firing `tick()` when a full frame interval has elapsed. */
117
+ /** Where the playhead is now, from elapsed `context.currentTime` (so it stays sample-accurate), and whether that is the end of the run. */
118
+ position() {
119
+ const elapsed = (this.context.currentTime - this.playStartCtxTime) * this.speed;
120
+ const next = this.reverse
121
+ ? this.seekOffset - elapsed
122
+ : this.seekOffset + elapsed;
123
+ const time = Math.max(0, Math.min(next, this.duration));
124
+ return { time, ended: this.reverse ? time <= 0 : time >= this.duration };
125
+ }
126
+ /**
127
+ * Drives ticks off `requestAnimationFrame`, firing `tick()` when the playhead reaches another
128
+ * project frame — and once more at the end of the run, whether or not it landed on a boundary.
129
+ *
130
+ * `context.currentTime` moves in whole render quanta handed over in bursts: at 48 kHz on a
131
+ * 60 Hz display it advances 8, 10.67, 18.67 or 21.33 ms between animation frames, and a
132
+ * playhead read straight off it skipped up to a third of the frames. So the playhead advances
133
+ * by each animation frame's own interval, pulled 5% of the way to the audio clock every frame
134
+ * and snapped to it when they disagree by more than 50 ms (a stall, a seek). It stays within
135
+ * a few ms of the audio and moves one frame per frame.
136
+ *
137
+ * A listener may `seek` (a host looping wraps to 0 when it hears the end), so the end is
138
+ * re-read after the listeners and the tick callbacks have run: a run that was seeked out of
139
+ * its end carries on, and one that was not pauses itself.
140
+ */
88
141
  startLoop() {
89
142
  if (this.rafId !== null)
90
143
  return;
91
- const frameDt = 1 / this.fps;
92
- let lastTickTime = this.currentTime;
93
- const loop = async () => {
94
- const elapsed = (this.context.currentTime - this.playStartCtxTime) * this.speed;
95
- const next = this.reverse
96
- ? this.seekOffset - elapsed
97
- : this.seekOffset + elapsed;
98
- const clamped = Math.max(0, Math.min(next, this.duration));
99
- const ended = this.reverse ? clamped <= 0 : clamped >= this.duration;
100
- // Whether this animation frame has carried the playhead onto a new
101
- // *scene* frame. This loop runs at the display's refresh rate, which
102
- // on a 120 or 144Hz panel is two or three times the rate anything is
103
- // rendered at — so the clock advances every time round (the elapsed
104
- // maths in `play`/`pause` depend on it being exact) while listeners
105
- // hear about it once per frame. The end of the run always reports,
106
- // whether or not it landed on a boundary, or the final position would
107
- // never reach a host. See `Ticker.setCurrentTime`.
108
- const crossed = Math.abs(clamped - lastTickTime) >= frameDt;
109
- this.setCurrentTime(clamped, crossed || ended);
110
- if (crossed) {
111
- lastTickTime = clamped;
112
- // Await so that a slow tick (asset load on a scene
144
+ const frameOf = (t) => Math.floor(t * this.fps + 1e-6);
145
+ let lastFrame = frameOf(this.currentTime);
146
+ let steady = null;
147
+ const steadied = (now) => {
148
+ const audio = this.position();
149
+ let time = audio.time;
150
+ if (!audio.ended && steady && steady.epoch === this.seekEpoch) {
151
+ const step = ((now - steady.at) / 1000) * this.speed;
152
+ const predicted = this.reverse ? steady.time - step : steady.time + step;
153
+ const drift = audio.time - predicted;
154
+ const next = predicted + drift * 0.05;
155
+ if (Math.abs(drift) <= 0.05 && next > 0 && next < this.duration)
156
+ time = next;
157
+ }
158
+ steady = { time, at: now, epoch: this.seekEpoch };
159
+ return { time, ended: audio.ended };
160
+ };
161
+ const loop = async (now) => {
162
+ let epoch = this.seekEpoch;
163
+ const position = steadied(now);
164
+ let ended = position.ended;
165
+ // This loop runs at the display's refresh rate, which on a 120 or 144Hz panel is two
166
+ // or three times the rate anything is rendered at — so the clock advances every time
167
+ // round (the elapsed maths in `play`/`pause` depend on it being exact) while listeners
168
+ // hear about it once per frame. The end of the run always reports, or the final
169
+ // position would never reach a host. See `Ticker.setCurrentTime`.
170
+ const crossed = frameOf(position.time) !== lastFrame;
171
+ const report = crossed || ended;
172
+ this.setCurrentTime(position.time, report);
173
+ const reread = () => {
174
+ if (this.seekEpoch === epoch)
175
+ return;
176
+ epoch = this.seekEpoch;
177
+ lastFrame = frameOf(this.currentTime);
178
+ ended = this.position().ended;
179
+ };
180
+ reread();
181
+ if (report) {
182
+ lastFrame = frameOf(this.currentTime);
183
+ // Await so that a slow tick (asset load on a clip
113
184
  // boundary) doesn't queue a second concurrent tick.
114
185
  await this.tick();
115
- }
116
- if (ended) {
117
- this.rafId = null;
118
- return;
186
+ reread();
119
187
  }
120
188
  // If pause() cleared the rafId while we were awaiting tick(),
121
189
  // do not resume the loop.
122
190
  if (this.rafId === null)
123
191
  return;
192
+ if (ended) {
193
+ this.pause();
194
+ return;
195
+ }
124
196
  this.rafId = requestAnimationFrame(loop);
125
197
  };
126
198
  this.rafId = requestAnimationFrame(loop);
@@ -135,7 +207,7 @@ export class WebTicker extends Ticker {
135
207
  this.stopLoop();
136
208
  this.stopSilentTicker();
137
209
  if (this.ownsContext) {
138
- this.context.close();
210
+ void this._context?.close();
139
211
  }
140
212
  super.dispose();
141
213
  }
@@ -1 +1 @@
1
- {"version":3,"file":"ticker.js","sourceRoot":"","sources":["../src/ticker.ts"],"names":[],"mappings":"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","sourcesContent":["import { Ticker } from \"@motionscript/core\";\n\n/**\n * Ticker backed by a Web Audio AudioContext. Scene time advances in\n * lock-step with `audioContext.currentTime`, so any AudioBufferSourceNodes\n * scheduled on the same context play perfectly in sync.\n *\n * When paused we suspend the context (audio stops, currentTime freezes);\n * when playing we resume and a rAF loop publishes ticks at the target fps.\n *\n * A single silent buffer covering the full timeline is started on play(),\n * which keeps the context active and gives the browser a stable audio\n * graph to schedule against even on scenes that contain no audio nodes.\n */\nexport class WebTicker extends Ticker {\n private context: AudioContext;\n private ownsContext: boolean;\n private fps: number;\n\n private playStartCtxTime: number = 0;\n private seekOffset: number = 0;\n private speed: number = 1;\n private reverse: boolean = false;\n\n private rafId: number | null = null;\n private silentSource: AudioBufferSourceNode | null = null;\n\n constructor(opts: { context?: AudioContext; fps: number }) {\n super();\n this.context = opts.context ?? new AudioContext();\n this.ownsContext = !opts.context;\n this.fps = opts.fps;\n }\n\n getContext(): AudioContext {\n return this.context;\n }\n\n seek(t: number): void {\n const clamped = Math.max(0, Math.min(t, this.duration));\n this.seekOffset = clamped;\n this.playStartCtxTime = this.context.currentTime;\n this.setCurrentTime(clamped);\n }\n\n /** Resumes the (possibly suspended) context, anchors elapsed-time tracking to the current `context.currentTime`, and starts the silent ticker + rAF tick loop. */\n async play(speed: number = 1, reverse: boolean = false): Promise<void> {\n this.speed = speed;\n this.reverse = reverse;\n this.seekOffset = this.currentTime;\n\n if (this.context.state === \"suspended\") {\n await this.context.resume();\n }\n\n this.playStartCtxTime = this.context.currentTime;\n this.startSilentTicker();\n super.play(speed, reverse);\n this.startLoop();\n }\n\n /** Stops the tick loop and silent ticker, then suspends the context — freezing `currentTime` so elapsed-time math stays correct across the pause. */\n pause(): void {\n this.seekOffset = this.currentTime;\n this.stopLoop();\n this.stopSilentTicker();\n if (this.context.state === \"running\") {\n this.context.suspend();\n }\n super.pause();\n }\n\n /** (Re)starts the silent buffer source covering the remaining timeline — keeps the AudioContext active and gives a stable graph for other AudioBufferSourceNodes to schedule against, per the class-level note. */\n private startSilentTicker(): void {\n this.stopSilentTicker();\n const remaining = Math.max(0.001, this.duration - this.seekOffset);\n const sampleRate = this.context.sampleRate;\n const buffer = this.context.createBuffer(1, Math.ceil(remaining * sampleRate), sampleRate);\n const source = this.context.createBufferSource();\n source.buffer = buffer;\n source.connect(this.context.destination);\n source.start();\n this.silentSource = source;\n }\n\n private stopSilentTicker(): void {\n if (this.silentSource) {\n try {\n this.silentSource.onended = null;\n this.silentSource.stop();\n this.silentSource.disconnect();\n } catch {\n // already stopped\n }\n this.silentSource = null;\n }\n }\n\n /** Drives ticks off `requestAnimationFrame`, deriving scene time from elapsed `context.currentTime` (so it stays sample-accurate) and only firing `tick()` when a full frame interval has elapsed. */\n private startLoop(): void {\n if (this.rafId !== null) return;\n const frameDt = 1 / this.fps;\n let lastTickTime = this.currentTime;\n\n const loop = async () => {\n const elapsed = (this.context.currentTime - this.playStartCtxTime) * this.speed;\n const next = this.reverse\n ? this.seekOffset - elapsed\n : this.seekOffset + elapsed;\n const clamped = Math.max(0, Math.min(next, this.duration));\n const ended = this.reverse ? clamped <= 0 : clamped >= this.duration;\n // Whether this animation frame has carried the playhead onto a new\n // *scene* frame. This loop runs at the display's refresh rate, which\n // on a 120 or 144Hz panel is two or three times the rate anything is\n // rendered at — so the clock advances every time round (the elapsed\n // maths in `play`/`pause` depend on it being exact) while listeners\n // hear about it once per frame. The end of the run always reports,\n // whether or not it landed on a boundary, or the final position would\n // never reach a host. See `Ticker.setCurrentTime`.\n const crossed = Math.abs(clamped - lastTickTime) >= frameDt;\n this.setCurrentTime(clamped, crossed || ended);\n\n if (crossed) {\n lastTickTime = clamped;\n // Await so that a slow tick (asset load on a scene\n // boundary) doesn't queue a second concurrent tick.\n await this.tick();\n }\n\n if (ended) {\n this.rafId = null;\n return;\n }\n // If pause() cleared the rafId while we were awaiting tick(),\n // do not resume the loop.\n if (this.rafId === null) return;\n this.rafId = requestAnimationFrame(loop);\n };\n\n this.rafId = requestAnimationFrame(loop);\n }\n\n private stopLoop(): void {\n if (this.rafId !== null) {\n cancelAnimationFrame(this.rafId);\n this.rafId = null;\n }\n }\n\n dispose(): void {\n this.stopLoop();\n this.stopSilentTicker();\n if (this.ownsContext) {\n this.context.close();\n }\n super.dispose();\n }\n}\n"]}
1
+ {"version":3,"file":"ticker.js","sourceRoot":"","sources":["../src/ticker.ts"],"names":[],"mappings":"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","sourcesContent":["import { Ticker } from \"@motionscript/core\";\n\n/**\n * Ticker backed by a Web Audio AudioContext. Clip time follows\n * `audioContext.currentTime` (within a few ms, see `startLoop`), so any\n * AudioBufferSourceNodes scheduled on the same context play in sync.\n *\n * When paused we suspend the context (audio stops, currentTime freezes);\n * when playing we resume and a rAF loop publishes ticks at the target fps.\n *\n * A single silent buffer covering the full timeline is started on play(),\n * which keeps the context active and gives the browser a stable audio\n * graph to schedule against even on scenes that contain no audio nodes.\n */\nexport class WebTicker extends Ticker {\n private readonly contextOf: () => AudioContext;\n private readonly ownsContext: boolean;\n private _context: AudioContext | null = null;\n private fps: number;\n\n private playStartCtxTime: number = 0;\n private seekOffset: number = 0;\n private speed: number = 1;\n private reverse: boolean = false;\n\n private rafId: number | null = null;\n /** Bumped by every `seek`, so the loop can tell that a listener or tick callback moved the playhead under it. */\n private seekEpoch: number = 0;\n private silentSource: AudioBufferSourceNode | null = null;\n\n /**\n * `context` may be a getter, called on the first `play`: a session hands\n * over its audio sink's, which is created on first use, so a session that\n * never plays never opens the audio device.\n */\n constructor(opts: { context?: AudioContext | (() => AudioContext); fps: number }) {\n super();\n const { context } = opts;\n this.contextOf = typeof context === \"function\" ? context : () => context ?? new AudioContext();\n this.ownsContext = !context;\n this.fps = opts.fps;\n }\n\n getContext(): AudioContext {\n this._context ??= this.contextOf();\n return this._context;\n }\n\n private get context(): AudioContext {\n return this.getContext();\n }\n\n seek(t: number): void {\n const clamped = Math.max(0, Math.min(t, this.duration));\n this.seekOffset = clamped;\n // The clock is read only while playing; `play` anchors it afresh.\n if (this.isPlaying) this.playStartCtxTime = this.context.currentTime;\n this.seekEpoch++;\n this.setCurrentTime(clamped);\n }\n\n /**\n * Anchors elapsed-time tracking to the current `context.currentTime`, starts the silent ticker\n * + rAF tick loop and asks a suspended context to resume.\n *\n * Synchronous, as {@link Ticker.play} is: a `pause` or `seek` in the same turn must find a\n * running clock to stop. A suspended context's `currentTime` is frozen, so the clock holds at\n * the anchor until the resume lands — outside a user gesture that is never.\n */\n play(speed: number = 1, reverse: boolean = false): void {\n this.speed = speed;\n this.reverse = reverse;\n this.seekOffset = this.currentTime;\n\n if (this.context.state === \"suspended\") {\n this.context.resume().then(\n () => {\n // A pause that arrived while the resume was pending could not suspend it.\n if (!this.isPlaying && this.context.state === \"running\") void this.context.suspend();\n },\n (error) => {\n console.error(\"[WebTicker] The audio context could not resume:\", error);\n this.pause();\n },\n );\n }\n\n this.playStartCtxTime = this.context.currentTime;\n this.startSilentTicker();\n super.play(speed, reverse);\n this.startLoop();\n }\n\n /** Stops the tick loop and silent ticker, then suspends the context — freezing `currentTime` so elapsed-time math stays correct across the pause. */\n pause(): void {\n this.seekOffset = this.currentTime;\n this.stopLoop();\n this.stopSilentTicker();\n if (this._context?.state === \"running\") {\n this._context.suspend();\n }\n super.pause();\n }\n\n /** (Re)starts the silent buffer source covering the remaining timeline — keeps the AudioContext active and gives a stable graph for other AudioBufferSourceNodes to schedule against, per the class-level note. */\n private startSilentTicker(): void {\n this.stopSilentTicker();\n const remaining = Math.max(0.001, this.duration - this.seekOffset);\n const sampleRate = this.context.sampleRate;\n const buffer = this.context.createBuffer(1, Math.ceil(remaining * sampleRate), sampleRate);\n const source = this.context.createBufferSource();\n source.buffer = buffer;\n source.connect(this.context.destination);\n source.start();\n this.silentSource = source;\n }\n\n private stopSilentTicker(): void {\n if (this.silentSource) {\n try {\n this.silentSource.onended = null;\n this.silentSource.stop();\n this.silentSource.disconnect();\n } catch {\n // already stopped\n }\n this.silentSource = null;\n }\n }\n\n /** Where the playhead is now, from elapsed `context.currentTime` (so it stays sample-accurate), and whether that is the end of the run. */\n private position(): { time: number; ended: boolean } {\n const elapsed = (this.context.currentTime - this.playStartCtxTime) * this.speed;\n const next = this.reverse\n ? this.seekOffset - elapsed\n : this.seekOffset + elapsed;\n const time = Math.max(0, Math.min(next, this.duration));\n return { time, ended: this.reverse ? time <= 0 : time >= this.duration };\n }\n\n /**\n * Drives ticks off `requestAnimationFrame`, firing `tick()` when the playhead reaches another\n * project frame — and once more at the end of the run, whether or not it landed on a boundary.\n *\n * `context.currentTime` moves in whole render quanta handed over in bursts: at 48 kHz on a\n * 60 Hz display it advances 8, 10.67, 18.67 or 21.33 ms between animation frames, and a\n * playhead read straight off it skipped up to a third of the frames. So the playhead advances\n * by each animation frame's own interval, pulled 5% of the way to the audio clock every frame\n * and snapped to it when they disagree by more than 50 ms (a stall, a seek). It stays within\n * a few ms of the audio and moves one frame per frame.\n *\n * A listener may `seek` (a host looping wraps to 0 when it hears the end), so the end is\n * re-read after the listeners and the tick callbacks have run: a run that was seeked out of\n * its end carries on, and one that was not pauses itself.\n */\n private startLoop(): void {\n if (this.rafId !== null) return;\n const frameOf = (t: number) => Math.floor(t * this.fps + 1e-6);\n let lastFrame = frameOf(this.currentTime);\n let steady: { time: number; at: number; epoch: number } | null = null;\n const steadied = (now: number): { time: number; ended: boolean } => {\n const audio = this.position();\n let time = audio.time;\n if (!audio.ended && steady && steady.epoch === this.seekEpoch) {\n const step = ((now - steady.at) / 1000) * this.speed;\n const predicted = this.reverse ? steady.time - step : steady.time + step;\n const drift = audio.time - predicted;\n const next = predicted + drift * 0.05;\n if (Math.abs(drift) <= 0.05 && next > 0 && next < this.duration) time = next;\n }\n steady = { time, at: now, epoch: this.seekEpoch };\n return { time, ended: audio.ended };\n };\n\n const loop = async (now: number) => {\n let epoch = this.seekEpoch;\n const position = steadied(now);\n let ended = position.ended;\n // This loop runs at the display's refresh rate, which on a 120 or 144Hz panel is two\n // or three times the rate anything is rendered at — so the clock advances every time\n // round (the elapsed maths in `play`/`pause` depend on it being exact) while listeners\n // hear about it once per frame. The end of the run always reports, or the final\n // position would never reach a host. See `Ticker.setCurrentTime`.\n const crossed = frameOf(position.time) !== lastFrame;\n const report = crossed || ended;\n this.setCurrentTime(position.time, report);\n\n const reread = () => {\n if (this.seekEpoch === epoch) return;\n epoch = this.seekEpoch;\n lastFrame = frameOf(this.currentTime);\n ended = this.position().ended;\n };\n reread();\n\n if (report) {\n lastFrame = frameOf(this.currentTime);\n // Await so that a slow tick (asset load on a clip\n // boundary) doesn't queue a second concurrent tick.\n await this.tick();\n reread();\n }\n\n // If pause() cleared the rafId while we were awaiting tick(),\n // do not resume the loop.\n if (this.rafId === null) return;\n if (ended) {\n this.pause();\n return;\n }\n this.rafId = requestAnimationFrame(loop);\n };\n\n this.rafId = requestAnimationFrame(loop);\n }\n\n private stopLoop(): void {\n if (this.rafId !== null) {\n cancelAnimationFrame(this.rafId);\n this.rafId = null;\n }\n }\n\n dispose(): void {\n this.stopLoop();\n this.stopSilentTicker();\n if (this.ownsContext) {\n void this._context?.close();\n }\n super.dispose();\n }\n}\n"]}
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@motionscript/browser",
3
- "version": "0.1.0-alpha.0",
3
+ "version": "0.1.0-alpha.10",
4
4
  "type": "module",
5
- "description": "Browser rendering backend for Motion Script, powered by Skia/CanvasKit.",
5
+ "description": "Motion Script in the browser: WebGPU rendering through @motionscript/renderer, video and still export, audio playback.",
6
6
  "repository": {
7
7
  "type": "git",
8
8
  "url": "git+https://github.com/motionscript-dev/motionscript.git",
@@ -15,12 +15,14 @@
15
15
  "sideEffects": false,
16
16
  "keywords": [],
17
17
  "license": "Apache-2.0",
18
+ "author": "Motionscript",
18
19
  "publishConfig": {
19
20
  "access": "public"
20
21
  },
21
22
  "main": "dist/index.js",
22
23
  "types": "dist/index.d.ts",
23
24
  "files": [
25
+ "NOTICE",
24
26
  "dist",
25
27
  "CHANGELOG.md",
26
28
  "!dist/**/*.tsbuildinfo"
@@ -36,11 +38,9 @@
36
38
  }
37
39
  },
38
40
  "peerDependencies": {
39
- "@motionscript/canvaskit": "^0.1.0-alpha.0",
40
- "@motionscript/core": "^0.1.0-alpha.0"
41
+ "@motionscript/core": "^0.1.0-alpha.10"
41
42
  },
42
43
  "devDependencies": {
43
- "@types/three": "0.185.1",
44
44
  "@vitest/browser": "4.1.7",
45
45
  "@vitest/browser-playwright": "^4.1.7",
46
46
  "concurrently": "^9.0.0",
@@ -48,22 +48,23 @@
48
48
  "eslint-import-resolver-typescript": "^4.4.4",
49
49
  "eslint-plugin-import-x": "^4.16.1",
50
50
  "playwright": "^1.60.0",
51
- "three": "0.185.1",
51
+ "rolldown": "1.0.3",
52
52
  "tsc-alias": "^1.8.16",
53
53
  "typescript": "^6.0.3",
54
54
  "typescript-eslint": "^8.48.0",
55
55
  "vitest": "^4.1.5",
56
- "@motionscript/core": "0.1.0-alpha.0",
57
- "@motionscript/canvaskit": "0.1.0-alpha.0"
56
+ "@motionscript/core": "0.1.0-alpha.10"
58
57
  },
59
58
  "dependencies": {
59
+ "@mediabunny/mp3-encoder": "^1.49.0",
60
60
  "esbuild-wasm": "^0.28.1",
61
61
  "mediabunny": "^1.40.1",
62
- "@motionscript/skia-render": "^0.1.0-alpha.0"
62
+ "@motionscript/renderer": "^0.1.0-alpha.10"
63
63
  },
64
64
  "motionscriptBrowser": {
65
65
  "assets": {
66
- "esbuild.wasm": "esbuild-wasm/esbuild.wasm"
66
+ "esbuild.wasm": "esbuild-wasm/esbuild.wasm",
67
+ "ms_renderer_bg.wasm": "@motionscript/renderer/ms_renderer_bg.wasm"
67
68
  }
68
69
  },
69
70
  "scripts": {
@@ -1,58 +0,0 @@
1
- import { AudioFilterItem } from "@motionscript/core";
2
- /**
3
- * Result of building an audio-filter graph for one source.
4
- *
5
- * - `output` is the last node in the chain; connect it downstream (e.g. to the
6
- * per-request gain node).
7
- * - `nodes` are every intermediate node, so callers can `disconnect()` them on
8
- * teardown.
9
- * - `oscillators` are LFOs (from `tremolo`) the caller must `start()` — and may
10
- * `stop()` — alongside the source, since oscillators need an explicit start.
11
- */
12
- export interface AudioFilterGraph {
13
- output: AudioNode;
14
- nodes: AudioNode[];
15
- oscillators: OscillatorNode[];
16
- }
17
- /**
18
- * Builds the Web Audio graph for a clip's filter chain, returning the node to
19
- * connect downstream. Shared by live playback ({@link WebAudioSink}) and
20
- * offline export (`mixAudio`) so both paths sound identical.
21
- *
22
- * Filters apply in array order (index 0 closest to the source). `speed` filters
23
- * are intentionally ignored here — they're realized via `source.playbackRate`
24
- * by the caller (and affect timeline scheduling), not as a graph node.
25
- *
26
- * `startTime` is the AudioContext time the source starts; `clipDuration` is the
27
- * clip's scene-time length; `elapsed` is scene-seconds already played at a mid-clip
28
- * seek (0 for a clean start). These let curve-valued params schedule and align.
29
- *
30
- * Pass the already-created `source` so its `playbackRate` can be set by the
31
- * caller; this function only wires nodes *after* the source.
32
- */
33
- export declare function buildAudioFilterGraph(ctx: BaseAudioContext, source: AudioNode, filters: readonly AudioFilterItem[], startTime?: number, clipDuration?: number, elapsed?: number): AudioFilterGraph;
34
- /**
35
- * Net *constant* playback-rate multiplier from any scalar `speed` filters in the
36
- * chain (product of all), defaulting to 1. Curve-valued speeds contribute 1 here
37
- * and are scheduled separately by {@link applySpeedToPlaybackRate}. Mirrors
38
- * `Sound.effectiveSpeed()` so the renderer applies the same rate the core timeline
39
- * reserved time for.
40
- */
41
- export declare function effectiveSpeed(filters: readonly AudioFilterItem[] | undefined): number;
42
- /** The first curve-valued `speed` filter in the chain, if any. */
43
- export declare function speedCurveOf(filters: readonly AudioFilterItem[] | undefined): import("@motionscript/core").Curve | undefined;
44
- /**
45
- * Schedule a time-varying speed curve onto an `AudioBufferSourceNode.playbackRate`.
46
- *
47
- * The curve is authored over **source time** (rate as a function of how far through
48
- * the source we've played). `setValueAtTime`/`linearRampToValueAtTime` schedule in
49
- * **context time**, so we walk scene-time forward in small steps, convert each to a
50
- * source-time offset via the integral inverse, read the curve there, and lay down a
51
- * value point at the matching context time. `scalar` folds in any constant speed
52
- * filters (they scale the whole rate uniformly).
53
- *
54
- * `elapsed` is scene-seconds already played at a mid-clip seek, so the schedule
55
- * starts at the seek point and aligns the playbackRate to the clip's true position.
56
- */
57
- export declare function applySpeedToPlaybackRate(rate: AudioParam, curve: ReturnType<typeof speedCurveOf>, sourceLength: number, sceneDuration: number, scalar: number, startTime: number, elapsed: number): void;
58
- //# sourceMappingURL=filter-graph.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"filter-graph.d.ts","sourceRoot":"","sources":["../../src/audio/filter-graph.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAA4C,MAAM,oBAAoB,CAAC;AAE/F;;;;;;;;;GASG;AACH,MAAM,WAAW,gBAAgB;IAC7B,MAAM,EAAE,SAAS,CAAC;IAClB,KAAK,EAAE,SAAS,EAAE,CAAC;IACnB,WAAW,EAAE,cAAc,EAAE,CAAC;CACjC;AAyFD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,qBAAqB,CACjC,GAAG,EAAE,gBAAgB,EACrB,MAAM,EAAE,SAAS,EACjB,OAAO,EAAE,SAAS,eAAe,EAAE,EACnC,SAAS,GAAE,MAAwB,EACnC,YAAY,GAAE,MAAU,EACxB,OAAO,GAAE,MAAU,GACpB,gBAAgB,CAwFlB;AAYD;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,SAAS,eAAe,EAAE,GAAG,SAAS,GAAG,MAAM,CAOtF;AAED,kEAAkE;AAClE,wBAAgB,YAAY,CAAC,OAAO,EAAE,SAAS,eAAe,EAAE,GAAG,SAAS,kDAM3E;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,wBAAwB,CACpC,IAAI,EAAE,UAAU,EAChB,KAAK,EAAE,UAAU,CAAC,OAAO,YAAY,CAAC,EACtC,YAAY,EAAE,MAAM,EACpB,aAAa,EAAE,MAAM,EACrB,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,GAChB,IAAI,CAgBN"}