@aethervtt/shard-audio-preview 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.
- package/LICENSE +21 -0
- package/README.md +2 -2
- package/dist/index.d.ts +62 -0
- package/dist/index.js +317 -0
- package/package.json +36 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Patrick Dias
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @aethervtt/shard-audio-preview
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Part of [Shard](https://github.com/aethervtt/shard), a WebGPU-first 2D/3D game engine in TypeScript. See the repository for documentation.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { type JsonObject } from '@aethervtt/shard-core';
|
|
2
|
+
export interface AudioRenderBundle {
|
|
3
|
+
/** Serialized AudioPreviewPlan; runtime handles must pass through its serializer first. */
|
|
4
|
+
plan: JsonObject;
|
|
5
|
+
assets: {
|
|
6
|
+
guid: string;
|
|
7
|
+
path: string;
|
|
8
|
+
type: string;
|
|
9
|
+
value: unknown;
|
|
10
|
+
}[];
|
|
11
|
+
}
|
|
12
|
+
export declare function renderAudioBundle(bundle: AudioRenderBundle, options?: {
|
|
13
|
+
signal?: AbortSignal;
|
|
14
|
+
progress?: (value: Record<string, unknown>) => void;
|
|
15
|
+
}): Promise<{
|
|
16
|
+
wav: Uint8Array<ArrayBufferLike>;
|
|
17
|
+
measured: {
|
|
18
|
+
peak: number;
|
|
19
|
+
rms: number;
|
|
20
|
+
clipped: number;
|
|
21
|
+
nonfinite: number;
|
|
22
|
+
sampleRate: number;
|
|
23
|
+
channels: number;
|
|
24
|
+
frames: number;
|
|
25
|
+
duration: number;
|
|
26
|
+
};
|
|
27
|
+
trace: {
|
|
28
|
+
zones: {
|
|
29
|
+
at: number;
|
|
30
|
+
buses: string[];
|
|
31
|
+
}[];
|
|
32
|
+
events: {
|
|
33
|
+
entries: import("@aethervtt/shard-audio").AudioEventTraceEntry[];
|
|
34
|
+
oldestCursor: number;
|
|
35
|
+
nextCursor: number;
|
|
36
|
+
truncated: boolean;
|
|
37
|
+
};
|
|
38
|
+
music: {
|
|
39
|
+
entries: import("@aethervtt/shard-audio").AudioEventTraceEntry[];
|
|
40
|
+
oldestCursor: number;
|
|
41
|
+
nextCursor: number;
|
|
42
|
+
truncated: boolean;
|
|
43
|
+
};
|
|
44
|
+
controls: {
|
|
45
|
+
index: number;
|
|
46
|
+
requestedAt: number;
|
|
47
|
+
appliedAt: number;
|
|
48
|
+
}[];
|
|
49
|
+
sources: {
|
|
50
|
+
id: number;
|
|
51
|
+
presentation: Readonly<import("@aethervtt/shard-platform").AudioPresentation> | undefined;
|
|
52
|
+
}[];
|
|
53
|
+
controlRate: number;
|
|
54
|
+
quantumFrames: number;
|
|
55
|
+
};
|
|
56
|
+
capabilities: Readonly<import("@aethervtt/shard-platform").AudioCapabilities>;
|
|
57
|
+
backend: {
|
|
58
|
+
kind: string;
|
|
59
|
+
userAgent: string;
|
|
60
|
+
sampleRate: number;
|
|
61
|
+
};
|
|
62
|
+
}>;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
import { assetServer } from "@aethervtt/shard-assets";
|
|
2
|
+
import {
|
|
3
|
+
AudioAcoustics,
|
|
4
|
+
AudioClips,
|
|
5
|
+
AudioListener,
|
|
6
|
+
AudioPreviewPlan,
|
|
7
|
+
AudioTransport,
|
|
8
|
+
acquireAudioSnapshot,
|
|
9
|
+
audioAcousticsPlugin,
|
|
10
|
+
audioEvents,
|
|
11
|
+
audioEventsPlugin,
|
|
12
|
+
audioMixerPlugin,
|
|
13
|
+
audioMusicPlugin,
|
|
14
|
+
audioPlugin,
|
|
15
|
+
audioState,
|
|
16
|
+
configureAudioMixer,
|
|
17
|
+
describeAudioEvents,
|
|
18
|
+
describeMusic,
|
|
19
|
+
pauseMusic,
|
|
20
|
+
playAudioEvent,
|
|
21
|
+
playMusicStinger,
|
|
22
|
+
prepareAudio,
|
|
23
|
+
prepareMusic,
|
|
24
|
+
prepareMusicSection,
|
|
25
|
+
ReverbZone,
|
|
26
|
+
releaseAudioSnapshot,
|
|
27
|
+
requestMusicSection,
|
|
28
|
+
resumeMusic,
|
|
29
|
+
seekMusic,
|
|
30
|
+
setAudioParameter,
|
|
31
|
+
setAudioSnapshotWeight,
|
|
32
|
+
setBus,
|
|
33
|
+
setMusicParameter,
|
|
34
|
+
startMusic,
|
|
35
|
+
stopAudioEvent,
|
|
36
|
+
stopMusic,
|
|
37
|
+
updateMusic,
|
|
38
|
+
validateAudioPreview
|
|
39
|
+
} from "@aethervtt/shard-audio";
|
|
40
|
+
import { ShardError } from "@aethervtt/shard-core";
|
|
41
|
+
import {
|
|
42
|
+
audioBufferWav,
|
|
43
|
+
createWebAudioBackend,
|
|
44
|
+
measureAudioBuffer
|
|
45
|
+
} from "@aethervtt/shard-platform-web";
|
|
46
|
+
import { App } from "@aethervtt/shard-runtime";
|
|
47
|
+
import { Transform, TransformPlugin } from "@aethervtt/shard-transform";
|
|
48
|
+
async function renderAudioBundle(bundle, options = {}) {
|
|
49
|
+
if (typeof OfflineAudioContext === "undefined" || typeof OfflineAudioContext.prototype.suspend !== "function" || typeof OfflineAudioContext.prototype.resume !== "function")
|
|
50
|
+
throw new ShardError(
|
|
51
|
+
"audio-preview/unsupported-capability",
|
|
52
|
+
"This browser lacks suspended offline rendering",
|
|
53
|
+
{
|
|
54
|
+
hint: "Use a browser with OfflineAudioContext suspend/resume, or provide a host export adapter."
|
|
55
|
+
}
|
|
56
|
+
);
|
|
57
|
+
const plan = AudioPreviewPlan.deserialize(bundle.plan);
|
|
58
|
+
validateAudioPreview(plan);
|
|
59
|
+
const frames = Math.ceil((plan.duration + plan.tailSeconds) * plan.sampleRate);
|
|
60
|
+
if (frames * plan.channels * 6 + 44 > 64 * 1024 * 1024)
|
|
61
|
+
throw new ShardError("audio/preview-unavailable", "Preview output exceeds 64 MiB");
|
|
62
|
+
const context = new OfflineAudioContext(plan.channels, frames, plan.sampleRate);
|
|
63
|
+
const backend = createWebAudioBackend({
|
|
64
|
+
context,
|
|
65
|
+
fixedListener: true,
|
|
66
|
+
maxVoices: 4096,
|
|
67
|
+
maxHostVoices: 4096
|
|
68
|
+
});
|
|
69
|
+
const app = new App({ seed: plan.seed }).addPlugin(
|
|
70
|
+
TransformPlugin,
|
|
71
|
+
audioPlugin({ backend, maxVoices: 4096 }),
|
|
72
|
+
audioEventsPlugin(),
|
|
73
|
+
audioMixerPlugin(),
|
|
74
|
+
...plan.zones.length ? [audioAcousticsPlugin()] : [],
|
|
75
|
+
audioMusicPlugin()
|
|
76
|
+
);
|
|
77
|
+
const world = app.world;
|
|
78
|
+
try {
|
|
79
|
+
await app.init();
|
|
80
|
+
const assets = assetServer(world), runtime = audioState(world).backend.runtime;
|
|
81
|
+
for (const item of bundle.assets)
|
|
82
|
+
assets.virtual(item.guid, item.path, item.type, () => item.value, { lazy: true });
|
|
83
|
+
for (const item of bundle.assets) await assets.load(item.guid);
|
|
84
|
+
const decoded = [];
|
|
85
|
+
for (const clip of world.resource(AudioClips).values())
|
|
86
|
+
decoded.push(await runtime.prepare(clip));
|
|
87
|
+
if (plan.mixer) await configureAudioMixer(world, plan.mixer);
|
|
88
|
+
const eventLeases = [];
|
|
89
|
+
for (const event of plan.events) eventLeases.push(await prepareAudio(world, event.asset));
|
|
90
|
+
const programs = /* @__PURE__ */ new Map();
|
|
91
|
+
for (const entry of plan.music) {
|
|
92
|
+
const program = await prepareMusic(world, entry.asset);
|
|
93
|
+
for (const section of program.definition.sections)
|
|
94
|
+
await prepareMusicSection(world, program, section.id);
|
|
95
|
+
programs.set(entry.id, program);
|
|
96
|
+
}
|
|
97
|
+
const eventIds = /* @__PURE__ */ new Map(), musicIds = /* @__PURE__ */ new Map(), emitters = /* @__PURE__ */ new Map(), snapshots = /* @__PURE__ */ new Map();
|
|
98
|
+
for (const entry of plan.music)
|
|
99
|
+
musicIds.set(
|
|
100
|
+
entry.id,
|
|
101
|
+
startMusic(world, programs.get(entry.id), {
|
|
102
|
+
section: entry.section || void 0,
|
|
103
|
+
at: entry.at,
|
|
104
|
+
parameters: entry.parameters
|
|
105
|
+
})
|
|
106
|
+
);
|
|
107
|
+
for (const entry of plan.zones)
|
|
108
|
+
world.spawn([Transform, { translation: entry.position }], [ReverbZone, entry.zone]);
|
|
109
|
+
const zoneTrace = [];
|
|
110
|
+
let lastZones = "";
|
|
111
|
+
const listener = world.spawn(Transform, AudioListener);
|
|
112
|
+
const voices = plan.voices.map(
|
|
113
|
+
(voice) => runtime.play({
|
|
114
|
+
clip: world.resource(AudioClips).require(plan.clips.find((c) => c.id === voice.clip).asset),
|
|
115
|
+
bus: voice.bus,
|
|
116
|
+
when: voice.at,
|
|
117
|
+
duration: voice.duration || (voice.loop ? plan.duration - voice.at : void 0),
|
|
118
|
+
offset: voice.offset,
|
|
119
|
+
loop: voice.loop,
|
|
120
|
+
gain: 10 ** (voice.gainDb / 20),
|
|
121
|
+
pitch: voice.pitch,
|
|
122
|
+
x: voice.position[0],
|
|
123
|
+
y: voice.position[1],
|
|
124
|
+
z: voice.position[2],
|
|
125
|
+
spatial: voice.spatial ? {
|
|
126
|
+
panning: "equal-power",
|
|
127
|
+
distanceModel: "inverse",
|
|
128
|
+
refDistance: voice.minDistance,
|
|
129
|
+
maxDistance: voice.maxDistance,
|
|
130
|
+
rolloffFactor: 1
|
|
131
|
+
} : null
|
|
132
|
+
})
|
|
133
|
+
);
|
|
134
|
+
for (const command of plan.automation)
|
|
135
|
+
runtime.automate(voices[plan.voices.findIndex((v) => v.id === command.voice)], command);
|
|
136
|
+
const controls = plan.controls.map((control, index) => ({ control, index })).sort((a, b) => a.control.at - b.control.at || a.index - b.index);
|
|
137
|
+
const applied = [];
|
|
138
|
+
let nextControl = 0, last = 0, stopped = false;
|
|
139
|
+
const target = (map, name) => {
|
|
140
|
+
const id = map.get(name);
|
|
141
|
+
if (id === void 0)
|
|
142
|
+
throw new ShardError("audio/preview-unavailable", `Preview target ${name} has not started`);
|
|
143
|
+
return id;
|
|
144
|
+
};
|
|
145
|
+
async function tick() {
|
|
146
|
+
const now = context.currentTime;
|
|
147
|
+
if (options.signal?.aborted)
|
|
148
|
+
throw new ShardError("audio-preview/disposed", "Offline preview cancelled");
|
|
149
|
+
options.progress?.({ stage: "tick", time: now, nextControl });
|
|
150
|
+
for (const entry of plan.events)
|
|
151
|
+
if (!eventIds.has(entry.id) && now >= entry.at) {
|
|
152
|
+
const entity = world.spawn(Transform);
|
|
153
|
+
world.set(entity, Transform, { translation: entry.position });
|
|
154
|
+
emitters.set(entry.id, entity);
|
|
155
|
+
eventIds.set(
|
|
156
|
+
entry.id,
|
|
157
|
+
playAudioEvent(world, entry.asset, {
|
|
158
|
+
entity,
|
|
159
|
+
parameters: entry.parameters,
|
|
160
|
+
clock: entry.clock,
|
|
161
|
+
pitchLinked: entry.pitchLinked
|
|
162
|
+
})
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
while (nextControl < controls.length && controls[nextControl].control.at <= now) {
|
|
166
|
+
const { control: c, index } = controls[nextControl++], opts = c.options;
|
|
167
|
+
switch (c.kind) {
|
|
168
|
+
case "event.parameter":
|
|
169
|
+
setAudioParameter(world, target(eventIds, c.target), c.name, c.value);
|
|
170
|
+
break;
|
|
171
|
+
case "global.parameter":
|
|
172
|
+
setAudioParameter(world, null, c.name, c.value);
|
|
173
|
+
break;
|
|
174
|
+
case "event.stop":
|
|
175
|
+
stopAudioEvent(world, target(eventIds, c.target), opts);
|
|
176
|
+
break;
|
|
177
|
+
case "event.position": {
|
|
178
|
+
const entity = emitters.get(c.target);
|
|
179
|
+
if (entity === void 0)
|
|
180
|
+
throw new ShardError("audio/preview-unavailable", "Emitter has not started");
|
|
181
|
+
world.set(entity, Transform, { translation: c.position });
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
case "listener.position":
|
|
185
|
+
world.set(listener, Transform, { translation: c.position });
|
|
186
|
+
break;
|
|
187
|
+
case "music.parameter":
|
|
188
|
+
setMusicParameter(world, target(musicIds, c.target), c.name, c.value);
|
|
189
|
+
break;
|
|
190
|
+
case "music.section":
|
|
191
|
+
requestMusicSection(world, target(musicIds, c.target), c.name, opts);
|
|
192
|
+
break;
|
|
193
|
+
case "music.stinger":
|
|
194
|
+
await playMusicStinger(world, target(musicIds, c.target), c.name, opts);
|
|
195
|
+
break;
|
|
196
|
+
case "music.pause":
|
|
197
|
+
pauseMusic(world, target(musicIds, c.target));
|
|
198
|
+
break;
|
|
199
|
+
case "music.resume":
|
|
200
|
+
resumeMusic(world, target(musicIds, c.target));
|
|
201
|
+
break;
|
|
202
|
+
case "music.seek":
|
|
203
|
+
seekMusic(world, target(musicIds, c.target), Number(c.value));
|
|
204
|
+
break;
|
|
205
|
+
case "snapshot.acquire":
|
|
206
|
+
if (snapshots.has(c.target)) releaseAudioSnapshot(world, snapshots.get(c.target));
|
|
207
|
+
snapshots.set(
|
|
208
|
+
c.target,
|
|
209
|
+
await acquireAudioSnapshot(world, c.asset, c.value == null ? 1 : Number(c.value))
|
|
210
|
+
);
|
|
211
|
+
break;
|
|
212
|
+
case "snapshot.set":
|
|
213
|
+
setAudioSnapshotWeight(world, target(snapshots, c.target), Number(c.value));
|
|
214
|
+
break;
|
|
215
|
+
case "snapshot.release":
|
|
216
|
+
releaseAudioSnapshot(world, target(snapshots, c.target));
|
|
217
|
+
break;
|
|
218
|
+
case "bus.volume":
|
|
219
|
+
setBus(world, c.target, { volume: Number(c.value) });
|
|
220
|
+
break;
|
|
221
|
+
case "transport.pause":
|
|
222
|
+
world.resource(AudioTransport).paused = Boolean(c.value);
|
|
223
|
+
break;
|
|
224
|
+
}
|
|
225
|
+
applied.push({ index, requestedAt: c.at, appliedAt: now });
|
|
226
|
+
}
|
|
227
|
+
if (!stopped && now >= plan.duration) {
|
|
228
|
+
stopped = true;
|
|
229
|
+
for (const instance of audioEvents(world).instances)
|
|
230
|
+
if (instance) stopAudioEvent(world, instance.id, { release: 0 });
|
|
231
|
+
for (const id of musicIds.values()) stopMusic(world, id);
|
|
232
|
+
for (const id of voices) runtime.cancel(id);
|
|
233
|
+
}
|
|
234
|
+
app.update(now - last);
|
|
235
|
+
const acoustics = world.tryResource(AudioAcoustics);
|
|
236
|
+
if (acoustics) {
|
|
237
|
+
const buses = acoustics.slots.filter((slot) => slot.weight > 0).map((slot) => slot.bus);
|
|
238
|
+
const key = buses.join(",");
|
|
239
|
+
if (key !== lastZones) {
|
|
240
|
+
zoneTrace.push({ at: now, buses });
|
|
241
|
+
lastZones = key;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
updateMusic(world);
|
|
245
|
+
last = now;
|
|
246
|
+
await runtime.flush();
|
|
247
|
+
}
|
|
248
|
+
await tick();
|
|
249
|
+
const total = frames / plan.sampleRate;
|
|
250
|
+
let rendering;
|
|
251
|
+
let step = 1;
|
|
252
|
+
for (; ; ) {
|
|
253
|
+
const wanted = Math.min(
|
|
254
|
+
step / 60,
|
|
255
|
+
nextControl < controls.length ? controls[nextControl].control.at : Infinity,
|
|
256
|
+
...plan.events.filter((e) => !eventIds.has(e.id)).map((e) => e.at),
|
|
257
|
+
...plan.music.filter((e) => !musicIds.has(e.id)).map((e) => e.at),
|
|
258
|
+
stopped ? Infinity : plan.duration
|
|
259
|
+
);
|
|
260
|
+
const at = Math.max(
|
|
261
|
+
Math.round(last * plan.sampleRate / 128) + 1,
|
|
262
|
+
Math.ceil(wanted * plan.sampleRate / 128)
|
|
263
|
+
) * 128 / plan.sampleRate;
|
|
264
|
+
if (at >= total) break;
|
|
265
|
+
options.progress?.({ stage: "suspend", time: context.currentTime, target: at });
|
|
266
|
+
const suspended = context.suspend(at + 0.5 / plan.sampleRate);
|
|
267
|
+
if (!rendering) rendering = context.startRendering();
|
|
268
|
+
else await context.resume();
|
|
269
|
+
await suspended;
|
|
270
|
+
await tick();
|
|
271
|
+
step = Math.floor(context.currentTime * 60) + 1;
|
|
272
|
+
}
|
|
273
|
+
if (!rendering) rendering = context.startRendering();
|
|
274
|
+
else await context.resume();
|
|
275
|
+
const buffer = await rendering, measured = measureAudioBuffer(buffer);
|
|
276
|
+
if (measured.nonfinite)
|
|
277
|
+
throw new ShardError("audio/preview-unavailable", "Rendered nonfinite samples");
|
|
278
|
+
const trace = {
|
|
279
|
+
zones: zoneTrace,
|
|
280
|
+
events: describeAudioEvents(world).trace,
|
|
281
|
+
music: describeMusic(world).trace,
|
|
282
|
+
controls: applied,
|
|
283
|
+
sources: voices.map((id) => ({ id, presentation: runtime.presentation(id) })),
|
|
284
|
+
controlRate: 60,
|
|
285
|
+
quantumFrames: 128
|
|
286
|
+
};
|
|
287
|
+
for (const lease of decoded) lease.release();
|
|
288
|
+
for (const lease of eventLeases) lease.release();
|
|
289
|
+
for (const program of programs.values()) program.release();
|
|
290
|
+
return {
|
|
291
|
+
wav: audioBufferWav(buffer),
|
|
292
|
+
measured,
|
|
293
|
+
trace,
|
|
294
|
+
capabilities: runtime.capabilities,
|
|
295
|
+
backend: {
|
|
296
|
+
kind: "web-audio-offline",
|
|
297
|
+
userAgent: navigator.userAgent,
|
|
298
|
+
sampleRate: context.sampleRate
|
|
299
|
+
}
|
|
300
|
+
};
|
|
301
|
+
} finally {
|
|
302
|
+
try {
|
|
303
|
+
await app.dispose();
|
|
304
|
+
} finally {
|
|
305
|
+
backend.dispose?.();
|
|
306
|
+
if (options.signal?.aborted && context.state === "suspended") {
|
|
307
|
+
try {
|
|
308
|
+
await context.resume();
|
|
309
|
+
} catch {
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
export {
|
|
316
|
+
renderAudioBundle
|
|
317
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,38 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aethervtt/shard-audio-preview",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"sideEffects": false,
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"default": "./dist/index.js"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist",
|
|
15
|
+
"LICENSE",
|
|
16
|
+
"README.md"
|
|
17
|
+
],
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"@aethervtt/shard-assets": "0.2.0",
|
|
20
|
+
"@aethervtt/shard-audio": "0.2.0",
|
|
21
|
+
"@aethervtt/shard-core": "0.2.0",
|
|
22
|
+
"@aethervtt/shard-platform-web": "0.2.0",
|
|
23
|
+
"@aethervtt/shard-runtime": "0.2.0",
|
|
24
|
+
"@aethervtt/shard-transform": "0.2.0"
|
|
25
|
+
},
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "git+https://github.com/aethervtt/shard.git",
|
|
29
|
+
"directory": "packages/audio-preview"
|
|
30
|
+
},
|
|
31
|
+
"publishConfig": {
|
|
32
|
+
"access": "public",
|
|
33
|
+
"provenance": true
|
|
34
|
+
},
|
|
35
|
+
"shard": {
|
|
36
|
+
"buildSha": "32c22a3944ed23f5d1baa90bf7cc9fae724f5fd8"
|
|
37
|
+
}
|
|
38
|
+
}
|