@aethervtt/shard-audio-editor 0.0.0-stage → 0.2.0

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 (49) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +2 -2
  3. package/dist/dom.d.ts +6 -0
  4. package/dist/dom.js +11 -0
  5. package/dist/drawing.d.ts +2 -0
  6. package/dist/drawing.js +12 -0
  7. package/dist/icons.d.ts +1 -0
  8. package/dist/icons.js +29 -0
  9. package/dist/index.d.ts +2 -0
  10. package/dist/index.js +2 -0
  11. package/dist/memory.d.ts +13 -0
  12. package/dist/memory.js +93 -0
  13. package/dist/methods.d.ts +4 -0
  14. package/dist/methods.js +79 -0
  15. package/dist/model.d.ts +61 -0
  16. package/dist/model.js +560 -0
  17. package/dist/panels/common.d.ts +33 -0
  18. package/dist/panels/common.js +1556 -0
  19. package/dist/panels/diagnostics.d.ts +2 -0
  20. package/dist/panels/diagnostics.js +7 -0
  21. package/dist/panels/event.d.ts +2 -0
  22. package/dist/panels/event.js +7 -0
  23. package/dist/panels/library.d.ts +2 -0
  24. package/dist/panels/library.js +7 -0
  25. package/dist/panels/mixer.d.ts +2 -0
  26. package/dist/panels/mixer.js +7 -0
  27. package/dist/panels/music.d.ts +2 -0
  28. package/dist/panels/music.js +7 -0
  29. package/dist/panels/parameters.d.ts +2 -0
  30. package/dist/panels/parameters.js +7 -0
  31. package/dist/preview-export.d.ts +68 -0
  32. package/dist/preview-export.js +143 -0
  33. package/dist/preview-render.d.ts +2 -0
  34. package/dist/preview-render.js +6 -0
  35. package/dist/preview-runtime.d.ts +114 -0
  36. package/dist/preview-runtime.js +414 -0
  37. package/dist/preview.d.ts +25 -0
  38. package/dist/preview.js +296 -0
  39. package/dist/project.d.ts +15 -0
  40. package/dist/project.js +332 -0
  41. package/dist/schema.d.ts +22 -0
  42. package/dist/schema.js +155 -0
  43. package/dist/styles.d.ts +2 -0
  44. package/dist/styles.js +192 -0
  45. package/dist/types.d.ts +113 -0
  46. package/dist/types.js +0 -0
  47. package/dist/waveform.d.ts +16 -0
  48. package/dist/waveform.js +154 -0
  49. package/package.json +83 -4
@@ -0,0 +1,296 @@
1
+ import { ShardError } from "@aethervtt/shard-core";
2
+ import { validateAudioDocument } from "./schema.js";
3
+ import { createAudioWaveformCache } from "./waveform.js";
4
+ function createIsolatedAudioPreview(options) {
5
+ let runtime;
6
+ let opening;
7
+ let disposed = false;
8
+ let scope = JSON.stringify([options.assets.scope, options.documents.scope]);
9
+ let controller = new AbortController();
10
+ let factory;
11
+ let createBackend;
12
+ const preload = Promise.all([
13
+ import("./preview-runtime.js"),
14
+ import("@aethervtt/shard-platform-web")
15
+ ]).then(([runtimeModule, platform]) => {
16
+ factory = runtimeModule.createPreviewRuntime;
17
+ createBackend = platform.createWebAudioBackend;
18
+ });
19
+ void preload.catch(() => {
20
+ });
21
+ const waveforms = createAudioWaveformCache(options.assets, {
22
+ decode: options.waveformDecoder,
23
+ maxBytes: options.waveformBytes
24
+ });
25
+ const canExport = !!options.export || typeof OfflineAudioContext !== "undefined" && typeof OfflineAudioContext.prototype.suspend === "function" && typeof OfflineAudioContext.prototype.resume === "function";
26
+ const assertLive = (signal) => {
27
+ if (disposed || signal?.aborted)
28
+ throw new ShardError("audio-editor/disposed", "Audio audition is closed or cancelled", {
29
+ hint: "Use an active editor preview session."
30
+ });
31
+ };
32
+ const reset = () => {
33
+ controller.abort();
34
+ controller = new AbortController();
35
+ runtime?.dispose();
36
+ runtime = void 0;
37
+ opening = void 0;
38
+ waveforms.clear();
39
+ };
40
+ const checkScope = () => {
41
+ const current = JSON.stringify([options.assets.scope, options.documents.scope]);
42
+ if (scope !== current) {
43
+ reset();
44
+ scope = current;
45
+ }
46
+ };
47
+ const invalidate = () => {
48
+ const current = JSON.stringify([options.assets.scope, options.documents.scope]);
49
+ if (current !== scope) {
50
+ reset();
51
+ scope = current;
52
+ return;
53
+ }
54
+ controller.abort();
55
+ controller = new AbortController();
56
+ opening = void 0;
57
+ waveforms.clear();
58
+ };
59
+ const unsubscribe = options.assets.subscribe?.(invalidate);
60
+ const unsubscribeDocuments = options.documents.subscribe?.(invalidate);
61
+ async function ensure(signal) {
62
+ assertLive(signal);
63
+ checkScope();
64
+ if (runtime) return runtime;
65
+ if (!opening) {
66
+ const token = controller.signal;
67
+ const start = async () => {
68
+ assertLive(token);
69
+ const value = await factory({ ...options, createBackend });
70
+ if (disposed || token.aborted) {
71
+ value.dispose();
72
+ assertLive(token);
73
+ }
74
+ runtime = value;
75
+ return value;
76
+ };
77
+ opening = (factory ? start() : preload.then(start)).catch((error) => {
78
+ if (!token.aborted) opening = void 0;
79
+ throw error;
80
+ });
81
+ }
82
+ return opening;
83
+ }
84
+ return {
85
+ mode: "audition",
86
+ capabilities: {
87
+ play: true,
88
+ control: true,
89
+ drafts: true,
90
+ waveform: true,
91
+ listener: true,
92
+ emitter: true,
93
+ userControls: true,
94
+ export: canExport
95
+ },
96
+ async play(document, signal) {
97
+ assertLive(signal);
98
+ checkScope();
99
+ const combined = signal ? AbortSignal.any([signal, controller.signal]) : controller.signal;
100
+ runtime?.unlock();
101
+ const pending = ensure(combined);
102
+ void pending.catch(() => {
103
+ });
104
+ const result = await validateAudioDocument(
105
+ document,
106
+ options.documents,
107
+ options.assets,
108
+ combined
109
+ );
110
+ assertLive(combined);
111
+ if (result.errors.length) throw result.errors[0];
112
+ const active = await pending;
113
+ assertLive(combined);
114
+ return active.play(result.effective ?? document, combined);
115
+ },
116
+ stop() {
117
+ assertLive();
118
+ controller.abort();
119
+ controller = new AbortController();
120
+ opening = void 0;
121
+ runtime?.stop();
122
+ },
123
+ inspect() {
124
+ assertLive();
125
+ checkScope();
126
+ return runtime?.inspect() ?? { mode: "audition", state: "idle", owned: true };
127
+ },
128
+ async command(name, params) {
129
+ return (await ensure()).command(name, params);
130
+ },
131
+ waveform(id, bins, signal) {
132
+ assertLive(signal);
133
+ checkScope();
134
+ return waveforms.get(id, bins, signal);
135
+ },
136
+ ...canExport ? {
137
+ async export(document, plan, signal) {
138
+ assertLive(signal);
139
+ checkScope();
140
+ const combined = signal ? AbortSignal.any([signal, controller.signal]) : controller.signal;
141
+ const result = await validateAudioDocument(
142
+ document,
143
+ options.documents,
144
+ options.assets,
145
+ combined
146
+ );
147
+ assertLive(combined);
148
+ if (result.errors.length) throw result.errors[0];
149
+ if (options.export) return options.export(result.effective ?? document, plan, combined);
150
+ const { exportAudioEditorPreview } = await import("./preview-export.js");
151
+ assertLive(combined);
152
+ return exportAudioEditorPreview(options, result.effective ?? document, plan, combined);
153
+ }
154
+ } : {},
155
+ dispose() {
156
+ if (disposed) return;
157
+ disposed = true;
158
+ reset();
159
+ unsubscribe?.();
160
+ unsubscribeDocuments?.();
161
+ waveforms.dispose();
162
+ }
163
+ };
164
+ }
165
+ const inspection = /* @__PURE__ */ new Set([
166
+ "audio.describe",
167
+ "audio.events.inspect",
168
+ "audio.mix.inspect",
169
+ "audio.music.inspect",
170
+ "audio.meters",
171
+ "audio.log"
172
+ ]);
173
+ function createAttachedAudioPreview(options) {
174
+ const ownedEvents = /* @__PURE__ */ new Set();
175
+ const ownedMusic = /* @__PURE__ */ new Set();
176
+ const ownedSnapshots = /* @__PURE__ */ new Set();
177
+ let disposed = false;
178
+ let controller = new AbortController();
179
+ const assertLive = () => {
180
+ if (disposed) throw new ShardError("audio-editor/disposed", "Runtime attachment is closed");
181
+ };
182
+ const requireControl = () => {
183
+ assertLive();
184
+ if (!options.liveControl)
185
+ throw new ShardError(
186
+ "audio-editor/read-only",
187
+ `Attachment ${options.target} permits inspection only`,
188
+ { hint: "The host must explicitly grant live audio control." }
189
+ );
190
+ };
191
+ const stop = async () => {
192
+ controller.abort();
193
+ controller = new AbortController();
194
+ for (const instance of ownedEvents) await options.call("audio.stop", { instance, release: 0 });
195
+ for (const transport of ownedMusic) await options.call("audio.music.stop", { transport });
196
+ for (const snapshot of ownedSnapshots)
197
+ await options.call("audio.snapshot.release", { snapshot });
198
+ ownedEvents.clear();
199
+ ownedMusic.clear();
200
+ ownedSnapshots.clear();
201
+ };
202
+ return {
203
+ mode: "attached",
204
+ target: options.target,
205
+ capabilities: {
206
+ play: !!options.liveControl && !!options.applyDraft,
207
+ control: !!options.liveControl,
208
+ drafts: !!options.applyDraft,
209
+ waveform: false,
210
+ export: false
211
+ },
212
+ async play(document, signal) {
213
+ requireControl();
214
+ if (!options.applyDraft)
215
+ throw new ShardError(
216
+ "audio-editor/unsupported-capability",
217
+ "This attachment cannot apply unsaved drafts",
218
+ { hint: "Use isolated audition, or supply a host draft-apply capability." }
219
+ );
220
+ const method = document.type === "audio/AudioEvent" ? "audio.play" : document.type === "audio/MusicProgram" ? "audio.music.start" : document.type === "audio/AudioSnapshot" ? "audio.snapshot.acquire" : void 0;
221
+ if (!method)
222
+ throw new ShardError(
223
+ "audio-editor/unsupported-capability",
224
+ "This document type has no attached play command"
225
+ );
226
+ const combined = signal ? AbortSignal.any([signal, controller.signal]) : controller.signal;
227
+ const asset = await options.applyDraft(document, combined);
228
+ if (combined.aborted)
229
+ throw new ShardError("audio-editor/disposed", "Draft attachment was cancelled");
230
+ assertLive();
231
+ const result = await options.call(
232
+ method,
233
+ method === "audio.play" ? { event: asset } : { asset },
234
+ combined
235
+ );
236
+ if (disposed || combined.aborted) {
237
+ if (typeof result.instance === "number")
238
+ await options.call("audio.stop", { instance: result.instance, release: 0 });
239
+ if (typeof result.transport === "number")
240
+ await options.call("audio.music.stop", { transport: result.transport });
241
+ if (typeof result.snapshot === "number")
242
+ await options.call("audio.snapshot.release", { snapshot: result.snapshot });
243
+ throw new ShardError("audio-editor/disposed", "Attached playback was cancelled");
244
+ }
245
+ if (typeof result.instance === "number") ownedEvents.add(result.instance);
246
+ if (typeof result.transport === "number") ownedMusic.add(result.transport);
247
+ if (typeof result.snapshot === "number") ownedSnapshots.add(result.snapshot);
248
+ return result;
249
+ },
250
+ stop,
251
+ async inspect() {
252
+ assertLive();
253
+ return {
254
+ target: options.target,
255
+ mode: "attached",
256
+ control: !!options.liveControl,
257
+ audio: await options.call("audio.describe", {}, controller.signal)
258
+ };
259
+ },
260
+ async command(name, params) {
261
+ assertLive();
262
+ if (!inspection.has(name)) {
263
+ requireControl();
264
+ const event = Number(params.instance);
265
+ const music = Number(params.transport);
266
+ const snapshot = Number(params.snapshot);
267
+ const allowed = ["audio.stop", "audio.parameter.set"].includes(name) && ownedEvents.has(event) || [
268
+ "audio.music.parameter",
269
+ "audio.music.transition",
270
+ "audio.music.stinger",
271
+ "audio.music.pause",
272
+ "audio.music.resume",
273
+ "audio.music.seek",
274
+ "audio.music.cancel",
275
+ "audio.music.stop"
276
+ ].includes(name) && ownedMusic.has(music) || ["audio.snapshot.set", "audio.snapshot.release"].includes(name) && ownedSnapshots.has(snapshot);
277
+ if (!allowed)
278
+ throw new ShardError(
279
+ "audio-editor/unsupported-capability",
280
+ "Command is outside this editor attachment\u2019s owned instances"
281
+ );
282
+ }
283
+ return options.call(name, params, controller.signal);
284
+ },
285
+ async dispose() {
286
+ if (disposed) return;
287
+ disposed = true;
288
+ controller.abort();
289
+ await stop();
290
+ }
291
+ };
292
+ }
293
+ export {
294
+ createAttachedAudioPreview,
295
+ createIsolatedAudioPreview
296
+ };
@@ -0,0 +1,15 @@
1
+ import type { Platform } from '@aethervtt/shard-platform';
2
+ import type { AudioAssetCatalog, AudioDocumentStore } from './types.js';
3
+ /**
4
+ * A project-file host. It uses the project's manifest and audio import probe, never a DOM picker.
5
+ * Commits serialize within this adapter and preflight source revisions. PlatformFileSystem has no
6
+ * cross-process CAS primitive: concurrent external writers must be coordinated by the host.
7
+ * Multi-document atomic commits are explicitly unsupported.
8
+ */
9
+ export declare function openAudioProject(platform: Platform, scope?: string): Promise<{
10
+ name: string;
11
+ documents: AudioDocumentStore;
12
+ assets: AudioAssetCatalog;
13
+ refresh: () => Promise<void>;
14
+ dispose(): void;
15
+ }>;
@@ -0,0 +1,332 @@
1
+ import { sha256Hex } from "@aethervtt/shard-assets";
2
+ import { AudioImportSettings, audioClip } from "@aethervtt/shard-audio";
3
+ import { ShardError } from "@aethervtt/shard-core";
4
+ import { loadProject } from "@aethervtt/shard-project/manifest";
5
+ const types = {
6
+ "audio-event": "audio/AudioEvent",
7
+ "audio-mixer": "audio/AudioMixer",
8
+ "audio-music": "audio/MusicProgram",
9
+ "audio-parameters": "audio/AudioParameters",
10
+ "audio-snapshot": "audio/AudioSnapshot",
11
+ "audio-collection": "audio/AudioCollection"
12
+ };
13
+ function revision(text) {
14
+ return sha256Hex(new TextEncoder().encode(text));
15
+ }
16
+ async function openAudioProject(platform, scope = "project") {
17
+ const { manifest } = await loadProject(platform);
18
+ const fs = platform.fs;
19
+ if (!fs.list)
20
+ throw new ShardError(
21
+ "audio-editor/unsupported-capability",
22
+ "This host cannot list project assets"
23
+ );
24
+ const paths = /* @__PURE__ */ new Map();
25
+ const aliases = /* @__PURE__ */ new Map();
26
+ const listeners = /* @__PURE__ */ new Set();
27
+ let catalogError;
28
+ let disposed = false;
29
+ const check = (signal) => {
30
+ if (disposed) throw new ShardError("audio-editor/disposed", "The project adapter is closed");
31
+ signal?.throwIfAborted();
32
+ };
33
+ const scan = async (dir, index = paths, sourceAliases = aliases) => {
34
+ for (const entry of await fs.list(dir)) {
35
+ const path = `${dir}/${entry.name}`;
36
+ if (entry.kind === "dir") await scan(path, index, sourceAliases);
37
+ else {
38
+ const suffix = /\.([^.]+)\.json$/.exec(path)?.[1];
39
+ const type = suffix ? types[suffix] : /\.(wav|ogg|mp3|flac|opus)$/i.test(path) ? "AudioClip" : void 0;
40
+ if (!type) continue;
41
+ let id = path;
42
+ if (await fs.exists(`${path}.meta`)) {
43
+ const meta = JSON.parse(await fs.readText(`${path}.meta`));
44
+ if (typeof meta.guid !== "string" || !meta.guid)
45
+ throw new ShardError(
46
+ "audio-editor/invalid-document",
47
+ "Audio sidecar has no stable GUID",
48
+ {
49
+ path: `${path}.meta`,
50
+ hint: "Restore its GUID or re-import the source through the project host."
51
+ }
52
+ );
53
+ id = meta.guid;
54
+ }
55
+ if (index.has(id) && index.get(id).path !== path)
56
+ throw new ShardError("audio-editor/invalid-document", "Audio sources share a GUID", {
57
+ path: `${path}.meta`,
58
+ hint: "Assign a unique GUID to the copied asset before importing it."
59
+ });
60
+ index.set(id, { path, type, name: entry.name });
61
+ sourceAliases.set(path, id);
62
+ }
63
+ }
64
+ };
65
+ for (const root of manifest.assetRoots) await scan(root);
66
+ const lookup = (id) => {
67
+ if (catalogError) throw catalogError;
68
+ const item = paths.get(id) ?? paths.get(aliases.get(id) ?? "");
69
+ if (!item)
70
+ throw new ShardError("audio-editor/asset-unavailable", `Unknown project audio asset: ${id}`, {
71
+ path: id,
72
+ hint: "Import it into a project asset root first."
73
+ });
74
+ return item;
75
+ };
76
+ const read = async (id, signal) => {
77
+ check(signal);
78
+ const item = lookup(id);
79
+ if (item.type === "AudioClip")
80
+ throw new ShardError(
81
+ "audio-editor/invalid-document",
82
+ "A clip is not an authored audio document",
83
+ { path: id }
84
+ );
85
+ const text = await fs.readText(item.path);
86
+ check(signal);
87
+ return {
88
+ id,
89
+ name: item.name,
90
+ type: item.type,
91
+ revision: await revision(text),
92
+ value: JSON.parse(text),
93
+ sourceText: text
94
+ };
95
+ };
96
+ let pending = Promise.resolve();
97
+ const documents = {
98
+ scope,
99
+ atomic: false,
100
+ read,
101
+ subscribe(listener) {
102
+ listeners.add(listener);
103
+ return () => listeners.delete(listener);
104
+ },
105
+ ...fs.writable ? {
106
+ commit(changes, signal) {
107
+ const operation = pending.then(async () => {
108
+ check(signal);
109
+ if (changes.length !== 1)
110
+ throw new ShardError(
111
+ "audio-editor/unsupported-capability",
112
+ "Project files do not support atomic multi-document commits"
113
+ );
114
+ const change = changes[0];
115
+ if (change.expectedRevision === null) {
116
+ if (paths.has(change.document.id) || aliases.has(change.document.id))
117
+ throw new ShardError("audio-editor/conflict", "This document ID already exists");
118
+ const extension = Object.entries(types).find(
119
+ ([, type]) => type === change.document.type
120
+ )?.[0];
121
+ if (!extension)
122
+ throw new ShardError(
123
+ "audio-editor/unsupported-schema",
124
+ "This type is not an audio source document"
125
+ );
126
+ if (!fs.remove)
127
+ throw new ShardError(
128
+ "audio-editor/unsupported-capability",
129
+ "Creating project sources requires rollback-capable file access"
130
+ );
131
+ let directory;
132
+ for (const root of manifest.assetRoots) {
133
+ if (await fs.exists(root)) {
134
+ directory = root;
135
+ break;
136
+ }
137
+ }
138
+ if (!directory)
139
+ throw new ShardError(
140
+ "audio-editor/unsupported-capability",
141
+ "Create a project asset root before adding audio documents"
142
+ );
143
+ const stem = change.document.name.replace(/\.(audio-[a-z]+\.)?json$/i, "").replace(/[^a-zA-Z0-9_-]+/g, "-").replace(/^-+|-+$/g, "") || "audio";
144
+ let path = `${directory}/${stem}.${extension}.json`;
145
+ let suffix = 1;
146
+ while (await fs.exists(path) || await fs.exists(`${path}.meta`))
147
+ path = `${directory}/${stem}-${suffix++}.${extension}.json`;
148
+ const text2 = `${JSON.stringify(change.document.value, null, 2)}
149
+ `;
150
+ check(signal);
151
+ try {
152
+ await fs.writeText(
153
+ `${path}.meta`,
154
+ `${JSON.stringify({ guid: change.document.id, importer: `data/${extension}`, settings: {} }, null, 2)}
155
+ `
156
+ );
157
+ await fs.writeText(path, text2);
158
+ } catch (cause) {
159
+ await fs.remove(path);
160
+ await fs.remove(`${path}.meta`);
161
+ throw cause;
162
+ }
163
+ const name = path.slice(directory.length + 1);
164
+ paths.set(change.document.id, { path, type: change.document.type, name });
165
+ aliases.set(path, change.document.id);
166
+ const saved2 = {
167
+ ...change.document,
168
+ name,
169
+ revision: await revision(text2),
170
+ sourceText: text2
171
+ };
172
+ for (const listener of listeners) listener([saved2.id]);
173
+ return [saved2];
174
+ }
175
+ const current = await read(change.document.id, signal);
176
+ if (current.revision !== change.expectedRevision)
177
+ throw new ShardError(
178
+ "audio-editor/conflict",
179
+ "The source changed outside this editor",
180
+ { path: change.document.id, hint: "Reload the remote version before saving." }
181
+ );
182
+ if (JSON.stringify(current.value) === JSON.stringify(change.document.value))
183
+ return [current];
184
+ check(signal);
185
+ const text = `${JSON.stringify(change.document.value, null, 2)}
186
+ `;
187
+ await fs.writeText(lookup(change.document.id).path, text);
188
+ const saved = { ...change.document, revision: await revision(text), sourceText: text };
189
+ for (const listener of listeners) listener([saved.id]);
190
+ return [saved];
191
+ });
192
+ pending = operation.then(
193
+ () => {
194
+ },
195
+ () => {
196
+ }
197
+ );
198
+ return operation;
199
+ }
200
+ } : {}
201
+ };
202
+ const assets = {
203
+ scope,
204
+ async list(query) {
205
+ check(query.signal);
206
+ if (catalogError) throw catalogError;
207
+ const all = [...paths].sort(([, a], [, b]) => a.name.localeCompare(b.name)).filter(
208
+ ([, item]) => (!query.type || item.type === query.type) && (!query.search || item.name.toLowerCase().includes(query.search.toLowerCase()))
209
+ );
210
+ const offset = Number(query.cursor ?? 0), limit = Math.min(query.limit ?? 50, 100);
211
+ return {
212
+ items: await Promise.all(
213
+ all.slice(offset, offset + limit).map(([id]) => assets.get(id, query.signal))
214
+ ),
215
+ total: all.length,
216
+ ...offset + limit < all.length ? { nextCursor: String(offset + limit) } : {}
217
+ };
218
+ },
219
+ async get(id, signal) {
220
+ check(signal);
221
+ const item = lookup(id);
222
+ const stat = await fs.stat?.(item.path);
223
+ return {
224
+ id,
225
+ name: item.name,
226
+ type: item.type,
227
+ revision: stat ? `${stat.mtime}:${stat.size}` : "project",
228
+ ...stat ? { bytes: stat.size } : {}
229
+ };
230
+ },
231
+ async resolve(id, signal) {
232
+ check(signal);
233
+ const item = lookup(id);
234
+ if (item.type !== "AudioClip")
235
+ throw new ShardError(
236
+ "audio-editor/asset-unavailable",
237
+ "Only audio clips can resolve playable bytes",
238
+ { path: id }
239
+ );
240
+ const bytes = await fs.readBytes(item.path);
241
+ check(signal);
242
+ const meta = await fs.exists(`${item.path}.meta`) ? JSON.parse(await fs.readText(`${item.path}.meta`)) : void 0;
243
+ const settings = AudioImportSettings.deserialize(meta?.settings ?? {});
244
+ audioClip(bytes, { ...settings, id });
245
+ check(signal);
246
+ return { bytes, settings };
247
+ },
248
+ subscribe(listener) {
249
+ listeners.add(listener);
250
+ return () => listeners.delete(listener);
251
+ }
252
+ };
253
+ const refresh = () => {
254
+ const operation = pending.then(async () => {
255
+ check();
256
+ const next = /* @__PURE__ */ new Map();
257
+ const nextAliases = /* @__PURE__ */ new Map();
258
+ for (const root of manifest.assetRoots) await scan(root, next, nextAliases);
259
+ check();
260
+ const changed = [.../* @__PURE__ */ new Set([...paths.keys(), ...next.keys()])];
261
+ paths.clear();
262
+ aliases.clear();
263
+ for (const [id, value] of next) paths.set(id, value);
264
+ for (const [path, id] of nextAliases) aliases.set(path, id);
265
+ catalogError = void 0;
266
+ for (const listener of listeners) listener(changed);
267
+ });
268
+ pending = operation.then(
269
+ () => {
270
+ },
271
+ () => {
272
+ }
273
+ );
274
+ return operation;
275
+ };
276
+ let refreshing = false, refreshAgain = false;
277
+ const queueRefresh = () => {
278
+ if (disposed) return;
279
+ if (refreshing) {
280
+ refreshAgain = true;
281
+ return;
282
+ }
283
+ refreshing = true;
284
+ void refresh().catch((cause) => {
285
+ if (disposed) return;
286
+ catalogError = new ShardError(
287
+ "audio-editor/asset-unavailable",
288
+ "The project audio catalog could not refresh",
289
+ { cause, hint: "Check changed audio sidecars and refresh the project." }
290
+ );
291
+ for (const listener of listeners) listener([...paths.keys()]);
292
+ }).finally(() => {
293
+ refreshing = false;
294
+ if (refreshAgain) {
295
+ refreshAgain = false;
296
+ queueRefresh();
297
+ }
298
+ });
299
+ };
300
+ const watchers = [];
301
+ try {
302
+ if (fs.watch)
303
+ for (const root of manifest.assetRoots) {
304
+ if (!await fs.exists(root)) continue;
305
+ watchers.push(
306
+ await fs.watch(root, (event) => {
307
+ const id = aliases.get(event.path);
308
+ if (event.kind !== "modify" || event.path.endsWith(".meta") || !id) queueRefresh();
309
+ else for (const listener of listeners) listener([id]);
310
+ })
311
+ );
312
+ }
313
+ } catch (error) {
314
+ for (const unwatch of watchers) unwatch();
315
+ throw error;
316
+ }
317
+ return {
318
+ name: manifest.name,
319
+ documents,
320
+ assets,
321
+ refresh,
322
+ dispose() {
323
+ if (disposed) return;
324
+ disposed = true;
325
+ for (const unwatch of watchers) unwatch();
326
+ listeners.clear();
327
+ }
328
+ };
329
+ }
330
+ export {
331
+ openAudioProject
332
+ };
@@ -0,0 +1,22 @@
1
+ import type { DataType } from '@aethervtt/shard-assets';
2
+ import { type Fields, type JsonObject, ShardError } from '@aethervtt/shard-core';
3
+ import type { AudioAssetCatalog, AudioDocument, AudioDocumentStore } from './types.js';
4
+ export declare const audioEditorTypes: readonly DataType[];
5
+ export declare function getAudioEditorSchema(type: string): DataType | undefined;
6
+ export declare function editorError(code: string, message: string, path?: string, cause?: unknown): ShardError;
7
+ export declare function asEditorError(error: unknown): ShardError;
8
+ export declare function clone<T>(value: T): T;
9
+ export declare function equal(a: unknown, b: unknown): boolean;
10
+ export declare function merge(base: JsonObject, overrides: JsonObject): JsonObject;
11
+ export declare function knownFields(fields: Fields, value: JsonObject): JsonObject;
12
+ export interface AudioReference {
13
+ id: string;
14
+ type: string;
15
+ path: string;
16
+ }
17
+ export declare function audioDocumentReferences(fields: Fields, value: JsonObject, prefix?: string): AudioReference[];
18
+ export declare function materializeAudioDocument(document: AudioDocument, documents: AudioDocumentStore, signal?: AbortSignal, chain?: string[]): Promise<AudioDocument>;
19
+ export declare function validateAudioDocument(document: AudioDocument, documents: AudioDocumentStore, assets: AudioAssetCatalog, signal?: AbortSignal): Promise<{
20
+ effective?: AudioDocument;
21
+ errors: ShardError[];
22
+ }>;