@aethervtt/shard-platform-web 0.1.0 → 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/dist/audio-cache.d.ts +44 -0
- package/dist/audio-cache.js +215 -0
- package/dist/audio-compat.d.ts +4 -0
- package/dist/audio-compat.js +34 -0
- package/dist/audio-graph.d.ts +35 -0
- package/dist/audio-graph.js +365 -0
- package/dist/audio-preview.d.ts +59 -0
- package/dist/audio-preview.js +142 -0
- package/dist/audio.d.ts +8 -12
- package/dist/audio.js +835 -248
- package/dist/index.d.ts +1 -0
- package/dist/index.js +22 -1
- package/dist/performance.js +66 -1
- package/package.json +4 -4
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { AudioAutomation, AudioMixerGraph, AudioScheduledVoice } from '@aethervtt/shard-platform';
|
|
2
|
+
export interface AudioRenderPlan {
|
|
3
|
+
duration: number;
|
|
4
|
+
tailSeconds?: number;
|
|
5
|
+
sampleRate?: number;
|
|
6
|
+
channels?: 1 | 2;
|
|
7
|
+
mixer?: AudioMixerGraph;
|
|
8
|
+
voices: AudioScheduledVoice[];
|
|
9
|
+
automation?: {
|
|
10
|
+
voice: number;
|
|
11
|
+
command: AudioAutomation;
|
|
12
|
+
}[];
|
|
13
|
+
maxOutputBytes?: number;
|
|
14
|
+
decodedBudgetBytes?: number;
|
|
15
|
+
}
|
|
16
|
+
/** Measurements use the floating-point render, before PCM16 quantization/clipping. */
|
|
17
|
+
export declare function measureAudioBuffer(buffer: AudioBuffer): {
|
|
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
|
+
export declare function audioBufferWav(buffer: AudioBuffer): Uint8Array;
|
|
28
|
+
/** Same source/graph implementation as live playback. No sound card, microphone or GPU. */
|
|
29
|
+
export declare function renderAudioPreview(plan: AudioRenderPlan): Promise<{
|
|
30
|
+
buffer: AudioBuffer;
|
|
31
|
+
wav: Uint8Array<ArrayBufferLike>;
|
|
32
|
+
measured: {
|
|
33
|
+
peak: number;
|
|
34
|
+
rms: number;
|
|
35
|
+
clipped: number;
|
|
36
|
+
nonfinite: number;
|
|
37
|
+
sampleRate: number;
|
|
38
|
+
channels: number;
|
|
39
|
+
frames: number;
|
|
40
|
+
duration: number;
|
|
41
|
+
};
|
|
42
|
+
trace: {
|
|
43
|
+
voice: number;
|
|
44
|
+
clip: string;
|
|
45
|
+
requestedAt: number;
|
|
46
|
+
presentation: {
|
|
47
|
+
state: import("@aethervtt/shard-platform").AudioPresentationState;
|
|
48
|
+
scheduledAt: number;
|
|
49
|
+
endedAt: number;
|
|
50
|
+
error: string | null;
|
|
51
|
+
};
|
|
52
|
+
}[];
|
|
53
|
+
capabilities: Readonly<import("@aethervtt/shard-platform").AudioCapabilities>;
|
|
54
|
+
backend: {
|
|
55
|
+
kind: string;
|
|
56
|
+
userAgent: string;
|
|
57
|
+
sampleRate: number;
|
|
58
|
+
};
|
|
59
|
+
}>;
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { ShardError } from "@aethervtt/shard-core";
|
|
2
|
+
import { createWebAudioBackend } from "./audio.js";
|
|
3
|
+
function measureAudioBuffer(buffer) {
|
|
4
|
+
let peak = 0, square = 0, clipped = 0, nonfinite = 0;
|
|
5
|
+
for (let channel = 0; channel < buffer.numberOfChannels; channel++) {
|
|
6
|
+
const data = buffer.getChannelData(channel);
|
|
7
|
+
for (let i = 0; i < data.length; i++) {
|
|
8
|
+
const sample = data[i];
|
|
9
|
+
if (!Number.isFinite(sample)) {
|
|
10
|
+
nonfinite++;
|
|
11
|
+
continue;
|
|
12
|
+
}
|
|
13
|
+
peak = Math.max(peak, Math.abs(sample));
|
|
14
|
+
square += sample * sample;
|
|
15
|
+
if (Math.abs(sample) > 1) clipped++;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return {
|
|
19
|
+
peak,
|
|
20
|
+
rms: Math.sqrt(square / (buffer.length * buffer.numberOfChannels)),
|
|
21
|
+
clipped,
|
|
22
|
+
nonfinite,
|
|
23
|
+
sampleRate: buffer.sampleRate,
|
|
24
|
+
channels: buffer.numberOfChannels,
|
|
25
|
+
frames: buffer.length,
|
|
26
|
+
duration: buffer.duration
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function audioBufferWav(buffer) {
|
|
30
|
+
const channels = buffer.numberOfChannels, size = buffer.length * channels * 2;
|
|
31
|
+
const bytes = new Uint8Array(44 + size), view = new DataView(bytes.buffer);
|
|
32
|
+
const text = (at, value) => {
|
|
33
|
+
for (let i = 0; i < value.length; i++) bytes[at + i] = value.charCodeAt(i);
|
|
34
|
+
};
|
|
35
|
+
text(0, "RIFF");
|
|
36
|
+
view.setUint32(4, 36 + size, true);
|
|
37
|
+
text(8, "WAVE");
|
|
38
|
+
text(12, "fmt ");
|
|
39
|
+
view.setUint32(16, 16, true);
|
|
40
|
+
view.setUint16(20, 1, true);
|
|
41
|
+
view.setUint16(22, channels, true);
|
|
42
|
+
view.setUint32(24, buffer.sampleRate, true);
|
|
43
|
+
view.setUint32(28, buffer.sampleRate * channels * 2, true);
|
|
44
|
+
view.setUint16(32, channels * 2, true);
|
|
45
|
+
view.setUint16(34, 16, true);
|
|
46
|
+
text(36, "data");
|
|
47
|
+
view.setUint32(40, size, true);
|
|
48
|
+
for (let c = 0; c < channels; c++) {
|
|
49
|
+
const data = buffer.getChannelData(c);
|
|
50
|
+
for (let i = 0; i < buffer.length; i++) {
|
|
51
|
+
const value = Math.max(-1, Math.min(1, data[i]));
|
|
52
|
+
view.setInt16(
|
|
53
|
+
44 + (i * channels + c) * 2,
|
|
54
|
+
Math.round(value * (value < 0 ? 32768 : 32767)),
|
|
55
|
+
true
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return bytes;
|
|
60
|
+
}
|
|
61
|
+
async function renderAudioPreview(plan) {
|
|
62
|
+
const duration = plan.duration + (plan.tailSeconds ?? 0), sampleRate = plan.sampleRate ?? 48e3, channels = plan.channels ?? 2;
|
|
63
|
+
const frames = Math.ceil(duration * sampleRate);
|
|
64
|
+
if (!Number.isFinite(duration) || plan.duration <= 0 || duration > 60 || !Number.isInteger(sampleRate) || sampleRate < 8e3 || sampleRate > 96e3 || ![1, 2].includes(channels) || frames * channels * 6 + 44 > (plan.maxOutputBytes ?? 64 * 1024 * 1024) || plan.voices.length > 4096) {
|
|
65
|
+
throw new ShardError(
|
|
66
|
+
"audio/preview-unavailable",
|
|
67
|
+
"Preview exceeds its duration, rate, voice or output-memory limits"
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
if (typeof OfflineAudioContext === "undefined")
|
|
71
|
+
throw new ShardError("audio/preview-unavailable", "This host has no OfflineAudioContext");
|
|
72
|
+
for (const voice of plan.voices)
|
|
73
|
+
if (voice.clip.stream || !voice.clip.bytes.byteLength)
|
|
74
|
+
throw new ShardError(
|
|
75
|
+
"audio/preview-unavailable",
|
|
76
|
+
"Preview requires local encoded bytes for decoded clips"
|
|
77
|
+
);
|
|
78
|
+
const context = new OfflineAudioContext(channels, frames, sampleRate);
|
|
79
|
+
const backend = createWebAudioBackend({
|
|
80
|
+
context,
|
|
81
|
+
fixedListener: true,
|
|
82
|
+
maxVoices: 4096,
|
|
83
|
+
maxHostVoices: 4096,
|
|
84
|
+
decodedBudgetBytes: plan.decodedBudgetBytes ?? 64 * 1024 * 1024
|
|
85
|
+
});
|
|
86
|
+
try {
|
|
87
|
+
const leases = await Promise.all(
|
|
88
|
+
plan.voices.map((voice) => backend.runtime.prepare(voice.clip))
|
|
89
|
+
);
|
|
90
|
+
if (plan.mixer) await backend.runtime.configureMixer(plan.mixer);
|
|
91
|
+
const ids = plan.voices.map(
|
|
92
|
+
(voice) => backend.runtime.play({
|
|
93
|
+
...voice,
|
|
94
|
+
when: voice.when ?? 0,
|
|
95
|
+
duration: voice.duration ?? (voice.loop ? Math.max(0, plan.duration - (voice.when ?? 0)) : void 0)
|
|
96
|
+
})
|
|
97
|
+
);
|
|
98
|
+
await backend.runtime.flush();
|
|
99
|
+
for (const item of plan.automation ?? []) {
|
|
100
|
+
const id = ids[item.voice];
|
|
101
|
+
if (id === void 0)
|
|
102
|
+
throw new ShardError(
|
|
103
|
+
"audio/preview-unavailable",
|
|
104
|
+
`Automation refers to unknown voice ${item.voice}`
|
|
105
|
+
);
|
|
106
|
+
backend.runtime.automate(id, item.command);
|
|
107
|
+
}
|
|
108
|
+
const trace = ids.map((id, index) => ({
|
|
109
|
+
voice: index,
|
|
110
|
+
clip: plan.voices[index].clip.id,
|
|
111
|
+
requestedAt: plan.voices[index].when ?? 0,
|
|
112
|
+
presentation: { ...backend.runtime.presentation(id) }
|
|
113
|
+
}));
|
|
114
|
+
const buffer = await context.startRendering();
|
|
115
|
+
const measured = measureAudioBuffer(buffer);
|
|
116
|
+
if (measured.nonfinite)
|
|
117
|
+
throw new ShardError(
|
|
118
|
+
"audio/preview-unavailable",
|
|
119
|
+
"Rendered signal contains nonfinite samples"
|
|
120
|
+
);
|
|
121
|
+
for (const lease of leases) lease.release();
|
|
122
|
+
return {
|
|
123
|
+
buffer,
|
|
124
|
+
wav: audioBufferWav(buffer),
|
|
125
|
+
measured,
|
|
126
|
+
trace,
|
|
127
|
+
capabilities: backend.runtime.capabilities,
|
|
128
|
+
backend: {
|
|
129
|
+
kind: "web-audio-offline",
|
|
130
|
+
userAgent: navigator.userAgent,
|
|
131
|
+
sampleRate: context.sampleRate
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
} finally {
|
|
135
|
+
backend.dispose?.();
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
export {
|
|
139
|
+
audioBufferWav,
|
|
140
|
+
measureAudioBuffer,
|
|
141
|
+
renderAudioPreview
|
|
142
|
+
};
|
package/dist/audio.d.ts
CHANGED
|
@@ -1,18 +1,14 @@
|
|
|
1
|
-
import type { AudioBackend } from '@aethervtt/shard-platform';
|
|
2
|
-
/** Web Audio in a browser or webview: one context, a gain node per bus, a PannerNode per spatial voice. */
|
|
1
|
+
import type { AudioBackend, AudioRuntimeExtension, AudioSessionOptions } from '@aethervtt/shard-platform';
|
|
3
2
|
export interface WebAudioBackend extends AudioBackend {
|
|
4
|
-
readonly context:
|
|
5
|
-
/** Everything mixed (before the destination), for meters. */
|
|
3
|
+
readonly context: BaseAudioContext;
|
|
6
4
|
readonly output: GainNode;
|
|
7
|
-
|
|
5
|
+
readonly runtime: AudioRuntimeExtension;
|
|
8
6
|
busNode(name: string): GainNode;
|
|
9
|
-
/** Resumes the context (browsers need a user gesture first). */
|
|
10
7
|
resume(): Promise<void>;
|
|
11
8
|
}
|
|
12
|
-
/**
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
context?: AudioContext;
|
|
9
|
+
/** fixedListener:true explicitly grants Shard ownership of an external context's listener. */
|
|
10
|
+
export declare function createWebAudioBackend(options?: AudioSessionOptions & {
|
|
11
|
+
context?: BaseAudioContext;
|
|
12
|
+
fixedListener?: boolean;
|
|
13
|
+
maxHostVoices?: number;
|
|
18
14
|
}): WebAudioBackend;
|