@motionscript/browser 0.1.0-alpha.7 → 0.1.0-alpha.9
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.
- package/CHANGELOG.md +12 -0
- package/LICENSE +201 -201
- package/NOTICE +49 -48
- package/README.md +141 -118
- package/dist/asset-bytes.d.ts +7 -4
- package/dist/asset-bytes.d.ts.map +1 -1
- package/dist/asset-bytes.js +9 -4
- package/dist/asset-bytes.js.map +1 -1
- package/dist/audio/bus-graph.d.ts.map +1 -1
- package/dist/audio/bus-graph.js +15 -6
- package/dist/audio/bus-graph.js.map +1 -1
- package/dist/audio/effect-graph.d.ts +62 -0
- package/dist/audio/effect-graph.d.ts.map +1 -0
- package/dist/audio/effect-graph.js +234 -0
- package/dist/audio/effect-graph.js.map +1 -0
- package/dist/audio/index.d.ts.map +1 -1
- package/dist/audio/index.js +2 -3
- package/dist/audio/index.js.map +1 -1
- package/dist/audio/loop-region.js.map +1 -1
- package/dist/audio/mixer.d.ts +4 -4
- package/dist/audio/mixer.d.ts.map +1 -1
- package/dist/audio/mixer.js +18 -38
- package/dist/audio/mixer.js.map +1 -1
- package/dist/audio/player.d.ts +12 -6
- package/dist/audio/player.d.ts.map +1 -1
- package/dist/audio/player.js +49 -62
- package/dist/audio/player.js.map +1 -1
- package/dist/browser/THIRD_PARTY_LICENSES.txt +379 -27
- package/dist/browser/audio.js +1 -1
- package/dist/browser/chunks/chunk-F3AZKPHU.js +2 -0
- package/dist/browser/chunks/chunk-F3AZKPHU.js.map +7 -0
- package/dist/browser/chunks/chunk-KLXMRKTL.js +2 -0
- package/dist/browser/chunks/chunk-L5UFQACH.js +163 -0
- package/dist/browser/chunks/chunk-L5UFQACH.js.map +7 -0
- package/dist/browser/chunks/mediabunny-mp3-encoder-ZCIHVVTJ.js +14 -0
- package/dist/browser/chunks/mediabunny-mp3-encoder-ZCIHVVTJ.js.map +7 -0
- package/dist/browser/index.js +51 -1343
- package/dist/browser/index.js.map +4 -4
- package/dist/browser/manifest.json +8 -8
- package/dist/browser/ms_renderer_bg.wasm +0 -0
- package/dist/concat.d.ts +33 -7
- package/dist/concat.d.ts.map +1 -1
- package/dist/concat.js +117 -72
- package/dist/concat.js.map +1 -1
- package/dist/deliver.d.ts +10 -0
- package/dist/deliver.d.ts.map +1 -0
- package/dist/deliver.js +22 -0
- package/dist/deliver.js.map +1 -0
- package/dist/dynamic-nodes.d.ts.map +1 -1
- package/dist/dynamic-nodes.js +2 -3
- package/dist/dynamic-nodes.js.map +1 -1
- package/dist/engine.d.ts +37 -9
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +41 -32
- package/dist/engine.js.map +1 -1
- package/dist/export-audio.d.ts +36 -0
- package/dist/export-audio.d.ts.map +1 -0
- package/dist/export-audio.js +56 -0
- package/dist/export-audio.js.map +1 -0
- package/dist/exporter.d.ts +125 -14
- package/dist/exporter.d.ts.map +1 -1
- package/dist/exporter.js +133 -89
- package/dist/exporter.js.map +1 -1
- package/dist/formats.d.ts +36 -0
- package/dist/formats.d.ts.map +1 -0
- package/dist/formats.js +83 -0
- package/dist/formats.js.map +1 -0
- package/dist/frame-loop.d.ts +190 -0
- package/dist/frame-loop.d.ts.map +1 -0
- package/dist/frame-loop.js +304 -0
- package/dist/frame-loop.js.map +1 -0
- package/dist/index.d.ts +12 -11
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +17 -21
- package/dist/index.js.map +1 -1
- package/dist/mix-timeline-audio.d.ts +5 -5
- package/dist/mix-timeline-audio.d.ts.map +1 -1
- package/dist/mix-timeline-audio.js +10 -12
- package/dist/mix-timeline-audio.js.map +1 -1
- package/dist/parallel/export-parallel.d.ts +32 -0
- package/dist/parallel/export-parallel.d.ts.map +1 -0
- package/dist/parallel/export-parallel.js +114 -0
- package/dist/parallel/export-parallel.js.map +1 -0
- package/dist/parallel/governor.d.ts +57 -0
- package/dist/parallel/governor.d.ts.map +1 -0
- package/dist/parallel/governor.js +79 -0
- package/dist/parallel/governor.js.map +1 -0
- package/dist/parallel/lane.d.ts +45 -0
- package/dist/parallel/lane.d.ts.map +1 -0
- package/dist/parallel/lane.js +2 -0
- package/dist/parallel/lane.js.map +1 -0
- package/dist/parallel/lanes.d.ts +20 -0
- package/dist/parallel/lanes.d.ts.map +1 -0
- package/dist/parallel/lanes.js +152 -0
- package/dist/parallel/lanes.js.map +1 -0
- package/dist/parallel/plan.d.ts +23 -0
- package/dist/parallel/plan.d.ts.map +1 -0
- package/dist/parallel/plan.js +30 -0
- package/dist/parallel/plan.js.map +1 -0
- package/dist/parallel/run-lanes.d.ts +38 -0
- package/dist/parallel/run-lanes.d.ts.map +1 -0
- package/dist/parallel/run-lanes.js +164 -0
- package/dist/parallel/run-lanes.js.map +1 -0
- package/dist/parallel/session.d.ts +9 -0
- package/dist/parallel/session.d.ts.map +1 -0
- package/dist/parallel/session.js +79 -0
- package/dist/parallel/session.js.map +1 -0
- package/dist/renderer.d.ts +22 -0
- package/dist/renderer.d.ts.map +1 -0
- package/dist/renderer.js +23 -0
- package/dist/renderer.js.map +1 -0
- package/dist/screenshot.d.ts +4 -4
- package/dist/screenshot.d.ts.map +1 -1
- package/dist/screenshot.js +3 -3
- package/dist/screenshot.js.map +1 -1
- package/dist/still.d.ts +17 -19
- package/dist/still.d.ts.map +1 -1
- package/dist/still.js +27 -34
- package/dist/still.js.map +1 -1
- package/dist/ticker.d.ts +22 -8
- package/dist/ticker.d.ts.map +1 -1
- package/dist/ticker.js +55 -20
- package/dist/ticker.js.map +1 -1
- package/package.json +9 -10
- package/dist/audio/filter-graph.d.ts +0 -59
- package/dist/audio/filter-graph.d.ts.map +0 -1
- package/dist/audio/filter-graph.js +0 -241
- package/dist/audio/filter-graph.js.map +0 -1
- package/dist/browser/chunks/GLTFLoader-PGXD4V5B.js +0 -2
- package/dist/browser/chunks/GLTFLoader-PGXD4V5B.js.map +0 -7
- package/dist/browser/chunks/SkeletonUtils-XYDMTGTM.js +0 -2
- package/dist/browser/chunks/chunk-2MAFJWBZ.js +0 -2
- package/dist/browser/chunks/chunk-2MAFJWBZ.js.map +0 -7
- package/dist/browser/chunks/chunk-CTLBXZSU.js +0 -4119
- package/dist/browser/chunks/chunk-CTLBXZSU.js.map +0 -7
- package/dist/browser/chunks/chunk-FQ32PDIU.js +0 -2
- package/dist/browser/chunks/chunk-FQ32PDIU.js.map +0 -7
- package/dist/browser/chunks/chunk-FY3BTKY4.js +0 -2
- package/dist/browser/chunks/chunk-FY3BTKY4.js.map +0 -7
- package/dist/browser/chunks/three.module-Z4EOVUWF.js +0 -2
- package/dist/browser/chunks/three.module-Z4EOVUWF.js.map +0 -7
- package/dist/decode/image-levels.d.ts +0 -137
- package/dist/decode/image-levels.d.ts.map +0 -1
- package/dist/decode/image-levels.js +0 -477
- package/dist/decode/image-levels.js.map +0 -1
- package/dist/decode/levels.d.ts +0 -37
- package/dist/decode/levels.d.ts.map +0 -1
- package/dist/decode/levels.js +0 -68
- package/dist/decode/levels.js.map +0 -1
- package/dist/decode/video-levels.d.ts +0 -37
- package/dist/decode/video-levels.d.ts.map +0 -1
- package/dist/decode/video-levels.js +0 -95
- package/dist/decode/video-levels.js.map +0 -1
- package/dist/getter.d.ts +0 -10
- package/dist/getter.d.ts.map +0 -1
- package/dist/getter.js +0 -38
- package/dist/getter.js.map +0 -1
- package/dist/measure-context.d.ts +0 -12
- package/dist/measure-context.d.ts.map +0 -1
- package/dist/measure-context.js +0 -12
- package/dist/measure-context.js.map +0 -1
- package/dist/render-context.d.ts +0 -32
- package/dist/render-context.d.ts.map +0 -1
- package/dist/render-context.js +0 -59
- package/dist/render-context.js.map +0 -1
- package/dist/storage-adapter.d.ts +0 -367
- package/dist/storage-adapter.d.ts.map +0 -1
- package/dist/storage-adapter.js +0 -1280
- package/dist/storage-adapter.js.map +0 -1
- package/dist/three/renderer.d.ts +0 -47
- package/dist/three/renderer.d.ts.map +0 -1
- package/dist/three/renderer.js +0 -182
- package/dist/three/renderer.js.map +0 -1
- /package/dist/browser/chunks/{SkeletonUtils-XYDMTGTM.js.map → chunk-KLXMRKTL.js.map} +0 -0
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. Clip time
|
|
4
|
-
*
|
|
5
|
-
* scheduled on the same context play
|
|
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,8 +12,9 @@ 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
|
-
|
|
15
|
+
contextOf;
|
|
16
16
|
ownsContext;
|
|
17
|
+
_context = null;
|
|
17
18
|
fps;
|
|
18
19
|
playStartCtxTime = 0;
|
|
19
20
|
seekOffset = 0;
|
|
@@ -23,19 +24,31 @@ export class WebTicker extends Ticker {
|
|
|
23
24
|
/** Bumped by every `seek`, so the loop can tell that a listener or tick callback moved the playhead under it. */
|
|
24
25
|
seekEpoch = 0;
|
|
25
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
|
+
*/
|
|
26
32
|
constructor(opts) {
|
|
27
33
|
super();
|
|
28
|
-
|
|
29
|
-
this.
|
|
34
|
+
const { context } = opts;
|
|
35
|
+
this.contextOf = typeof context === "function" ? context : () => context ?? new AudioContext();
|
|
36
|
+
this.ownsContext = !context;
|
|
30
37
|
this.fps = opts.fps;
|
|
31
38
|
}
|
|
32
39
|
getContext() {
|
|
33
|
-
|
|
40
|
+
this._context ??= this.contextOf();
|
|
41
|
+
return this._context;
|
|
42
|
+
}
|
|
43
|
+
get context() {
|
|
44
|
+
return this.getContext();
|
|
34
45
|
}
|
|
35
46
|
seek(t) {
|
|
36
47
|
const clamped = Math.max(0, Math.min(t, this.duration));
|
|
37
48
|
this.seekOffset = clamped;
|
|
38
|
-
|
|
49
|
+
// The clock is read only while playing; `play` anchors it afresh.
|
|
50
|
+
if (this.isPlaying)
|
|
51
|
+
this.playStartCtxTime = this.context.currentTime;
|
|
39
52
|
this.seekEpoch++;
|
|
40
53
|
this.setCurrentTime(clamped);
|
|
41
54
|
}
|
|
@@ -71,8 +84,8 @@ export class WebTicker extends Ticker {
|
|
|
71
84
|
this.seekOffset = this.currentTime;
|
|
72
85
|
this.stopLoop();
|
|
73
86
|
this.stopSilentTicker();
|
|
74
|
-
if (this.
|
|
75
|
-
this.
|
|
87
|
+
if (this._context?.state === "running") {
|
|
88
|
+
this._context.suspend();
|
|
76
89
|
}
|
|
77
90
|
super.pause();
|
|
78
91
|
}
|
|
@@ -111,8 +124,15 @@ export class WebTicker extends Ticker {
|
|
|
111
124
|
return { time, ended: this.reverse ? time <= 0 : time >= this.duration };
|
|
112
125
|
}
|
|
113
126
|
/**
|
|
114
|
-
* Drives ticks off `requestAnimationFrame`, firing `tick()`
|
|
115
|
-
*
|
|
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.
|
|
116
136
|
*
|
|
117
137
|
* A listener may `seek` (a host looping wraps to 0 when it hears the end), so the end is
|
|
118
138
|
* re-read after the listeners and the tick callbacks have run: a run that was seeked out of
|
|
@@ -121,30 +141,45 @@ export class WebTicker extends Ticker {
|
|
|
121
141
|
startLoop() {
|
|
122
142
|
if (this.rafId !== null)
|
|
123
143
|
return;
|
|
124
|
-
const
|
|
125
|
-
let
|
|
126
|
-
|
|
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) => {
|
|
127
162
|
let epoch = this.seekEpoch;
|
|
128
|
-
const position =
|
|
163
|
+
const position = steadied(now);
|
|
129
164
|
let ended = position.ended;
|
|
130
165
|
// This loop runs at the display's refresh rate, which on a 120 or 144Hz panel is two
|
|
131
166
|
// or three times the rate anything is rendered at — so the clock advances every time
|
|
132
167
|
// round (the elapsed maths in `play`/`pause` depend on it being exact) while listeners
|
|
133
168
|
// hear about it once per frame. The end of the run always reports, or the final
|
|
134
169
|
// position would never reach a host. See `Ticker.setCurrentTime`.
|
|
135
|
-
const crossed =
|
|
170
|
+
const crossed = frameOf(position.time) !== lastFrame;
|
|
136
171
|
const report = crossed || ended;
|
|
137
172
|
this.setCurrentTime(position.time, report);
|
|
138
173
|
const reread = () => {
|
|
139
174
|
if (this.seekEpoch === epoch)
|
|
140
175
|
return;
|
|
141
176
|
epoch = this.seekEpoch;
|
|
142
|
-
|
|
177
|
+
lastFrame = frameOf(this.currentTime);
|
|
143
178
|
ended = this.position().ended;
|
|
144
179
|
};
|
|
145
180
|
reread();
|
|
146
181
|
if (report) {
|
|
147
|
-
|
|
182
|
+
lastFrame = frameOf(this.currentTime);
|
|
148
183
|
// Await so that a slow tick (asset load on a clip
|
|
149
184
|
// boundary) doesn't queue a second concurrent tick.
|
|
150
185
|
await this.tick();
|
|
@@ -172,7 +207,7 @@ export class WebTicker extends Ticker {
|
|
|
172
207
|
this.stopLoop();
|
|
173
208
|
this.stopSilentTicker();
|
|
174
209
|
if (this.ownsContext) {
|
|
175
|
-
this.
|
|
210
|
+
void this._context?.close();
|
|
176
211
|
}
|
|
177
212
|
super.dispose();
|
|
178
213
|
}
|
package/dist/ticker.js.map
CHANGED
|
@@ -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\";\r\n\r\n/**\r\n * Ticker backed by a Web Audio AudioContext. Clip time advances in\r\n * lock-step with `audioContext.currentTime`, so any AudioBufferSourceNodes\r\n * scheduled on the same context play perfectly in sync.\r\n *\r\n * When paused we suspend the context (audio stops, currentTime freezes);\r\n * when playing we resume and a rAF loop publishes ticks at the target fps.\r\n *\r\n * A single silent buffer covering the full timeline is started on play(),\r\n * which keeps the context active and gives the browser a stable audio\r\n * graph to schedule against even on scenes that contain no audio nodes.\r\n */\r\nexport class WebTicker extends Ticker {\r\n private context: AudioContext;\r\n private ownsContext: boolean;\r\n private fps: number;\r\n\r\n private playStartCtxTime: number = 0;\r\n private seekOffset: number = 0;\r\n private speed: number = 1;\r\n private reverse: boolean = false;\r\n\r\n private rafId: number | null = null;\r\n /** Bumped by every `seek`, so the loop can tell that a listener or tick callback moved the playhead under it. */\r\n private seekEpoch: number = 0;\r\n private silentSource: AudioBufferSourceNode | null = null;\r\n\r\n constructor(opts: { context?: AudioContext; fps: number }) {\r\n super();\r\n this.context = opts.context ?? new AudioContext();\r\n this.ownsContext = !opts.context;\r\n this.fps = opts.fps;\r\n }\r\n\r\n getContext(): AudioContext {\r\n return this.context;\r\n }\r\n\r\n seek(t: number): void {\r\n const clamped = Math.max(0, Math.min(t, this.duration));\r\n this.seekOffset = clamped;\r\n this.playStartCtxTime = this.context.currentTime;\r\n this.seekEpoch++;\r\n this.setCurrentTime(clamped);\r\n }\r\n\r\n /**\r\n * Anchors elapsed-time tracking to the current `context.currentTime`, starts the silent ticker\r\n * + rAF tick loop and asks a suspended context to resume.\r\n *\r\n * Synchronous, as {@link Ticker.play} is: a `pause` or `seek` in the same turn must find a\r\n * running clock to stop. A suspended context's `currentTime` is frozen, so the clock holds at\r\n * the anchor until the resume lands — outside a user gesture that is never.\r\n */\r\n play(speed: number = 1, reverse: boolean = false): void {\r\n this.speed = speed;\r\n this.reverse = reverse;\r\n this.seekOffset = this.currentTime;\r\n\r\n if (this.context.state === \"suspended\") {\r\n this.context.resume().then(\r\n () => {\r\n // A pause that arrived while the resume was pending could not suspend it.\r\n if (!this.isPlaying && this.context.state === \"running\") void this.context.suspend();\r\n },\r\n (error) => {\r\n console.error(\"[WebTicker] The audio context could not resume:\", error);\r\n this.pause();\r\n },\r\n );\r\n }\r\n\r\n this.playStartCtxTime = this.context.currentTime;\r\n this.startSilentTicker();\r\n super.play(speed, reverse);\r\n this.startLoop();\r\n }\r\n\r\n /** Stops the tick loop and silent ticker, then suspends the context — freezing `currentTime` so elapsed-time math stays correct across the pause. */\r\n pause(): void {\r\n this.seekOffset = this.currentTime;\r\n this.stopLoop();\r\n this.stopSilentTicker();\r\n if (this.context.state === \"running\") {\r\n this.context.suspend();\r\n }\r\n super.pause();\r\n }\r\n\r\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. */\r\n private startSilentTicker(): void {\r\n this.stopSilentTicker();\r\n const remaining = Math.max(0.001, this.duration - this.seekOffset);\r\n const sampleRate = this.context.sampleRate;\r\n const buffer = this.context.createBuffer(1, Math.ceil(remaining * sampleRate), sampleRate);\r\n const source = this.context.createBufferSource();\r\n source.buffer = buffer;\r\n source.connect(this.context.destination);\r\n source.start();\r\n this.silentSource = source;\r\n }\r\n\r\n private stopSilentTicker(): void {\r\n if (this.silentSource) {\r\n try {\r\n this.silentSource.onended = null;\r\n this.silentSource.stop();\r\n this.silentSource.disconnect();\r\n } catch {\r\n // already stopped\r\n }\r\n this.silentSource = null;\r\n }\r\n }\r\n\r\n /** Where the playhead is now, from elapsed `context.currentTime` (so it stays sample-accurate), and whether that is the end of the run. */\r\n private position(): { time: number; ended: boolean } {\r\n const elapsed = (this.context.currentTime - this.playStartCtxTime) * this.speed;\r\n const next = this.reverse\r\n ? this.seekOffset - elapsed\r\n : this.seekOffset + elapsed;\r\n const time = Math.max(0, Math.min(next, this.duration));\r\n return { time, ended: this.reverse ? time <= 0 : time >= this.duration };\r\n }\r\n\r\n /**\r\n * Drives ticks off `requestAnimationFrame`, firing `tick()` only when a full frame interval\r\n * has elapsed — and once more at the end of the run, whether or not it landed on a boundary.\r\n *\r\n * A listener may `seek` (a host looping wraps to 0 when it hears the end), so the end is\r\n * re-read after the listeners and the tick callbacks have run: a run that was seeked out of\r\n * its end carries on, and one that was not pauses itself.\r\n */\r\n private startLoop(): void {\r\n if (this.rafId !== null) return;\r\n const frameDt = 1 / this.fps;\r\n let lastTickTime = this.currentTime;\r\n\r\n const loop = async () => {\r\n let epoch = this.seekEpoch;\r\n const position = this.position();\r\n let ended = position.ended;\r\n // This loop runs at the display's refresh rate, which on a 120 or 144Hz panel is two\r\n // or three times the rate anything is rendered at — so the clock advances every time\r\n // round (the elapsed maths in `play`/`pause` depend on it being exact) while listeners\r\n // hear about it once per frame. The end of the run always reports, or the final\r\n // position would never reach a host. See `Ticker.setCurrentTime`.\r\n const crossed = Math.abs(position.time - lastTickTime) >= frameDt;\r\n const report = crossed || ended;\r\n this.setCurrentTime(position.time, report);\r\n\r\n const reread = () => {\r\n if (this.seekEpoch === epoch) return;\r\n epoch = this.seekEpoch;\r\n lastTickTime = this.currentTime;\r\n ended = this.position().ended;\r\n };\r\n reread();\r\n\r\n if (report) {\r\n lastTickTime = this.currentTime;\r\n // Await so that a slow tick (asset load on a clip\r\n // boundary) doesn't queue a second concurrent tick.\r\n await this.tick();\r\n reread();\r\n }\r\n\r\n // If pause() cleared the rafId while we were awaiting tick(),\r\n // do not resume the loop.\r\n if (this.rafId === null) return;\r\n if (ended) {\r\n this.pause();\r\n return;\r\n }\r\n this.rafId = requestAnimationFrame(loop);\r\n };\r\n\r\n this.rafId = requestAnimationFrame(loop);\r\n }\r\n\r\n private stopLoop(): void {\r\n if (this.rafId !== null) {\r\n cancelAnimationFrame(this.rafId);\r\n this.rafId = null;\r\n }\r\n }\r\n\r\n dispose(): void {\r\n this.stopLoop();\r\n this.stopSilentTicker();\r\n if (this.ownsContext) {\r\n this.context.close();\r\n }\r\n super.dispose();\r\n }\r\n}\r\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.
|
|
3
|
+
"version": "0.1.0-alpha.9",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "
|
|
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,6 +15,7 @@
|
|
|
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
|
},
|
|
@@ -37,11 +38,9 @@
|
|
|
37
38
|
}
|
|
38
39
|
},
|
|
39
40
|
"peerDependencies": {
|
|
40
|
-
"@motionscript/
|
|
41
|
-
"@motionscript/core": "^0.1.0-alpha.7"
|
|
41
|
+
"@motionscript/core": "^0.1.0-alpha.9"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
|
-
"@types/three": "0.185.1",
|
|
45
44
|
"@vitest/browser": "4.1.7",
|
|
46
45
|
"@vitest/browser-playwright": "^4.1.7",
|
|
47
46
|
"concurrently": "^9.0.0",
|
|
@@ -50,22 +49,22 @@
|
|
|
50
49
|
"eslint-plugin-import-x": "^4.16.1",
|
|
51
50
|
"playwright": "^1.60.0",
|
|
52
51
|
"rolldown": "1.0.3",
|
|
53
|
-
"three": "0.185.1",
|
|
54
52
|
"tsc-alias": "^1.8.16",
|
|
55
53
|
"typescript": "^6.0.3",
|
|
56
54
|
"typescript-eslint": "^8.48.0",
|
|
57
55
|
"vitest": "^4.1.5",
|
|
58
|
-
"@motionscript/
|
|
59
|
-
"@motionscript/core": "0.1.0-alpha.7"
|
|
56
|
+
"@motionscript/core": "0.1.0-alpha.9"
|
|
60
57
|
},
|
|
61
58
|
"dependencies": {
|
|
59
|
+
"@mediabunny/mp3-encoder": "^1.49.0",
|
|
62
60
|
"esbuild-wasm": "^0.28.1",
|
|
63
61
|
"mediabunny": "^1.40.1",
|
|
64
|
-
"@motionscript/
|
|
62
|
+
"@motionscript/renderer": "^0.1.0-alpha.9"
|
|
65
63
|
},
|
|
66
64
|
"motionscriptBrowser": {
|
|
67
65
|
"assets": {
|
|
68
|
-
"esbuild.wasm": "esbuild-wasm/esbuild.wasm"
|
|
66
|
+
"esbuild.wasm": "esbuild-wasm/esbuild.wasm",
|
|
67
|
+
"ms_renderer_bg.wasm": "@motionscript/renderer/ms_renderer_bg.wasm"
|
|
69
68
|
}
|
|
70
69
|
},
|
|
71
70
|
"scripts": {
|
|
@@ -1,59 +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-time length the curves run over (a clip's, or a bus's span); `elapsed` is
|
|
28
|
-
* clip-seconds already played at a mid-clip seek (0 for a clean start, negative
|
|
29
|
-
* before the span begins). These let curve-valued params schedule and align.
|
|
30
|
-
*
|
|
31
|
-
* Pass the already-created `source` so its `playbackRate` can be set by the
|
|
32
|
-
* caller; this function only wires nodes *after* the source.
|
|
33
|
-
*/
|
|
34
|
-
export declare function buildAudioFilterGraph(ctx: BaseAudioContext, source: AudioNode, filters: readonly AudioFilterItem[], startTime?: number, clipDuration?: number, elapsed?: number): AudioFilterGraph;
|
|
35
|
-
/**
|
|
36
|
-
* Net *constant* playback-rate multiplier from any scalar `speed` filters in the
|
|
37
|
-
* chain (product of all), defaulting to 1. Curve-valued speeds contribute 1 here
|
|
38
|
-
* and are scheduled separately by {@link applySpeedToPlaybackRate}. Mirrors
|
|
39
|
-
* `Sound.effectiveSpeed()` so the renderer applies the same rate the core timeline
|
|
40
|
-
* reserved time for.
|
|
41
|
-
*/
|
|
42
|
-
export declare function effectiveSpeed(filters: readonly AudioFilterItem[] | undefined): number;
|
|
43
|
-
/** The first curve-valued `speed` filter in the chain, if any. */
|
|
44
|
-
export declare function speedCurveOf(filters: readonly AudioFilterItem[] | undefined): import("@motionscript/core").Curve | undefined;
|
|
45
|
-
/**
|
|
46
|
-
* Schedule a time-varying speed curve onto an `AudioBufferSourceNode.playbackRate`.
|
|
47
|
-
*
|
|
48
|
-
* The curve is authored over **source time** (rate as a function of how far through
|
|
49
|
-
* the source we've played). `setValueAtTime`/`linearRampToValueAtTime` schedule in
|
|
50
|
-
* **context time**, so we walk clip-time forward in small steps, convert each to a
|
|
51
|
-
* source-time offset via the integral inverse, read the curve there, and lay down a
|
|
52
|
-
* value point at the matching context time. `scalar` folds in any constant speed
|
|
53
|
-
* filters (they scale the whole rate uniformly).
|
|
54
|
-
*
|
|
55
|
-
* `elapsed` is clip-seconds already played at a mid-clip seek, so the schedule
|
|
56
|
-
* starts at the seek point and aligns the playbackRate to the clip's true position.
|
|
57
|
-
*/
|
|
58
|
-
export declare function applySpeedToPlaybackRate(rate: AudioParam, curve: ReturnType<typeof speedCurveOf>, sourceLength: number, clipDuration: number, scalar: number, startTime: number, elapsed: number): void;
|
|
59
|
-
//# 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,EAA2C,MAAM,oBAAoB,CAAC;AAE9F;;;;;;;;;GASG;AACH,MAAM,WAAW,gBAAgB;IAC7B,MAAM,EAAE,SAAS,CAAC;IAClB,KAAK,EAAE,SAAS,EAAE,CAAC;IACnB,WAAW,EAAE,cAAc,EAAE,CAAC;CACjC;AA8FD;;;;;;;;;;;;;;;;GAgBG;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,YAAY,EAAE,MAAM,EACpB,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,GAChB,IAAI,CAgBN"}
|
|
@@ -1,241 +0,0 @@
|
|
|
1
|
-
import { isCurve, sourceTimeAtClipElapsed } from "@motionscript/core";
|
|
2
|
-
/**
|
|
3
|
-
* How many sample points per second to bake an eased curve segment into for
|
|
4
|
-
* `setValueCurveAtTime`. 200/s (≈ every 5ms) is smooth to the ear and cheap.
|
|
5
|
-
*/
|
|
6
|
-
const CURVE_SAMPLES_PER_SECOND = 200;
|
|
7
|
-
/**
|
|
8
|
-
* Apply a (possibly time-varying) {@link Param} to a Web Audio {@link AudioParam}.
|
|
9
|
-
*
|
|
10
|
-
* - **number** → set once (today's behavior; zero scheduling overhead).
|
|
11
|
-
* - **Curve** → resolve against `clipDuration` and schedule each segment:
|
|
12
|
-
* - linear segments via `setValueAtTime` + `linearRampToValueAtTime`,
|
|
13
|
-
* - eased segments baked into a `Float32Array` via `setValueCurveAtTime`.
|
|
14
|
-
*
|
|
15
|
-
* `elapsed` (clip-seconds already played when a mid-clip seek starts the source)
|
|
16
|
-
* shifts the schedule back by `elapsed` so "now" reads the curve's value at the
|
|
17
|
-
* clip's *current* position, not its start. A `clamp(v)` hook lets a param keep a
|
|
18
|
-
* valid range (e.g. echo mix 0–1) at every sampled point.
|
|
19
|
-
*/
|
|
20
|
-
function applyParam(ctx, audioParam, param, startTime, clipDuration, elapsed, clamp = (v) => v) {
|
|
21
|
-
// Coerce to a finite value before touching an AudioParam — the platform throws
|
|
22
|
-
// on NaN/Infinity. Falls back to the current param value (never undefined here).
|
|
23
|
-
const finite = (v) => {
|
|
24
|
-
const c = clamp(v);
|
|
25
|
-
return Number.isFinite(c) ? c : audioParam.value;
|
|
26
|
-
};
|
|
27
|
-
if (!isCurve(param)) {
|
|
28
|
-
audioParam.value = finite(param);
|
|
29
|
-
return;
|
|
30
|
-
}
|
|
31
|
-
// A non-finite clip length yields no usable schedule; hold the curve's start value.
|
|
32
|
-
if (!Number.isFinite(clipDuration) || clipDuration <= 0) {
|
|
33
|
-
audioParam.value = finite(param.staticValue(audioParam.value));
|
|
34
|
-
return;
|
|
35
|
-
}
|
|
36
|
-
const base = startTime - elapsed; // map curve t=0 to here so seeks line up
|
|
37
|
-
const resolved = param.resolve(clipDuration, audioParam.value);
|
|
38
|
-
if (resolved.length === 0)
|
|
39
|
-
return;
|
|
40
|
-
// Anchor the starting value at `startTime` (the seek point) so there is no
|
|
41
|
-
// jump from the param's default before the first scheduled segment runs.
|
|
42
|
-
const startValue = param.sampleAt(Math.max(0, elapsed), clipDuration, audioParam.value);
|
|
43
|
-
audioParam.setValueAtTime(finite(startValue), startTime);
|
|
44
|
-
for (const seg of resolved) {
|
|
45
|
-
const segStart = base + seg.startTime;
|
|
46
|
-
const segEnd = base + seg.endTime;
|
|
47
|
-
// Skip segments wholly in the past relative to the seek point, or any with a
|
|
48
|
-
// non-finite boundary (degenerate resolve) that would throw on schedule.
|
|
49
|
-
if (segEnd <= startTime || !Number.isFinite(segStart) || !Number.isFinite(segEnd))
|
|
50
|
-
continue;
|
|
51
|
-
const span = seg.endTime - seg.startTime;
|
|
52
|
-
if (span <= 0) {
|
|
53
|
-
audioParam.setValueAtTime(finite(seg.to), Math.max(segStart, startTime));
|
|
54
|
-
continue;
|
|
55
|
-
}
|
|
56
|
-
// How far through this segment the seek lands. A segment already under way
|
|
57
|
-
// carries on from the anchor rather than starting over from its `from` —
|
|
58
|
-
// the case a bus hits whenever its first clip enters partway through it.
|
|
59
|
-
const done = Math.max(0, (startTime - segStart) / span);
|
|
60
|
-
const curveStart = Math.max(segStart, startTime);
|
|
61
|
-
if (seg.isLinear) {
|
|
62
|
-
if (done === 0)
|
|
63
|
-
audioParam.setValueAtTime(finite(seg.from), curveStart);
|
|
64
|
-
audioParam.linearRampToValueAtTime(finite(seg.to), segEnd);
|
|
65
|
-
}
|
|
66
|
-
else {
|
|
67
|
-
const curveDur = segEnd - curveStart;
|
|
68
|
-
if (curveDur <= 0)
|
|
69
|
-
continue;
|
|
70
|
-
const count = Math.max(2, Math.ceil(curveDur * CURVE_SAMPLES_PER_SECOND));
|
|
71
|
-
const samples = new Float32Array(count);
|
|
72
|
-
for (let i = 0; i < count; i++) {
|
|
73
|
-
const local = done + (1 - done) * (i / (count - 1));
|
|
74
|
-
samples[i] = finite(seg.from + (seg.to - seg.from) * seg.ease(local));
|
|
75
|
-
}
|
|
76
|
-
audioParam.setValueCurveAtTime(samples, curveStart, curveDur);
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
/**
|
|
81
|
-
* Builds the Web Audio graph for a clip's filter chain, returning the node to
|
|
82
|
-
* connect downstream. Shared by live playback ({@link WebAudioSink}) and
|
|
83
|
-
* offline export (`mixAudio`) so both paths sound identical.
|
|
84
|
-
*
|
|
85
|
-
* Filters apply in array order (index 0 closest to the source). `speed` filters
|
|
86
|
-
* are intentionally ignored here — they're realized via `source.playbackRate`
|
|
87
|
-
* by the caller (and affect timeline scheduling), not as a graph node.
|
|
88
|
-
*
|
|
89
|
-
* `startTime` is the AudioContext time the source starts; `clipDuration` is the
|
|
90
|
-
* clip-time length the curves run over (a clip's, or a bus's span); `elapsed` is
|
|
91
|
-
* clip-seconds already played at a mid-clip seek (0 for a clean start, negative
|
|
92
|
-
* before the span begins). These let curve-valued params schedule and align.
|
|
93
|
-
*
|
|
94
|
-
* Pass the already-created `source` so its `playbackRate` can be set by the
|
|
95
|
-
* caller; this function only wires nodes *after* the source.
|
|
96
|
-
*/
|
|
97
|
-
export function buildAudioFilterGraph(ctx, source, filters, startTime = ctx.currentTime, clipDuration = 0, elapsed = 0) {
|
|
98
|
-
const nodes = [];
|
|
99
|
-
const oscillators = [];
|
|
100
|
-
let cursor = source;
|
|
101
|
-
const series = (node) => {
|
|
102
|
-
cursor.connect(node);
|
|
103
|
-
nodes.push(node);
|
|
104
|
-
cursor = node;
|
|
105
|
-
};
|
|
106
|
-
const apply = (p, v, clamp) => applyParam(ctx, p, v, startTime, clipDuration, elapsed, clamp);
|
|
107
|
-
for (const filter of filters) {
|
|
108
|
-
switch (filter.type) {
|
|
109
|
-
case "gain": {
|
|
110
|
-
const gain = ctx.createGain();
|
|
111
|
-
apply(gain.gain, filter.value);
|
|
112
|
-
series(gain);
|
|
113
|
-
break;
|
|
114
|
-
}
|
|
115
|
-
case "highpass":
|
|
116
|
-
case "lowpass": {
|
|
117
|
-
const biquad = ctx.createBiquadFilter();
|
|
118
|
-
biquad.type = filter.type === "highpass" ? "highpass" : "lowpass";
|
|
119
|
-
apply(biquad.frequency, filter.frequency);
|
|
120
|
-
if (filter.q !== undefined)
|
|
121
|
-
apply(biquad.Q, filter.q);
|
|
122
|
-
series(biquad);
|
|
123
|
-
break;
|
|
124
|
-
}
|
|
125
|
-
case "tremolo": {
|
|
126
|
-
// Carrier gain whose value is modulated by an LFO. With depth d the
|
|
127
|
-
// carrier base = (1 - d/2) and the LFO swing gain = d/2. The clamp
|
|
128
|
-
// hook applies these transforms whether depth is scalar or a curve.
|
|
129
|
-
const carrier = ctx.createGain();
|
|
130
|
-
const lfo = ctx.createOscillator();
|
|
131
|
-
const lfoDepth = ctx.createGain();
|
|
132
|
-
apply(carrier.gain, filter.depth, (d) => 1 - clamp01(d) / 2);
|
|
133
|
-
apply(lfoDepth.gain, filter.depth, (d) => clamp01(d) / 2);
|
|
134
|
-
apply(lfo.frequency, filter.rate);
|
|
135
|
-
lfo.connect(lfoDepth);
|
|
136
|
-
lfoDepth.connect(carrier.gain);
|
|
137
|
-
nodes.push(lfoDepth);
|
|
138
|
-
oscillators.push(lfo);
|
|
139
|
-
series(carrier);
|
|
140
|
-
break;
|
|
141
|
-
}
|
|
142
|
-
case "echo": {
|
|
143
|
-
// Dry + wet split. The wet path is a delay with feedback; both sum
|
|
144
|
-
// into a merge node that becomes the new cursor.
|
|
145
|
-
const split = cursor;
|
|
146
|
-
const merge = ctx.createGain();
|
|
147
|
-
const dry = ctx.createGain();
|
|
148
|
-
apply(dry.gain, filter.mix ?? 0.5, (m) => 1 - clamp01(m));
|
|
149
|
-
split.connect(dry);
|
|
150
|
-
dry.connect(merge);
|
|
151
|
-
// Delay buffer must accommodate the max delay; size from the static value.
|
|
152
|
-
const maxDelay = staticOf(filter.delay);
|
|
153
|
-
const delay = ctx.createDelay(Math.max(1, maxDelay + 1));
|
|
154
|
-
apply(delay.delayTime, filter.delay);
|
|
155
|
-
const feedbackGain = ctx.createGain();
|
|
156
|
-
apply(feedbackGain.gain, filter.feedback, (f) => Math.max(0, Math.min(0.99, f)));
|
|
157
|
-
const wet = ctx.createGain();
|
|
158
|
-
apply(wet.gain, filter.mix ?? 0.5, (m) => clamp01(m));
|
|
159
|
-
split.connect(delay);
|
|
160
|
-
delay.connect(feedbackGain);
|
|
161
|
-
feedbackGain.connect(delay); // feedback loop
|
|
162
|
-
delay.connect(wet);
|
|
163
|
-
wet.connect(merge);
|
|
164
|
-
nodes.push(dry, delay, feedbackGain, wet, merge);
|
|
165
|
-
cursor = merge;
|
|
166
|
-
break;
|
|
167
|
-
}
|
|
168
|
-
case "speed":
|
|
169
|
-
// Realized via source.playbackRate by the caller; no graph node.
|
|
170
|
-
break;
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
return { output: cursor, nodes, oscillators };
|
|
174
|
-
}
|
|
175
|
-
/** Clamp to [0, 1]. */
|
|
176
|
-
function clamp01(v) {
|
|
177
|
-
return Math.max(0, Math.min(1, v));
|
|
178
|
-
}
|
|
179
|
-
/** The static/representative number of a param (number itself, or curve's t=0 value). */
|
|
180
|
-
function staticOf(p) {
|
|
181
|
-
return isCurve(p) ? p.staticValue(0) : p;
|
|
182
|
-
}
|
|
183
|
-
/**
|
|
184
|
-
* Net *constant* playback-rate multiplier from any scalar `speed` filters in the
|
|
185
|
-
* chain (product of all), defaulting to 1. Curve-valued speeds contribute 1 here
|
|
186
|
-
* and are scheduled separately by {@link applySpeedToPlaybackRate}. Mirrors
|
|
187
|
-
* `Sound.effectiveSpeed()` so the renderer applies the same rate the core timeline
|
|
188
|
-
* reserved time for.
|
|
189
|
-
*/
|
|
190
|
-
export function effectiveSpeed(filters) {
|
|
191
|
-
if (!filters)
|
|
192
|
-
return 1;
|
|
193
|
-
let speed = 1;
|
|
194
|
-
for (const f of filters) {
|
|
195
|
-
if (f.type === "speed" && !isCurve(f.value) && f.value > 0)
|
|
196
|
-
speed *= f.value;
|
|
197
|
-
}
|
|
198
|
-
return speed;
|
|
199
|
-
}
|
|
200
|
-
/** The first curve-valued `speed` filter in the chain, if any. */
|
|
201
|
-
export function speedCurveOf(filters) {
|
|
202
|
-
if (!filters)
|
|
203
|
-
return undefined;
|
|
204
|
-
for (const f of filters) {
|
|
205
|
-
if (f.type === "speed" && isCurve(f.value))
|
|
206
|
-
return f.value;
|
|
207
|
-
}
|
|
208
|
-
return undefined;
|
|
209
|
-
}
|
|
210
|
-
/**
|
|
211
|
-
* Schedule a time-varying speed curve onto an `AudioBufferSourceNode.playbackRate`.
|
|
212
|
-
*
|
|
213
|
-
* The curve is authored over **source time** (rate as a function of how far through
|
|
214
|
-
* the source we've played). `setValueAtTime`/`linearRampToValueAtTime` schedule in
|
|
215
|
-
* **context time**, so we walk clip-time forward in small steps, convert each to a
|
|
216
|
-
* source-time offset via the integral inverse, read the curve there, and lay down a
|
|
217
|
-
* value point at the matching context time. `scalar` folds in any constant speed
|
|
218
|
-
* filters (they scale the whole rate uniformly).
|
|
219
|
-
*
|
|
220
|
-
* `elapsed` is clip-seconds already played at a mid-clip seek, so the schedule
|
|
221
|
-
* starts at the seek point and aligns the playbackRate to the clip's true position.
|
|
222
|
-
*/
|
|
223
|
-
export function applySpeedToPlaybackRate(rate, curve, sourceLength, clipDuration, scalar, startTime, elapsed) {
|
|
224
|
-
if (!curve)
|
|
225
|
-
return;
|
|
226
|
-
const step = 1 / CURVE_SAMPLES_PER_SECOND; // clip-time sampling step
|
|
227
|
-
const seekAt = Math.max(0, elapsed);
|
|
228
|
-
// Seed the value exactly at the seek point.
|
|
229
|
-
const seedSrc = sourceTimeAtClipElapsed(curve, sourceLength, seekAt);
|
|
230
|
-
rate.setValueAtTime(Math.max(1e-4, curve.sampleAt(seedSrc, sourceLength, 1) * scalar), startTime);
|
|
231
|
-
for (let s = seekAt + step; s <= clipDuration + step; s += step) {
|
|
232
|
-
const clampedElapsed = Math.min(s, clipDuration);
|
|
233
|
-
const srcT = sourceTimeAtClipElapsed(curve, sourceLength, clampedElapsed);
|
|
234
|
-
const value = Math.max(1e-4, curve.sampleAt(srcT, sourceLength, 1) * scalar);
|
|
235
|
-
const ctxTime = startTime + (clampedElapsed - seekAt);
|
|
236
|
-
rate.linearRampToValueAtTime(value, ctxTime);
|
|
237
|
-
if (clampedElapsed >= clipDuration)
|
|
238
|
-
break;
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
//# sourceMappingURL=filter-graph.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"filter-graph.js","sourceRoot":"","sources":["../../src/audio/filter-graph.ts"],"names":[],"mappings":"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","sourcesContent":["import { AudioFilterItem, Param, isCurve, sourceTimeAtClipElapsed } from \"@motionscript/core\";\r\n\r\n/**\r\n * Result of building an audio-filter graph for one source.\r\n *\r\n * - `output` is the last node in the chain; connect it downstream (e.g. to the\r\n * per-request gain node).\r\n * - `nodes` are every intermediate node, so callers can `disconnect()` them on\r\n * teardown.\r\n * - `oscillators` are LFOs (from `tremolo`) the caller must `start()` — and may\r\n * `stop()` — alongside the source, since oscillators need an explicit start.\r\n */\r\nexport interface AudioFilterGraph {\r\n output: AudioNode;\r\n nodes: AudioNode[];\r\n oscillators: OscillatorNode[];\r\n}\r\n\r\n/**\r\n * How many sample points per second to bake an eased curve segment into for\r\n * `setValueCurveAtTime`. 200/s (≈ every 5ms) is smooth to the ear and cheap.\r\n */\r\nconst CURVE_SAMPLES_PER_SECOND = 200;\r\n\r\n/**\r\n * Apply a (possibly time-varying) {@link Param} to a Web Audio {@link AudioParam}.\r\n *\r\n * - **number** → set once (today's behavior; zero scheduling overhead).\r\n * - **Curve** → resolve against `clipDuration` and schedule each segment:\r\n * - linear segments via `setValueAtTime` + `linearRampToValueAtTime`,\r\n * - eased segments baked into a `Float32Array` via `setValueCurveAtTime`.\r\n *\r\n * `elapsed` (clip-seconds already played when a mid-clip seek starts the source)\r\n * shifts the schedule back by `elapsed` so \"now\" reads the curve's value at the\r\n * clip's *current* position, not its start. A `clamp(v)` hook lets a param keep a\r\n * valid range (e.g. echo mix 0–1) at every sampled point.\r\n */\r\nfunction applyParam(\r\n ctx: BaseAudioContext,\r\n audioParam: AudioParam,\r\n param: Param,\r\n startTime: number,\r\n clipDuration: number,\r\n elapsed: number,\r\n clamp: (v: number) => number = (v) => v,\r\n): void {\r\n // Coerce to a finite value before touching an AudioParam — the platform throws\r\n // on NaN/Infinity. Falls back to the current param value (never undefined here).\r\n const finite = (v: number): number => {\r\n const c = clamp(v);\r\n return Number.isFinite(c) ? c : audioParam.value;\r\n };\r\n\r\n if (!isCurve(param)) {\r\n audioParam.value = finite(param);\r\n return;\r\n }\r\n\r\n // A non-finite clip length yields no usable schedule; hold the curve's start value.\r\n if (!Number.isFinite(clipDuration) || clipDuration <= 0) {\r\n audioParam.value = finite(param.staticValue(audioParam.value));\r\n return;\r\n }\r\n\r\n const base = startTime - elapsed; // map curve t=0 to here so seeks line up\r\n const resolved = param.resolve(clipDuration, audioParam.value);\r\n if (resolved.length === 0) return;\r\n\r\n // Anchor the starting value at `startTime` (the seek point) so there is no\r\n // jump from the param's default before the first scheduled segment runs.\r\n const startValue = param.sampleAt(Math.max(0, elapsed), clipDuration, audioParam.value);\r\n audioParam.setValueAtTime(finite(startValue), startTime);\r\n\r\n for (const seg of resolved) {\r\n const segStart = base + seg.startTime;\r\n const segEnd = base + seg.endTime;\r\n // Skip segments wholly in the past relative to the seek point, or any with a\r\n // non-finite boundary (degenerate resolve) that would throw on schedule.\r\n if (segEnd <= startTime || !Number.isFinite(segStart) || !Number.isFinite(segEnd)) continue;\r\n\r\n const span = seg.endTime - seg.startTime;\r\n\r\n if (span <= 0) {\r\n audioParam.setValueAtTime(finite(seg.to), Math.max(segStart, startTime));\r\n continue;\r\n }\r\n\r\n // How far through this segment the seek lands. A segment already under way\r\n // carries on from the anchor rather than starting over from its `from` —\r\n // the case a bus hits whenever its first clip enters partway through it.\r\n const done = Math.max(0, (startTime - segStart) / span);\r\n const curveStart = Math.max(segStart, startTime);\r\n\r\n if (seg.isLinear) {\r\n if (done === 0) audioParam.setValueAtTime(finite(seg.from), curveStart);\r\n audioParam.linearRampToValueAtTime(finite(seg.to), segEnd);\r\n } else {\r\n const curveDur = segEnd - curveStart;\r\n if (curveDur <= 0) continue;\r\n const count = Math.max(2, Math.ceil(curveDur * CURVE_SAMPLES_PER_SECOND));\r\n const samples = new Float32Array(count);\r\n for (let i = 0; i < count; i++) {\r\n const local = done + (1 - done) * (i / (count - 1));\r\n samples[i] = finite(seg.from + (seg.to - seg.from) * seg.ease(local));\r\n }\r\n audioParam.setValueCurveAtTime(samples, curveStart, curveDur);\r\n }\r\n }\r\n}\r\n\r\n/**\r\n * Builds the Web Audio graph for a clip's filter chain, returning the node to\r\n * connect downstream. Shared by live playback ({@link WebAudioSink}) and\r\n * offline export (`mixAudio`) so both paths sound identical.\r\n *\r\n * Filters apply in array order (index 0 closest to the source). `speed` filters\r\n * are intentionally ignored here — they're realized via `source.playbackRate`\r\n * by the caller (and affect timeline scheduling), not as a graph node.\r\n *\r\n * `startTime` is the AudioContext time the source starts; `clipDuration` is the\r\n * clip-time length the curves run over (a clip's, or a bus's span); `elapsed` is\r\n * clip-seconds already played at a mid-clip seek (0 for a clean start, negative\r\n * before the span begins). These let curve-valued params schedule and align.\r\n *\r\n * Pass the already-created `source` so its `playbackRate` can be set by the\r\n * caller; this function only wires nodes *after* the source.\r\n */\r\nexport function buildAudioFilterGraph(\r\n ctx: BaseAudioContext,\r\n source: AudioNode,\r\n filters: readonly AudioFilterItem[],\r\n startTime: number = ctx.currentTime,\r\n clipDuration: number = 0,\r\n elapsed: number = 0,\r\n): AudioFilterGraph {\r\n const nodes: AudioNode[] = [];\r\n const oscillators: OscillatorNode[] = [];\r\n let cursor: AudioNode = source;\r\n\r\n const series = (node: AudioNode): void => {\r\n cursor.connect(node);\r\n nodes.push(node);\r\n cursor = node;\r\n };\r\n\r\n const apply = (p: AudioParam, v: Param, clamp?: (n: number) => number): void =>\r\n applyParam(ctx, p, v, startTime, clipDuration, elapsed, clamp);\r\n\r\n for (const filter of filters) {\r\n switch (filter.type) {\r\n case \"gain\": {\r\n const gain = ctx.createGain();\r\n apply(gain.gain, filter.value);\r\n series(gain);\r\n break;\r\n }\r\n case \"highpass\":\r\n case \"lowpass\": {\r\n const biquad = ctx.createBiquadFilter();\r\n biquad.type = filter.type === \"highpass\" ? \"highpass\" : \"lowpass\";\r\n apply(biquad.frequency, filter.frequency);\r\n if (filter.q !== undefined) apply(biquad.Q, filter.q);\r\n series(biquad);\r\n break;\r\n }\r\n case \"tremolo\": {\r\n // Carrier gain whose value is modulated by an LFO. With depth d the\r\n // carrier base = (1 - d/2) and the LFO swing gain = d/2. The clamp\r\n // hook applies these transforms whether depth is scalar or a curve.\r\n const carrier = ctx.createGain();\r\n const lfo = ctx.createOscillator();\r\n const lfoDepth = ctx.createGain();\r\n\r\n apply(carrier.gain, filter.depth, (d) => 1 - clamp01(d) / 2);\r\n apply(lfoDepth.gain, filter.depth, (d) => clamp01(d) / 2);\r\n apply(lfo.frequency, filter.rate);\r\n\r\n lfo.connect(lfoDepth);\r\n lfoDepth.connect(carrier.gain);\r\n\r\n nodes.push(lfoDepth);\r\n oscillators.push(lfo);\r\n series(carrier);\r\n break;\r\n }\r\n case \"echo\": {\r\n // Dry + wet split. The wet path is a delay with feedback; both sum\r\n // into a merge node that becomes the new cursor.\r\n const split = cursor;\r\n const merge = ctx.createGain();\r\n\r\n const dry = ctx.createGain();\r\n apply(dry.gain, filter.mix ?? 0.5, (m) => 1 - clamp01(m));\r\n split.connect(dry);\r\n dry.connect(merge);\r\n\r\n // Delay buffer must accommodate the max delay; size from the static value.\r\n const maxDelay = staticOf(filter.delay);\r\n const delay = ctx.createDelay(Math.max(1, maxDelay + 1));\r\n apply(delay.delayTime, filter.delay);\r\n const feedbackGain = ctx.createGain();\r\n apply(feedbackGain.gain, filter.feedback, (f) => Math.max(0, Math.min(0.99, f)));\r\n const wet = ctx.createGain();\r\n apply(wet.gain, filter.mix ?? 0.5, (m) => clamp01(m));\r\n\r\n split.connect(delay);\r\n delay.connect(feedbackGain);\r\n feedbackGain.connect(delay); // feedback loop\r\n delay.connect(wet);\r\n wet.connect(merge);\r\n\r\n nodes.push(dry, delay, feedbackGain, wet, merge);\r\n cursor = merge;\r\n break;\r\n }\r\n case \"speed\":\r\n // Realized via source.playbackRate by the caller; no graph node.\r\n break;\r\n }\r\n }\r\n\r\n return { output: cursor, nodes, oscillators };\r\n}\r\n\r\n/** Clamp to [0, 1]. */\r\nfunction clamp01(v: number): number {\r\n return Math.max(0, Math.min(1, v));\r\n}\r\n\r\n/** The static/representative number of a param (number itself, or curve's t=0 value). */\r\nfunction staticOf(p: Param): number {\r\n return isCurve(p) ? p.staticValue(0) : p;\r\n}\r\n\r\n/**\r\n * Net *constant* playback-rate multiplier from any scalar `speed` filters in the\r\n * chain (product of all), defaulting to 1. Curve-valued speeds contribute 1 here\r\n * and are scheduled separately by {@link applySpeedToPlaybackRate}. Mirrors\r\n * `Sound.effectiveSpeed()` so the renderer applies the same rate the core timeline\r\n * reserved time for.\r\n */\r\nexport function effectiveSpeed(filters: readonly AudioFilterItem[] | undefined): number {\r\n if (!filters) return 1;\r\n let speed = 1;\r\n for (const f of filters) {\r\n if (f.type === \"speed\" && !isCurve(f.value) && f.value > 0) speed *= f.value;\r\n }\r\n return speed;\r\n}\r\n\r\n/** The first curve-valued `speed` filter in the chain, if any. */\r\nexport function speedCurveOf(filters: readonly AudioFilterItem[] | undefined) {\r\n if (!filters) return undefined;\r\n for (const f of filters) {\r\n if (f.type === \"speed\" && isCurve(f.value)) return f.value;\r\n }\r\n return undefined;\r\n}\r\n\r\n/**\r\n * Schedule a time-varying speed curve onto an `AudioBufferSourceNode.playbackRate`.\r\n *\r\n * The curve is authored over **source time** (rate as a function of how far through\r\n * the source we've played). `setValueAtTime`/`linearRampToValueAtTime` schedule in\r\n * **context time**, so we walk clip-time forward in small steps, convert each to a\r\n * source-time offset via the integral inverse, read the curve there, and lay down a\r\n * value point at the matching context time. `scalar` folds in any constant speed\r\n * filters (they scale the whole rate uniformly).\r\n *\r\n * `elapsed` is clip-seconds already played at a mid-clip seek, so the schedule\r\n * starts at the seek point and aligns the playbackRate to the clip's true position.\r\n */\r\nexport function applySpeedToPlaybackRate(\r\n rate: AudioParam,\r\n curve: ReturnType<typeof speedCurveOf>,\r\n sourceLength: number,\r\n clipDuration: number,\r\n scalar: number,\r\n startTime: number,\r\n elapsed: number,\r\n): void {\r\n if (!curve) return;\r\n const step = 1 / CURVE_SAMPLES_PER_SECOND; // clip-time sampling step\r\n const seekAt = Math.max(0, elapsed);\r\n // Seed the value exactly at the seek point.\r\n const seedSrc = sourceTimeAtClipElapsed(curve, sourceLength, seekAt);\r\n rate.setValueAtTime(Math.max(1e-4, curve.sampleAt(seedSrc, sourceLength, 1) * scalar), startTime);\r\n\r\n for (let s = seekAt + step; s <= clipDuration + step; s += step) {\r\n const clampedElapsed = Math.min(s, clipDuration);\r\n const srcT = sourceTimeAtClipElapsed(curve, sourceLength, clampedElapsed);\r\n const value = Math.max(1e-4, curve.sampleAt(srcT, sourceLength, 1) * scalar);\r\n const ctxTime = startTime + (clampedElapsed - seekAt);\r\n rate.linearRampToValueAtTime(value, ctxTime);\r\n if (clampedElapsed >= clipDuration) break;\r\n }\r\n}\r\n"]}
|