@aethervtt/shard-audio-editor 0.0.0-stage → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +2 -2
- package/dist/dom.d.ts +6 -0
- package/dist/dom.js +11 -0
- package/dist/drawing.d.ts +2 -0
- package/dist/drawing.js +12 -0
- package/dist/icons.d.ts +1 -0
- package/dist/icons.js +29 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/memory.d.ts +13 -0
- package/dist/memory.js +93 -0
- package/dist/methods.d.ts +4 -0
- package/dist/methods.js +79 -0
- package/dist/model.d.ts +61 -0
- package/dist/model.js +560 -0
- package/dist/panels/common.d.ts +33 -0
- package/dist/panels/common.js +1556 -0
- package/dist/panels/diagnostics.d.ts +2 -0
- package/dist/panels/diagnostics.js +7 -0
- package/dist/panels/event.d.ts +2 -0
- package/dist/panels/event.js +7 -0
- package/dist/panels/library.d.ts +2 -0
- package/dist/panels/library.js +7 -0
- package/dist/panels/mixer.d.ts +2 -0
- package/dist/panels/mixer.js +7 -0
- package/dist/panels/music.d.ts +2 -0
- package/dist/panels/music.js +7 -0
- package/dist/panels/parameters.d.ts +2 -0
- package/dist/panels/parameters.js +7 -0
- package/dist/preview-export.d.ts +68 -0
- package/dist/preview-export.js +143 -0
- package/dist/preview-render.d.ts +2 -0
- package/dist/preview-render.js +6 -0
- package/dist/preview-runtime.d.ts +114 -0
- package/dist/preview-runtime.js +414 -0
- package/dist/preview.d.ts +25 -0
- package/dist/preview.js +296 -0
- package/dist/project.d.ts +15 -0
- package/dist/project.js +332 -0
- package/dist/schema.d.ts +22 -0
- package/dist/schema.js +155 -0
- package/dist/styles.d.ts +2 -0
- package/dist/styles.js +192 -0
- package/dist/types.d.ts +113 -0
- package/dist/types.js +0 -0
- package/dist/waveform.d.ts +16 -0
- package/dist/waveform.js +154 -0
- package/package.json +83 -4
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AudioClips,
|
|
3
|
+
AudioCollection,
|
|
4
|
+
AudioEvent,
|
|
5
|
+
AudioListener,
|
|
6
|
+
AudioMixer,
|
|
7
|
+
AudioSnapshot,
|
|
8
|
+
acquireAudioSnapshot,
|
|
9
|
+
audioClip,
|
|
10
|
+
audioEventsPlugin,
|
|
11
|
+
audioMixerPlugin,
|
|
12
|
+
audioMusicPlugin,
|
|
13
|
+
audioPlugin,
|
|
14
|
+
audioState,
|
|
15
|
+
configureAudioMixer,
|
|
16
|
+
describeAudio,
|
|
17
|
+
MusicProgram,
|
|
18
|
+
playAudioEvent,
|
|
19
|
+
prepareAudio,
|
|
20
|
+
prepareMusic,
|
|
21
|
+
releaseAudioSnapshot,
|
|
22
|
+
setAudioUserBus,
|
|
23
|
+
startMusic,
|
|
24
|
+
stopAudioEvent,
|
|
25
|
+
stopMusic
|
|
26
|
+
} from "@aethervtt/shard-audio";
|
|
27
|
+
import {
|
|
28
|
+
ShardError
|
|
29
|
+
} from "@aethervtt/shard-core";
|
|
30
|
+
import { App } from "@aethervtt/shard-runtime";
|
|
31
|
+
import { Transform, TransformPlugin } from "@aethervtt/shard-transform";
|
|
32
|
+
import { getAudioEditorSchema, materializeAudioDocument, validateAudioDocument } from "./schema.js";
|
|
33
|
+
async function createPreviewRuntime(options) {
|
|
34
|
+
const supplied = options.backend;
|
|
35
|
+
if (supplied && (!supplied.runtime?.capabilities.sessions || !supplied.createSession))
|
|
36
|
+
throw new ShardError(
|
|
37
|
+
"audio-editor/unsupported-capability",
|
|
38
|
+
"The supplied backend cannot isolate an editor session",
|
|
39
|
+
{
|
|
40
|
+
hint: "Supply a backend with createSession capability, or let the editor own a browser backend."
|
|
41
|
+
}
|
|
42
|
+
);
|
|
43
|
+
const backend = supplied ?? options.createBackend?.() ?? (await import("@aethervtt/shard-platform-web")).createWebAudioBackend();
|
|
44
|
+
let auditionError;
|
|
45
|
+
const unlock = () => {
|
|
46
|
+
const failed = (cause) => {
|
|
47
|
+
auditionError = new ShardError(
|
|
48
|
+
"audio-editor/playback-blocked",
|
|
49
|
+
"Audio output could not resume",
|
|
50
|
+
{
|
|
51
|
+
cause,
|
|
52
|
+
hint: "Check the host audio device and browser permission, then press Play again."
|
|
53
|
+
}
|
|
54
|
+
);
|
|
55
|
+
};
|
|
56
|
+
try {
|
|
57
|
+
const pending = backend.resume?.();
|
|
58
|
+
void pending?.then(() => {
|
|
59
|
+
auditionError = void 0;
|
|
60
|
+
}, failed);
|
|
61
|
+
} catch (cause) {
|
|
62
|
+
failed(cause);
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
unlock();
|
|
66
|
+
const app = new App().addPlugin(
|
|
67
|
+
TransformPlugin,
|
|
68
|
+
audioPlugin({ backend }),
|
|
69
|
+
audioEventsPlugin(),
|
|
70
|
+
audioMixerPlugin(),
|
|
71
|
+
audioMusicPlugin()
|
|
72
|
+
);
|
|
73
|
+
try {
|
|
74
|
+
await app.init();
|
|
75
|
+
} catch (error) {
|
|
76
|
+
await app.dispose();
|
|
77
|
+
if (!supplied) backend.dispose?.();
|
|
78
|
+
throw error;
|
|
79
|
+
}
|
|
80
|
+
const owned = audioState(app.world).backend;
|
|
81
|
+
if (supplied && owned === supplied) {
|
|
82
|
+
await app.dispose();
|
|
83
|
+
throw new ShardError(
|
|
84
|
+
"audio-editor/unsupported-capability",
|
|
85
|
+
"The supplied backend cannot isolate an editor session",
|
|
86
|
+
{
|
|
87
|
+
hint: "Supply a backend with createSession capability, or let the editor own a browser backend."
|
|
88
|
+
}
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
const events = /* @__PURE__ */ new Set();
|
|
92
|
+
const emitters = /* @__PURE__ */ new Map();
|
|
93
|
+
const listener = app.world.spawn(Transform, AudioListener);
|
|
94
|
+
const music = /* @__PURE__ */ new Set();
|
|
95
|
+
const snapshots = /* @__PURE__ */ new Set();
|
|
96
|
+
const requests = /* @__PURE__ */ new Set();
|
|
97
|
+
const releases = [];
|
|
98
|
+
const removals = [];
|
|
99
|
+
const definitions = /* @__PURE__ */ new Map();
|
|
100
|
+
let disposed = false;
|
|
101
|
+
let serial = 0;
|
|
102
|
+
let last = owned.runtime.clock.now();
|
|
103
|
+
const tick = () => {
|
|
104
|
+
if (disposed) return;
|
|
105
|
+
const now = owned.runtime.clock.now();
|
|
106
|
+
app.update(Math.max(0, Math.min(0.1, now - last)));
|
|
107
|
+
last = now;
|
|
108
|
+
};
|
|
109
|
+
const wake = owned.runtime.clock.wake(tick, 1 / 60);
|
|
110
|
+
const assertLive = (signal) => {
|
|
111
|
+
if (disposed || signal?.aborted)
|
|
112
|
+
throw new ShardError("audio-editor/disposed", "Audio preview was cancelled");
|
|
113
|
+
};
|
|
114
|
+
const stop = () => {
|
|
115
|
+
for (const id of events) stopAudioEvent(app.world, id, { release: 0 });
|
|
116
|
+
for (const id of music) stopMusic(app.world, id);
|
|
117
|
+
for (const id of snapshots) releaseAudioSnapshot(app.world, id);
|
|
118
|
+
events.clear();
|
|
119
|
+
for (const entity of emitters.values()) app.world.despawn(entity);
|
|
120
|
+
emitters.clear();
|
|
121
|
+
music.clear();
|
|
122
|
+
snapshots.clear();
|
|
123
|
+
while (releases.length) releases.pop()();
|
|
124
|
+
while (removals.length) removals.pop()();
|
|
125
|
+
definitions.clear();
|
|
126
|
+
requests.clear();
|
|
127
|
+
owned.runtime.collect();
|
|
128
|
+
};
|
|
129
|
+
async function install(document, signal) {
|
|
130
|
+
const resolved = /* @__PURE__ */ new Map();
|
|
131
|
+
const pending = /* @__PURE__ */ new Set();
|
|
132
|
+
const scope = options.assets.scope;
|
|
133
|
+
const docsScope = options.documents.scope;
|
|
134
|
+
const check = () => {
|
|
135
|
+
assertLive(signal);
|
|
136
|
+
if (scope !== options.assets.scope || docsScope !== options.documents.scope)
|
|
137
|
+
throw new ShardError(
|
|
138
|
+
"audio-editor/asset-unavailable",
|
|
139
|
+
"Host scope changed during preparation"
|
|
140
|
+
);
|
|
141
|
+
};
|
|
142
|
+
async function reference(id, type) {
|
|
143
|
+
check();
|
|
144
|
+
const key = `${type}:${id}`;
|
|
145
|
+
const existing = resolved.get(key);
|
|
146
|
+
if (existing) return existing;
|
|
147
|
+
if (pending.has(key))
|
|
148
|
+
throw new ShardError("audio-editor/invalid-document", "Cyclic audio document references", {
|
|
149
|
+
path: id
|
|
150
|
+
});
|
|
151
|
+
if (type !== "AudioClip") {
|
|
152
|
+
const dependency = id === document.id ? document : await options.documents.read(id, signal);
|
|
153
|
+
if (dependency.type !== type)
|
|
154
|
+
throw new ShardError(
|
|
155
|
+
"audio-editor/invalid-document",
|
|
156
|
+
`Expected ${type}, received ${dependency.type}`,
|
|
157
|
+
{ path: id }
|
|
158
|
+
);
|
|
159
|
+
return installDocument(dependency);
|
|
160
|
+
}
|
|
161
|
+
const metadata = await options.assets.get(id, signal);
|
|
162
|
+
check();
|
|
163
|
+
const guid = JSON.stringify([scope, id, metadata.revision]);
|
|
164
|
+
const store = app.world.resource(AudioClips);
|
|
165
|
+
const result = { type, guid, path: void 0 };
|
|
166
|
+
if (store.has(guid)) {
|
|
167
|
+
resolved.set(key, result);
|
|
168
|
+
return result;
|
|
169
|
+
}
|
|
170
|
+
const content = await options.assets.resolve(id, signal);
|
|
171
|
+
try {
|
|
172
|
+
check();
|
|
173
|
+
if (!content.bytes)
|
|
174
|
+
throw new ShardError(
|
|
175
|
+
"audio-editor/unsupported-capability",
|
|
176
|
+
"Audition requires authorized audio bytes",
|
|
177
|
+
{
|
|
178
|
+
path: id,
|
|
179
|
+
hint: "Have the host resolver return encoded bytes for decoded preview playback."
|
|
180
|
+
}
|
|
181
|
+
);
|
|
182
|
+
store.set(
|
|
183
|
+
guid,
|
|
184
|
+
audioClip(Uint8Array.from(content.bytes), { ...content.settings, id: guid })
|
|
185
|
+
);
|
|
186
|
+
removals.push(() => {
|
|
187
|
+
store.delete(guid);
|
|
188
|
+
});
|
|
189
|
+
resolved.set(key, result);
|
|
190
|
+
return result;
|
|
191
|
+
} finally {
|
|
192
|
+
content.release?.();
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
async function field(field2, value) {
|
|
196
|
+
if (field2.kind === "handle" && value) {
|
|
197
|
+
const ref = value;
|
|
198
|
+
const id = ref.guid ?? ref.path;
|
|
199
|
+
if (!id)
|
|
200
|
+
throw new ShardError("audio-editor/asset-unavailable", "Audio reference has no host ID");
|
|
201
|
+
return reference(id, field2.assetType);
|
|
202
|
+
}
|
|
203
|
+
if (field2.kind === "list" && Array.isArray(value)) {
|
|
204
|
+
const items = [];
|
|
205
|
+
for (const item of value) items.push(await fieldValue(field2.item, item));
|
|
206
|
+
return items;
|
|
207
|
+
}
|
|
208
|
+
if (field2.kind === "struct") {
|
|
209
|
+
const result = { ...value };
|
|
210
|
+
for (const [name, child] of Object.entries(field2.fields))
|
|
211
|
+
result[name] = await fieldValue(child, result[name]);
|
|
212
|
+
return result;
|
|
213
|
+
}
|
|
214
|
+
return value;
|
|
215
|
+
}
|
|
216
|
+
const fieldValue = field;
|
|
217
|
+
async function installDocument(source) {
|
|
218
|
+
const key = `${source.type}:${source.id}`;
|
|
219
|
+
pending.add(key);
|
|
220
|
+
const schema = getAudioEditorSchema(source.type);
|
|
221
|
+
if (!schema)
|
|
222
|
+
throw new ShardError("audio-editor/unsupported-schema", `Cannot audition ${source.type}`);
|
|
223
|
+
source = await materializeAudioDocument(source, options.documents, signal);
|
|
224
|
+
const { errors } = await validateAudioDocument(
|
|
225
|
+
source,
|
|
226
|
+
options.documents,
|
|
227
|
+
options.assets,
|
|
228
|
+
signal
|
|
229
|
+
);
|
|
230
|
+
if (errors.length) throw errors[0];
|
|
231
|
+
const value = schema.deserialize(source.value);
|
|
232
|
+
for (const [name, definition] of Object.entries(schema.fields))
|
|
233
|
+
value[name] = await fieldValue(definition, value[name]);
|
|
234
|
+
check();
|
|
235
|
+
const signature = JSON.stringify([docsScope, source.id, source.revision, value]);
|
|
236
|
+
const previous = definitions.get(signature);
|
|
237
|
+
if (previous) {
|
|
238
|
+
resolved.set(key, previous);
|
|
239
|
+
pending.delete(key);
|
|
240
|
+
return previous;
|
|
241
|
+
}
|
|
242
|
+
if (definitions.size >= 256)
|
|
243
|
+
throw new ShardError("audio/capacity", "Preview retains at most 256 document revisions", {
|
|
244
|
+
hint: "Stop audition to release pinned preview revisions before preparing more."
|
|
245
|
+
});
|
|
246
|
+
const guid = JSON.stringify([docsScope, source.id, source.revision, ++serial]);
|
|
247
|
+
const store = app.world.initResource(schema.store);
|
|
248
|
+
store.set(guid, value);
|
|
249
|
+
removals.push(() => {
|
|
250
|
+
store.delete(guid);
|
|
251
|
+
});
|
|
252
|
+
const ref = { type: source.type, guid, path: void 0 };
|
|
253
|
+
definitions.set(signature, ref);
|
|
254
|
+
resolved.set(key, ref);
|
|
255
|
+
pending.delete(key);
|
|
256
|
+
return ref;
|
|
257
|
+
}
|
|
258
|
+
return installDocument(document);
|
|
259
|
+
}
|
|
260
|
+
async function playDocument(document, signal) {
|
|
261
|
+
assertLive(signal);
|
|
262
|
+
unlock();
|
|
263
|
+
const ref = await install(document, signal);
|
|
264
|
+
assertLive(signal);
|
|
265
|
+
const world = app.world;
|
|
266
|
+
if (document.type === AudioEvent.name || document.type === AudioCollection.name) {
|
|
267
|
+
const lease = await prepareAudio(world, ref);
|
|
268
|
+
if (disposed || signal?.aborted) {
|
|
269
|
+
lease.release();
|
|
270
|
+
assertLive(signal);
|
|
271
|
+
}
|
|
272
|
+
if (document.type === AudioCollection.name) {
|
|
273
|
+
releases.push(() => lease.release());
|
|
274
|
+
return { lease: lease.id, prepared: true };
|
|
275
|
+
}
|
|
276
|
+
let entity;
|
|
277
|
+
let instance;
|
|
278
|
+
try {
|
|
279
|
+
entity = world.spawn(Transform);
|
|
280
|
+
instance = playAudioEvent(world, ref, { entity });
|
|
281
|
+
emitters.set(instance, entity);
|
|
282
|
+
events.add(instance);
|
|
283
|
+
await Promise.resolve();
|
|
284
|
+
await Promise.resolve();
|
|
285
|
+
assertLive(signal);
|
|
286
|
+
tick();
|
|
287
|
+
releases.push(() => lease.release());
|
|
288
|
+
return { instance };
|
|
289
|
+
} catch (error) {
|
|
290
|
+
if (instance !== void 0) {
|
|
291
|
+
stopAudioEvent(world, instance, { release: 0 });
|
|
292
|
+
events.delete(instance);
|
|
293
|
+
emitters.delete(instance);
|
|
294
|
+
}
|
|
295
|
+
if (entity !== void 0 && world.isAlive(entity)) world.despawn(entity);
|
|
296
|
+
lease.release();
|
|
297
|
+
throw error;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
if (document.type === MusicProgram.name) {
|
|
301
|
+
const program = await prepareMusic(world, ref);
|
|
302
|
+
try {
|
|
303
|
+
assertLive(signal);
|
|
304
|
+
const transport = startMusic(world, program);
|
|
305
|
+
music.add(transport);
|
|
306
|
+
return { transport };
|
|
307
|
+
} finally {
|
|
308
|
+
program.release();
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
if (document.type === AudioMixer.name) {
|
|
312
|
+
await configureAudioMixer(world, ref);
|
|
313
|
+
assertLive(signal);
|
|
314
|
+
return { configured: true };
|
|
315
|
+
}
|
|
316
|
+
if (document.type === AudioSnapshot.name) {
|
|
317
|
+
const snapshot = await acquireAudioSnapshot(world, ref);
|
|
318
|
+
if (disposed || signal?.aborted) {
|
|
319
|
+
releaseAudioSnapshot(world, snapshot);
|
|
320
|
+
assertLive(signal);
|
|
321
|
+
}
|
|
322
|
+
snapshots.add(snapshot);
|
|
323
|
+
return { snapshot };
|
|
324
|
+
}
|
|
325
|
+
return { prepared: true };
|
|
326
|
+
}
|
|
327
|
+
return {
|
|
328
|
+
unlock,
|
|
329
|
+
async play(document, signal) {
|
|
330
|
+
assertLive(signal);
|
|
331
|
+
if (requests.size >= 256)
|
|
332
|
+
throw new ShardError("audio/capacity", "Preview retains at most 256 audition requests", {
|
|
333
|
+
hint: "Press Stop to release audition voices, preparations and pinned revisions before playing more."
|
|
334
|
+
});
|
|
335
|
+
const request = /* @__PURE__ */ Symbol();
|
|
336
|
+
requests.add(request);
|
|
337
|
+
try {
|
|
338
|
+
return await playDocument(document, signal);
|
|
339
|
+
} catch (error) {
|
|
340
|
+
requests.delete(request);
|
|
341
|
+
throw error;
|
|
342
|
+
}
|
|
343
|
+
},
|
|
344
|
+
command(name, params) {
|
|
345
|
+
assertLive();
|
|
346
|
+
if (name === "audio.user.set") {
|
|
347
|
+
if (typeof params.bus !== "string")
|
|
348
|
+
throw new ShardError("audio-editor/invalid-document", "User category requires a bus name");
|
|
349
|
+
setAudioUserBus(app.world, params.bus, params);
|
|
350
|
+
return { updated: true };
|
|
351
|
+
}
|
|
352
|
+
if (name === "audio.listener.set" || name === "audio.emitter.set") {
|
|
353
|
+
const position = params.position;
|
|
354
|
+
if (!Array.isArray(position) || position.length !== 3 || position.some((value) => typeof value !== "number" || !Number.isFinite(value)))
|
|
355
|
+
throw new ShardError(
|
|
356
|
+
"audio-editor/invalid-document",
|
|
357
|
+
"Position requires three finite coordinates",
|
|
358
|
+
{ path: "/position" }
|
|
359
|
+
);
|
|
360
|
+
const entity = name === "audio.listener.set" ? listener : emitters.get(Number(params.instance));
|
|
361
|
+
if (entity === void 0)
|
|
362
|
+
throw new ShardError(
|
|
363
|
+
"audio-editor/invalid-document",
|
|
364
|
+
"This preview does not own the emitter"
|
|
365
|
+
);
|
|
366
|
+
app.world.set(entity, Transform, { translation: position });
|
|
367
|
+
tick();
|
|
368
|
+
return { updated: true };
|
|
369
|
+
}
|
|
370
|
+
const method = app.methods.find((item) => item.name === name);
|
|
371
|
+
if (!method || !name.startsWith("audio."))
|
|
372
|
+
throw new ShardError("audio-editor/unsupported-capability", `Unknown audio command ${name}`);
|
|
373
|
+
if ([
|
|
374
|
+
"audio.play",
|
|
375
|
+
"audio.prepare",
|
|
376
|
+
"audio.music.start",
|
|
377
|
+
"audio.snapshot.acquire",
|
|
378
|
+
"audio.preview"
|
|
379
|
+
].includes(name))
|
|
380
|
+
throw new ShardError(
|
|
381
|
+
"audio-editor/unsupported-capability",
|
|
382
|
+
"Use editor audition to resolve host assets"
|
|
383
|
+
);
|
|
384
|
+
const errors = method.params.validate(params);
|
|
385
|
+
if (errors.length) throw errors[0];
|
|
386
|
+
return method.handler(
|
|
387
|
+
{ app, world: app.world },
|
|
388
|
+
method.params.deserialize(params)
|
|
389
|
+
);
|
|
390
|
+
},
|
|
391
|
+
inspect() {
|
|
392
|
+
assertLive();
|
|
393
|
+
tick();
|
|
394
|
+
return {
|
|
395
|
+
...structuredClone(describeAudio(app.world)),
|
|
396
|
+
...auditionError ? { auditionError: auditionError.toJSON() } : {}
|
|
397
|
+
};
|
|
398
|
+
},
|
|
399
|
+
stop,
|
|
400
|
+
dispose() {
|
|
401
|
+
if (disposed) return;
|
|
402
|
+
stop();
|
|
403
|
+
disposed = true;
|
|
404
|
+
wake();
|
|
405
|
+
owned.dispose?.();
|
|
406
|
+
const closing = app.dispose();
|
|
407
|
+
if (!supplied) backend.dispose?.();
|
|
408
|
+
return closing;
|
|
409
|
+
}
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
export {
|
|
413
|
+
createPreviewRuntime
|
|
414
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type JsonObject } from '@aethervtt/shard-core';
|
|
2
|
+
import type { AudioBackend } from '@aethervtt/shard-platform';
|
|
3
|
+
import type { AudioAssetCatalog, AudioDocument, AudioDocumentStore, AudioPreviewAdapter } from './types.js';
|
|
4
|
+
import { type AudioWaveformDecoder } from './waveform.js';
|
|
5
|
+
export interface IsolatedAudioPreviewOptions {
|
|
6
|
+
assets: AudioAssetCatalog;
|
|
7
|
+
documents: AudioDocumentStore;
|
|
8
|
+
/** Borrowed host root. It must support isolated sessions; the root is never closed. */
|
|
9
|
+
backend?: AudioBackend;
|
|
10
|
+
waveformDecoder?: AudioWaveformDecoder;
|
|
11
|
+
waveformBytes?: number;
|
|
12
|
+
/** The host implements the existing audio preview render-plan/export capability. */
|
|
13
|
+
export?(document: AudioDocument, plan: JsonObject, signal?: AbortSignal): Promise<unknown>;
|
|
14
|
+
}
|
|
15
|
+
/** Silent construction. Engine/platform implementation loads only on explicit Play. */
|
|
16
|
+
export declare function createIsolatedAudioPreview(options: IsolatedAudioPreviewOptions): AudioPreviewAdapter;
|
|
17
|
+
export interface AttachedAudioPreviewOptions {
|
|
18
|
+
target: string;
|
|
19
|
+
call(name: string, params: Record<string, unknown>, signal?: AbortSignal): Promise<unknown>;
|
|
20
|
+
liveControl?: boolean;
|
|
21
|
+
/** Explicit host capability; absence never causes an implicit save. */
|
|
22
|
+
applyDraft?(document: AudioDocument, signal?: AbortSignal): Promise<string>;
|
|
23
|
+
}
|
|
24
|
+
/** Protocol bridge has no privilege to close the target or stop its unrelated instances. */
|
|
25
|
+
export declare function createAttachedAudioPreview(options: AttachedAudioPreviewOptions): AudioPreviewAdapter;
|