@motionscript/browser 0.0.0-stage → 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 (133) hide show
  1. package/CHANGELOG.md +192 -0
  2. package/LICENSE +201 -0
  3. package/NOTICE +49 -0
  4. package/README.md +140 -3
  5. package/dist/asset-bytes.d.ts +59 -0
  6. package/dist/asset-bytes.d.ts.map +1 -0
  7. package/dist/asset-bytes.js +78 -0
  8. package/dist/asset-bytes.js.map +1 -0
  9. package/dist/audio/bus-graph.d.ts +52 -0
  10. package/dist/audio/bus-graph.d.ts.map +1 -0
  11. package/dist/audio/bus-graph.js +108 -0
  12. package/dist/audio/bus-graph.js.map +1 -0
  13. package/dist/audio/effect-graph.d.ts +62 -0
  14. package/dist/audio/effect-graph.d.ts.map +1 -0
  15. package/dist/audio/effect-graph.js +234 -0
  16. package/dist/audio/effect-graph.js.map +1 -0
  17. package/dist/audio/index.d.ts +5 -0
  18. package/dist/audio/index.d.ts.map +1 -0
  19. package/dist/audio/index.js +11 -0
  20. package/dist/audio/index.js.map +1 -0
  21. package/dist/audio/loop-region.d.ts +31 -0
  22. package/dist/audio/loop-region.d.ts.map +1 -0
  23. package/dist/audio/loop-region.js +43 -0
  24. package/dist/audio/loop-region.js.map +1 -0
  25. package/dist/audio/mixer.d.ts +59 -0
  26. package/dist/audio/mixer.d.ts.map +1 -0
  27. package/dist/audio/mixer.js +192 -0
  28. package/dist/audio/mixer.js.map +1 -0
  29. package/dist/audio/player.d.ts +55 -0
  30. package/dist/audio/player.d.ts.map +1 -0
  31. package/dist/audio/player.js +217 -0
  32. package/dist/audio/player.js.map +1 -0
  33. package/dist/browser/THIRD_PARTY_LICENSES.txt +784 -0
  34. package/dist/browser/audio.js +2 -0
  35. package/dist/browser/audio.js.map +7 -0
  36. package/dist/browser/chunks/chunk-F3AZKPHU.js +2 -0
  37. package/dist/browser/chunks/chunk-F3AZKPHU.js.map +7 -0
  38. package/dist/browser/chunks/chunk-KLXMRKTL.js +2 -0
  39. package/dist/browser/chunks/chunk-KLXMRKTL.js.map +7 -0
  40. package/dist/browser/chunks/chunk-L5UFQACH.js +163 -0
  41. package/dist/browser/chunks/chunk-L5UFQACH.js.map +7 -0
  42. package/dist/browser/chunks/mediabunny-mp3-encoder-ZCIHVVTJ.js +14 -0
  43. package/dist/browser/chunks/mediabunny-mp3-encoder-ZCIHVVTJ.js.map +7 -0
  44. package/dist/browser/esbuild.wasm +0 -0
  45. package/dist/browser/index.js +734 -0
  46. package/dist/browser/index.js.map +7 -0
  47. package/dist/browser/manifest.json +32 -0
  48. package/dist/browser/ms_renderer_bg.wasm +0 -0
  49. package/dist/concat.d.ts +121 -0
  50. package/dist/concat.d.ts.map +1 -0
  51. package/dist/concat.js +219 -0
  52. package/dist/concat.js.map +1 -0
  53. package/dist/deliver.d.ts +10 -0
  54. package/dist/deliver.d.ts.map +1 -0
  55. package/dist/deliver.js +22 -0
  56. package/dist/deliver.js.map +1 -0
  57. package/dist/dynamic-nodes.d.ts +9 -0
  58. package/dist/dynamic-nodes.d.ts.map +1 -0
  59. package/dist/dynamic-nodes.js +53 -0
  60. package/dist/dynamic-nodes.js.map +1 -0
  61. package/dist/engine.d.ts +87 -0
  62. package/dist/engine.d.ts.map +1 -0
  63. package/dist/engine.js +193 -0
  64. package/dist/engine.js.map +1 -0
  65. package/dist/export-audio.d.ts +36 -0
  66. package/dist/export-audio.d.ts.map +1 -0
  67. package/dist/export-audio.js +56 -0
  68. package/dist/export-audio.js.map +1 -0
  69. package/dist/exporter.d.ts +282 -0
  70. package/dist/exporter.d.ts.map +1 -0
  71. package/dist/exporter.js +308 -0
  72. package/dist/exporter.js.map +1 -0
  73. package/dist/formats.d.ts +36 -0
  74. package/dist/formats.d.ts.map +1 -0
  75. package/dist/formats.js +83 -0
  76. package/dist/formats.js.map +1 -0
  77. package/dist/frame-loop.d.ts +190 -0
  78. package/dist/frame-loop.d.ts.map +1 -0
  79. package/dist/frame-loop.js +304 -0
  80. package/dist/frame-loop.js.map +1 -0
  81. package/dist/index.d.ts +24 -0
  82. package/dist/index.d.ts.map +1 -0
  83. package/dist/index.js +43 -0
  84. package/dist/index.js.map +1 -0
  85. package/dist/mix-timeline-audio.d.ts +42 -0
  86. package/dist/mix-timeline-audio.d.ts.map +1 -0
  87. package/dist/mix-timeline-audio.js +60 -0
  88. package/dist/mix-timeline-audio.js.map +1 -0
  89. package/dist/parallel/export-parallel.d.ts +32 -0
  90. package/dist/parallel/export-parallel.d.ts.map +1 -0
  91. package/dist/parallel/export-parallel.js +114 -0
  92. package/dist/parallel/export-parallel.js.map +1 -0
  93. package/dist/parallel/governor.d.ts +57 -0
  94. package/dist/parallel/governor.d.ts.map +1 -0
  95. package/dist/parallel/governor.js +79 -0
  96. package/dist/parallel/governor.js.map +1 -0
  97. package/dist/parallel/lane.d.ts +45 -0
  98. package/dist/parallel/lane.d.ts.map +1 -0
  99. package/dist/parallel/lane.js +2 -0
  100. package/dist/parallel/lane.js.map +1 -0
  101. package/dist/parallel/lanes.d.ts +20 -0
  102. package/dist/parallel/lanes.d.ts.map +1 -0
  103. package/dist/parallel/lanes.js +152 -0
  104. package/dist/parallel/lanes.js.map +1 -0
  105. package/dist/parallel/plan.d.ts +23 -0
  106. package/dist/parallel/plan.d.ts.map +1 -0
  107. package/dist/parallel/plan.js +30 -0
  108. package/dist/parallel/plan.js.map +1 -0
  109. package/dist/parallel/run-lanes.d.ts +38 -0
  110. package/dist/parallel/run-lanes.d.ts.map +1 -0
  111. package/dist/parallel/run-lanes.js +164 -0
  112. package/dist/parallel/run-lanes.js.map +1 -0
  113. package/dist/parallel/session.d.ts +9 -0
  114. package/dist/parallel/session.d.ts.map +1 -0
  115. package/dist/parallel/session.js +79 -0
  116. package/dist/parallel/session.js.map +1 -0
  117. package/dist/renderer.d.ts +22 -0
  118. package/dist/renderer.d.ts.map +1 -0
  119. package/dist/renderer.js +23 -0
  120. package/dist/renderer.js.map +1 -0
  121. package/dist/screenshot.d.ts +45 -0
  122. package/dist/screenshot.d.ts.map +1 -0
  123. package/dist/screenshot.js +31 -0
  124. package/dist/screenshot.js.map +1 -0
  125. package/dist/still.d.ts +159 -0
  126. package/dist/still.d.ts.map +1 -0
  127. package/dist/still.js +222 -0
  128. package/dist/still.js.map +1 -0
  129. package/dist/ticker.d.ts +74 -0
  130. package/dist/ticker.d.ts.map +1 -0
  131. package/dist/ticker.js +215 -0
  132. package/dist/ticker.js.map +1 -0
  133. package/package.json +75 -3
@@ -0,0 +1,217 @@
1
+ import { AudioSink } from "@motionscript/core";
2
+ import { AudioBusGraph } from "./bus-graph";
3
+ import { applyLoop, wrapIntoRegion } from "./loop-region";
4
+ import { applySpeedToPlaybackRate, buildAudioEffectGraph, buildLevel, playbackOf, sourceOffsetOf } from "./effect-graph";
5
+ /**
6
+ * Web Audio implementation of {@link AudioSink} — decodes and caches source
7
+ * buffers, then schedules/plays {@link AudioRequest}s as `AudioBufferSourceNode`s
8
+ * through a shared master gain (global mute and volume). `schedule` registers the
9
+ * active set of requests for the current frame; `syncTo` then starts/stops
10
+ * sources to match `clipTime`, so playback follows scrubbing and seeks.
11
+ */
12
+ export class WebAudioSink extends AudioSink {
13
+ borrowed;
14
+ _context = null;
15
+ bufferCache = new Map();
16
+ decoding = new Map();
17
+ active = new Map();
18
+ scheduled = [];
19
+ disposed = false;
20
+ /** The master gain and the live bus chains (shared across every clip that names one), built with the context. */
21
+ _graph = null;
22
+ muted = false;
23
+ volume = 1;
24
+ constructor(context) {
25
+ super();
26
+ this.borrowed = context;
27
+ }
28
+ /**
29
+ * The context, created on first use rather than with the sink: a new
30
+ * AudioContext opens the audio device (50–65 ms on a desktop), and a
31
+ * session that never plays or decodes sound never needs one.
32
+ */
33
+ getContext() {
34
+ this._context ??= this.borrowed ?? new AudioContext();
35
+ return this._context;
36
+ }
37
+ get context() {
38
+ return this.getContext();
39
+ }
40
+ get graph() {
41
+ if (!this._graph) {
42
+ const master = this.context.createGain();
43
+ master.connect(this.context.destination);
44
+ this._graph = { master, buses: new AudioBusGraph(this.context, master) };
45
+ this.applyMasterGain();
46
+ }
47
+ return this._graph;
48
+ }
49
+ /**
50
+ * Resume the AudioContext. Must be called from within a user gesture
51
+ * (e.g. a click handler) the first time, or browsers keep the context
52
+ * suspended and no audio is heard.
53
+ */
54
+ unlock() {
55
+ if (this.context.state === "suspended") {
56
+ this.context.resume();
57
+ }
58
+ }
59
+ has(src) {
60
+ return this.bufferCache.has(src);
61
+ }
62
+ async append(src, data) {
63
+ if (this.bufferCache.has(src))
64
+ return;
65
+ const existing = this.decoding.get(src);
66
+ if (existing)
67
+ return existing;
68
+ // decodeAudioData detaches `data` — that's fine, the caller in
69
+ // AssetManager.fetchAudio discards its reference immediately after
70
+ // awaiting append. Skipping the defensive .slice(0) saves a multi-MB
71
+ // allocation per audio source.
72
+ const job = this.context.decodeAudioData(data).then((buffer) => {
73
+ this.bufferCache.set(src, buffer);
74
+ }).finally(() => {
75
+ this.decoding.delete(src);
76
+ });
77
+ this.decoding.set(src, job);
78
+ await job;
79
+ }
80
+ /** Drops cached buffers and stops active sources for srcs/requests no longer referenced — called when the asset set changes (e.g. seeking across scene boundaries). */
81
+ retain(keep) {
82
+ for (const src of [...this.bufferCache.keys()]) {
83
+ if (!keep.has(src))
84
+ this.bufferCache.delete(src);
85
+ }
86
+ for (const [id, active] of this.active) {
87
+ if (!keep.has(active.request.src))
88
+ this.stopSource(id, active);
89
+ }
90
+ }
91
+ /** Replaces the active request set; stops any currently-playing source whose request is no longer scheduled. Actual start/stop-to-match-time happens in `syncTo`. */
92
+ schedule(requests) {
93
+ const next = new Set(requests.map(r => r.id));
94
+ for (const [id, active] of this.active) {
95
+ if (!next.has(id))
96
+ this.stopSource(id, active);
97
+ }
98
+ this.scheduled = requests.slice();
99
+ }
100
+ /** Starts sources whose `[startAt, endAt)` window now contains `clipTime` (computing the correct buffer offset for the seek point) and stops any that have fallen outside it. */
101
+ syncTo(clipTime) {
102
+ const liveIds = new Set();
103
+ for (const req of this.scheduled) {
104
+ // `endAt` is `Infinity` for an open clip, so the comparison needs no
105
+ // nullish fallback — see AudioRequest.endAt.
106
+ const isInWindow = clipTime >= req.startAt && clipTime < req.endAt;
107
+ if (!isInWindow)
108
+ continue;
109
+ liveIds.add(req.id);
110
+ if (!this.active.has(req.id)) {
111
+ const buffer = this.bufferCache.get(req.src);
112
+ if (buffer)
113
+ this.playBuffer(buffer, req, clipTime);
114
+ }
115
+ }
116
+ for (const [id, active] of this.active) {
117
+ if (!liveIds.has(id))
118
+ this.stopSource(id, active);
119
+ }
120
+ }
121
+ async play(time, _speed, _reverse) {
122
+ if (this.context.state === "suspended") {
123
+ await this.context.resume();
124
+ }
125
+ this.syncTo(time);
126
+ }
127
+ stop() {
128
+ for (const [id, active] of this.active) {
129
+ this.stopSource(id, active);
130
+ }
131
+ // A bus's curves are scheduled against the clock of the run that built
132
+ // it, so the next play, from wherever it lands, builds them again.
133
+ this._graph?.buses.dispose();
134
+ }
135
+ setMuted(muted) {
136
+ this.muted = muted;
137
+ this.applyMasterGain();
138
+ }
139
+ setVolume(volume) {
140
+ this.volume = Math.max(0, volume);
141
+ this.applyMasterGain();
142
+ }
143
+ /** Mute and volume are held separately so unmuting restores the set level. */
144
+ applyMasterGain() {
145
+ if (this._graph)
146
+ this._graph.master.gain.value = this.muted ? 0 : this.volume;
147
+ }
148
+ /** Starts a buffer source mid-clip if `clipTime` lands after the request's start — the offset accounts for the request's trim, the elapsed time since `startAt`, and its speed. */
149
+ playBuffer(buffer, req, clipTime) {
150
+ const startTime = this.context.currentTime;
151
+ const playback = playbackOf(req, buffer.duration);
152
+ const elapsed = Math.max(0, clipTime - req.startAt);
153
+ const source = this.context.createBufferSource();
154
+ source.buffer = buffer;
155
+ const loopRegion = applyLoop(source, req);
156
+ if (playback.rate !== 1)
157
+ source.playbackRate.value = playback.rate;
158
+ applySpeedToPlaybackRate(source.playbackRate, playback.speedRamp, playback.sourceLength, playback.clipDuration, startTime, elapsed);
159
+ const graph = buildAudioEffectGraph(this.context, source, req.effects ?? [], startTime, playback.clipDuration, elapsed);
160
+ const level = buildLevel(this.context, graph.output, req, playback, startTime, elapsed);
161
+ // Into this clip's bus chain, or straight to the master when it has none.
162
+ // The chain is shared: two clips on one bus reach the same nodes, which is
163
+ // what makes a bus effect one tail rather than one per clip.
164
+ level.output.connect(this.graph.buses.inputFor(req.bus, startTime, clipTime));
165
+ const audioOffset = req.trimStart + sourceOffsetOf(playback, elapsed);
166
+ source.start(startTime, wrapIntoRegion(loopRegion, audioOffset));
167
+ for (const osc of graph.oscillators)
168
+ osc.start(startTime);
169
+ source.onended = () => {
170
+ if (this.active.get(req.id)?.source === source) {
171
+ this.active.delete(req.id);
172
+ }
173
+ };
174
+ this.active.set(req.id, {
175
+ source,
176
+ request: req,
177
+ nodes: [...graph.nodes, ...level.nodes],
178
+ oscillators: graph.oscillators,
179
+ });
180
+ }
181
+ stopSource(id, active) {
182
+ try {
183
+ active.source.onended = null;
184
+ active.source.stop();
185
+ for (const osc of active.oscillators)
186
+ osc.stop();
187
+ active.source.disconnect();
188
+ for (const node of active.nodes)
189
+ node.disconnect();
190
+ }
191
+ catch {
192
+ // already stopped
193
+ }
194
+ this.active.delete(id);
195
+ }
196
+ dispose() {
197
+ if (this.disposed)
198
+ return;
199
+ this.disposed = true;
200
+ super.dispose();
201
+ this.bufferCache.clear();
202
+ this.decoding.clear();
203
+ this.scheduled.length = 0;
204
+ this._graph?.buses.dispose();
205
+ try {
206
+ this._graph?.master.disconnect();
207
+ }
208
+ catch {
209
+ // already disconnected
210
+ }
211
+ const owned = this.borrowed ? null : this._context;
212
+ if (owned && owned.state !== "closed") {
213
+ owned.close();
214
+ }
215
+ }
216
+ }
217
+ //# sourceMappingURL=player.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"player.js","sourceRoot":"","sources":["../../src/audio/player.ts"],"names":[],"mappings":"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","sourcesContent":["import { AudioSink, AudioRequest } from \"@motionscript/core\";\nimport { AudioBusGraph } from \"./bus-graph\";\nimport { applyLoop, wrapIntoRegion } from \"./loop-region\";\nimport { applySpeedToPlaybackRate, buildAudioEffectGraph, buildLevel, playbackOf, sourceOffsetOf } from \"./effect-graph\";\n\ntype ActiveSource = {\n source: AudioBufferSourceNode;\n request: AudioRequest;\n /** Effect and level nodes to disconnect on teardown. */\n nodes: AudioNode[];\n /** Tremolo LFOs to stop on teardown. */\n oscillators: OscillatorNode[];\n};\n\n/**\n * Web Audio implementation of {@link AudioSink} — decodes and caches source\n * buffers, then schedules/plays {@link AudioRequest}s as `AudioBufferSourceNode`s\n * through a shared master gain (global mute and volume). `schedule` registers the\n * active set of requests for the current frame; `syncTo` then starts/stops\n * sources to match `clipTime`, so playback follows scrubbing and seeks.\n */\nexport class WebAudioSink extends AudioSink {\n private readonly borrowed: AudioContext | undefined;\n private _context: AudioContext | null = null;\n private bufferCache = new Map<string, AudioBuffer>();\n private decoding = new Map<string, Promise<void>>();\n private active = new Map<string, ActiveSource>();\n private scheduled: AudioRequest[] = [];\n private disposed: boolean = false;\n /** The master gain and the live bus chains (shared across every clip that names one), built with the context. */\n private _graph: { master: GainNode; buses: AudioBusGraph } | null = null;\n private muted = false;\n private volume = 1;\n\n constructor(context?: AudioContext) {\n super();\n this.borrowed = context;\n }\n\n /**\n * The context, created on first use rather than with the sink: a new\n * AudioContext opens the audio device (50–65 ms on a desktop), and a\n * session that never plays or decodes sound never needs one.\n */\n getContext(): AudioContext {\n this._context ??= this.borrowed ?? new AudioContext();\n return this._context;\n }\n\n private get context(): AudioContext {\n return this.getContext();\n }\n\n private get graph(): { master: GainNode; buses: AudioBusGraph } {\n if (!this._graph) {\n const master = this.context.createGain();\n master.connect(this.context.destination);\n this._graph = { master, buses: new AudioBusGraph(this.context, master) };\n this.applyMasterGain();\n }\n return this._graph;\n }\n\n /**\n * Resume the AudioContext. Must be called from within a user gesture\n * (e.g. a click handler) the first time, or browsers keep the context\n * suspended and no audio is heard.\n */\n unlock(): void {\n if (this.context.state === \"suspended\") {\n this.context.resume();\n }\n }\n\n has(src: string): boolean {\n return this.bufferCache.has(src);\n }\n\n async append(src: string, data: ArrayBuffer): Promise<void> {\n if (this.bufferCache.has(src)) return;\n const existing = this.decoding.get(src);\n if (existing) return existing;\n\n // decodeAudioData detaches `data` — that's fine, the caller in\n // AssetManager.fetchAudio discards its reference immediately after\n // awaiting append. Skipping the defensive .slice(0) saves a multi-MB\n // allocation per audio source.\n const job = this.context.decodeAudioData(data).then((buffer) => {\n this.bufferCache.set(src, buffer);\n }).finally(() => {\n this.decoding.delete(src);\n });\n this.decoding.set(src, job);\n await job;\n }\n\n /** Drops cached buffers and stops active sources for srcs/requests no longer referenced — called when the asset set changes (e.g. seeking across scene boundaries). */\n retain(keep: ReadonlySet<string>): void {\n for (const src of [...this.bufferCache.keys()]) {\n if (!keep.has(src)) this.bufferCache.delete(src);\n }\n for (const [id, active] of this.active) {\n if (!keep.has(active.request.src)) this.stopSource(id, active);\n }\n }\n\n /** Replaces the active request set; stops any currently-playing source whose request is no longer scheduled. Actual start/stop-to-match-time happens in `syncTo`. */\n schedule(requests: readonly AudioRequest[]): void {\n const next = new Set(requests.map(r => r.id));\n for (const [id, active] of this.active) {\n if (!next.has(id)) this.stopSource(id, active);\n }\n this.scheduled = requests.slice();\n }\n\n /** Starts sources whose `[startAt, endAt)` window now contains `clipTime` (computing the correct buffer offset for the seek point) and stops any that have fallen outside it. */\n syncTo(clipTime: number): void {\n const liveIds = new Set<string>();\n\n for (const req of this.scheduled) {\n // `endAt` is `Infinity` for an open clip, so the comparison needs no\n // nullish fallback — see AudioRequest.endAt.\n const isInWindow = clipTime >= req.startAt && clipTime < req.endAt;\n if (!isInWindow) continue;\n\n liveIds.add(req.id);\n if (!this.active.has(req.id)) {\n const buffer = this.bufferCache.get(req.src);\n if (buffer) this.playBuffer(buffer, req, clipTime);\n }\n }\n\n for (const [id, active] of this.active) {\n if (!liveIds.has(id)) this.stopSource(id, active);\n }\n }\n\n async play(time: number, _speed: number, _reverse: boolean): Promise<void> {\n if (this.context.state === \"suspended\") {\n await this.context.resume();\n }\n this.syncTo(time);\n }\n\n stop(): void {\n for (const [id, active] of this.active) {\n this.stopSource(id, active);\n }\n // A bus's curves are scheduled against the clock of the run that built\n // it, so the next play, from wherever it lands, builds them again.\n this._graph?.buses.dispose();\n }\n\n setMuted(muted: boolean): void {\n this.muted = muted;\n this.applyMasterGain();\n }\n\n setVolume(volume: number): void {\n this.volume = Math.max(0, volume);\n this.applyMasterGain();\n }\n\n /** Mute and volume are held separately so unmuting restores the set level. */\n private applyMasterGain(): void {\n if (this._graph) this._graph.master.gain.value = this.muted ? 0 : this.volume;\n }\n\n /** Starts a buffer source mid-clip if `clipTime` lands after the request's start — the offset accounts for the request's trim, the elapsed time since `startAt`, and its speed. */\n private playBuffer(buffer: AudioBuffer, req: AudioRequest, clipTime: number): void {\n const startTime = this.context.currentTime;\n const playback = playbackOf(req, buffer.duration);\n const elapsed = Math.max(0, clipTime - req.startAt);\n\n const source = this.context.createBufferSource();\n source.buffer = buffer;\n const loopRegion = applyLoop(source, req);\n if (playback.rate !== 1) source.playbackRate.value = playback.rate;\n applySpeedToPlaybackRate(\n source.playbackRate, playback.speedRamp, playback.sourceLength, playback.clipDuration, startTime, elapsed,\n );\n\n const graph = buildAudioEffectGraph(\n this.context, source, req.effects ?? [], startTime, playback.clipDuration, elapsed,\n );\n const level = buildLevel(this.context, graph.output, req, playback, startTime, elapsed);\n // Into this clip's bus chain, or straight to the master when it has none.\n // The chain is shared: two clips on one bus reach the same nodes, which is\n // what makes a bus effect one tail rather than one per clip.\n level.output.connect(this.graph.buses.inputFor(req.bus, startTime, clipTime));\n\n const audioOffset = req.trimStart + sourceOffsetOf(playback, elapsed);\n source.start(startTime, wrapIntoRegion(loopRegion, audioOffset));\n for (const osc of graph.oscillators) osc.start(startTime);\n\n source.onended = () => {\n if (this.active.get(req.id)?.source === source) {\n this.active.delete(req.id);\n }\n };\n\n this.active.set(req.id, {\n source,\n request: req,\n nodes: [...graph.nodes, ...level.nodes],\n oscillators: graph.oscillators,\n });\n }\n\n private stopSource(id: string, active: ActiveSource): void {\n try {\n active.source.onended = null;\n active.source.stop();\n for (const osc of active.oscillators) osc.stop();\n active.source.disconnect();\n for (const node of active.nodes) node.disconnect();\n } catch {\n // already stopped\n }\n this.active.delete(id);\n }\n\n dispose(): void {\n if (this.disposed) return;\n this.disposed = true;\n super.dispose();\n this.bufferCache.clear();\n this.decoding.clear();\n this.scheduled.length = 0;\n this._graph?.buses.dispose();\n try {\n this._graph?.master.disconnect();\n } catch {\n // already disconnected\n }\n const owned = this.borrowed ? null : this._context;\n if (owned && owned.state !== \"closed\") {\n owned.close();\n }\n }\n}\n"]}