@daugasauron/dolly-host-audio 0.0.0-stage → 0.1.4-rc.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/README.md CHANGED
@@ -1,3 +1,4 @@
1
- # Temporary Holding Version
1
+ # @daugasauron/dolly-host-audio
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Dolly's `audio@0` host module: stereo PCM playback. Give the URL of its
4
+ `module.json` to `boot()`'s `modules`; @daugasauron/dolly's README says how.
package/abi.mjs ADDED
@@ -0,0 +1,18 @@
1
+ // Generated from host/audio/dolly-audio-0.wat, host/audio/audio.h, include/dolly/process.h.
2
+ export const DOLLY_AUDIO_VERSION = 0;
3
+ export const DOLLY_AUDIO_PROCESS_OP = 129;
4
+ export const DOLLY_AUDIO_SLOTS = 4;
5
+ export const DOLLY_AUDIO_REPLY_BYTES = 16;
6
+ export const DOLLY_AUDIO_PACKET_BYTES = 32808;
7
+ export const DOLLY_AUDIO_RATE = 48000;
8
+ export const DOLLY_AUDIO_CHANNELS = 2;
9
+ export const DOLLY_AUDIO_MIN_FRAMES = 128;
10
+ export const DOLLY_AUDIO_MAX_FRAMES = 4096;
11
+ export const DOLLY_AUDIO_QUEUE_FRAMES = 48000;
12
+ export const DOLLY_AUDIO_MAX_BUFFERS = 64;
13
+ export const DOLLY_AUDIO_OPEN = 1;
14
+ export const DOLLY_AUDIO_WRITE = 2;
15
+ export const DOLLY_AUDIO_STATUS = 3;
16
+ export const DOLLY_AUDIO_CLOSE = 4;
17
+ export const DOLLY_ERRNO = Object.freeze({ E2BIG: 1, EACCES: 2, EADDRINUSE: 3, EADDRNOTAVAIL: 4, EAFNOSUPPORT: 5, EAGAIN: 6, EALREADY: 7, EBADF: 8, EBADMSG: 9, EBUSY: 10, ECANCELED: 11, ECHILD: 12, ECONNABORTED: 13, ECONNREFUSED: 14, ECONNRESET: 15, EDEADLK: 16, EDESTADDRREQ: 17, EDOM: 18, EDQUOT: 19, EEXIST: 20, EFAULT: 21, EFBIG: 22, EHOSTUNREACH: 23, EIDRM: 24, EILSEQ: 25, EINPROGRESS: 26, EINTR: 27, EINVAL: 28, EIO: 29, EISCONN: 30, EISDIR: 31, ELOOP: 32, EMFILE: 33, EMLINK: 34, EMSGSIZE: 35, EMULTIHOP: 36, ENAMETOOLONG: 37, ENETDOWN: 38, ENETRESET: 39, ENETUNREACH: 40, ENFILE: 41, ENOBUFS: 42, ENODEV: 43, ENOENT: 44, ENOEXEC: 45, ENOLCK: 46, ENOLINK: 47, ENOMEM: 48, ENOMSG: 49, ENOPROTOOPT: 50, ENOSPC: 51, ENOSYS: 52, ENOTCONN: 53, ENOTDIR: 54, ENOTEMPTY: 55, ENOTRECOVERABLE: 56, ENOTSOCK: 57, ENOTTY: 59, ENXIO: 60, EOVERFLOW: 61, EOWNERDEAD: 62, EPERM: 63, EPIPE: 64, EPROTO: 65, EPROTONOSUPPORT: 66, EPROTOTYPE: 67, ERANGE: 68, EROFS: 69, ESPIPE: 70, ESRCH: 71, ESTALE: 72, ETIMEDOUT: 73, ETXTBSY: 74, EXDEV: 75, ENODATA: 116, ENOTSUP: 138 });
18
+ export const DOLLY_AUDIO_ABI_DIGEST = "bf9f97c6945174f9788f03091ca6e1c982f5b8c80488c1b5526804ceb84a1046";
package/audio.mjs ADDED
@@ -0,0 +1,52 @@
1
+ import { createAudioProvider } from "./provider.mjs";
2
+ import * as A from "./abi.mjs";
3
+ import { DOLLY_ERRNO as E } from "./abi.mjs";
4
+ export { DOLLY_AUDIO_ABI_DIGEST as digest } from "./abi.mjs";
5
+
6
+ // The device as the lease bridge sees it.
7
+ export const lease = { type: "audio", slots: A.DOLLY_AUDIO_SLOTS, packetBytes: A.DOLLY_AUDIO_PACKET_BYTES,
8
+ replyBytes: A.DOLLY_AUDIO_REPLY_BYTES, open: A.DOLLY_AUDIO_OPEN, close: A.DOLLY_AUDIO_CLOSE };
9
+
10
+ export function check() {
11
+ return typeof globalThis.AudioContext === "function" ? null : "Web Audio playback is unavailable in this browser";
12
+ }
13
+
14
+ export function browser({ send }) {
15
+ const provider = createAudioProvider();
16
+ const resume = event => { if (event.isTrusted) provider.resume(); };
17
+ window.addEventListener("keydown", resume, { capture: true });
18
+ window.addEventListener("pointerdown", resume, { capture: true });
19
+ return {
20
+ get status() { return provider.status(); },
21
+ page: { get audio() { return provider.status(); } },
22
+ messages: {
23
+ "audio-request"(message) {
24
+ const result = provider.dispatch(message.packet);
25
+ send({ type: "audio-complete", scope: message.scope, sequence: message.sequence, ...result },
26
+ [result.bytes.buffer]);
27
+ },
28
+ "audio-revoke"(message) {
29
+ provider.release(message.scope);
30
+ send({ type: "audio-complete", scope: message.scope, revoked: true });
31
+ },
32
+ },
33
+ dispose() {
34
+ window.removeEventListener("keydown", resume, { capture: true });
35
+ window.removeEventListener("pointerdown", resume, { capture: true });
36
+ void provider.close();
37
+ },
38
+ };
39
+ }
40
+
41
+ export function worker({ send, get }) {
42
+ let bridge;
43
+ return {
44
+ bindings: { "env.dolly_audio_dispatch": (address, bytes) =>
45
+ bridge ? bridge.dispatch({ address, bytes }) : -E.ENOSYS },
46
+ start({ kernel }) {
47
+ bridge = get("runtime").leaseBridge(lease, Number(kernel.dolly_audio_mailbox_address()), send);
48
+ },
49
+ messages: { "audio-complete": message => bridge?.acknowledge(message) },
50
+ dispose() { bridge?.dispose(); bridge = undefined; },
51
+ };
52
+ }
package/module.json ADDED
@@ -0,0 +1,15 @@
1
+ {
2
+ "name": "audio",
3
+ "version": 0,
4
+ "gives": "stereo PCM playback",
5
+ "dependencies": ["runtime@0"],
6
+ "phase": "kernel",
7
+ "imports": ["env.dolly_audio_dispatch"],
8
+ "host": "audio.mjs",
9
+ "contracts": ["dolly-audio-0.wat"],
10
+ "process": [],
11
+ "headers": ["audio.h", "audio-abi.h"],
12
+ "kernel": ["kernel.c"],
13
+ "client": ["client.c"],
14
+ "package": "libdolly-audio"
15
+ }
package/package.json CHANGED
@@ -1,6 +1,14 @@
1
1
  {
2
+ "version": "0.1.4-rc.1",
3
+ "type": "module",
2
4
  "name": "@daugasauron/dolly-host-audio",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
5
+ "license": "MIT",
6
+ "dependencies": {
7
+ "@daugasauron/dolly": "0.1.4-rc.1"
8
+ },
9
+ "description": "Dolly's audio@0 host module: stereo PCM playback.",
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+https://github.com/daugasauron/dolly.git"
13
+ }
14
+ }
package/provider.mjs ADDED
@@ -0,0 +1,120 @@
1
+ import * as A from "./abi.mjs";
2
+ import { DOLLY_ERRNO as E } from "./abi.mjs";
3
+
4
+ const fail = errno => { throw Object.assign(new Error("Audio request failed"), {errno}); };
5
+ const response = size => new Uint8Array(size);
6
+
7
+ export function createAudioProvider() {
8
+ const streams = new Map(), generations = new Uint32Array(A.DOLLY_AUDIO_SLOTS);
9
+ let context, resumePending = false;
10
+
11
+ function finish(stream, node) {
12
+ if (!stream.nodes.delete(node)) return;
13
+ node.source.disconnect();
14
+ stream.played += node.frames;
15
+ }
16
+ function queued(stream) {
17
+ const now = context.currentTime;
18
+ let frames = 0;
19
+ for (const node of stream.nodes) {
20
+ if (now >= node.end) finish(stream, node);
21
+ else frames += node.frames - Math.max(0, Math.floor((now - node.start) * A.DOLLY_AUDIO_RATE));
22
+ }
23
+ return frames;
24
+ }
25
+ function status() {
26
+ let queuedFrames = 0, buffers = 0;
27
+ for (const stream of streams.values()) { queuedFrames += queued(stream); buffers += stream.nodes.size; }
28
+ return {activeScopes: streams.size, queuedFrames, buffers, state: context?.state ?? "closed"};
29
+ }
30
+ function resume() {
31
+ if (!resumePending && context?.state === "suspended" && streams.size &&
32
+ navigator.userActivation?.hasBeenActive) {
33
+ resumePending = true;
34
+ const settled = () => { resumePending = false; };
35
+ void context.resume().then(settled, settled);
36
+ }
37
+ }
38
+ function release(scope) {
39
+ const stream = streams.get(scope);
40
+ if (!stream) return;
41
+ streams.delete(scope);
42
+ for (const node of stream.nodes) { node.source.onended = null; node.source.stop(); node.source.disconnect(); }
43
+ stream.nodes.clear();
44
+ }
45
+ function dispatch(packet) {
46
+ try {
47
+ if (!(packet instanceof Uint8Array) || packet.byteLength < 32 || packet.byteLength > A.DOLLY_AUDIO_PACKET_BYTES) fail(E.EINVAL);
48
+ packet = packet.slice();
49
+ const view = new DataView(packet.buffer, packet.byteOffset, packet.byteLength);
50
+ const op = view.getUint32(4, true), scope = view.getUint32(8, true), sequence = view.getUint32(16, true);
51
+ if (view.getUint32(0, true) !== A.DOLLY_AUDIO_VERSION || view.getUint32(12, true) || view.getUint32(20, true) ||
52
+ !scope || !sequence || view.getUint32(24, true) !== packet.length - 32 || view.getUint32(28, true)) fail(E.EINVAL);
53
+ const slot = (scope - 1) % A.DOLLY_AUDIO_SLOTS;
54
+ let stream = streams.get(scope), result;
55
+ if (op === A.DOLLY_AUDIO_OPEN) {
56
+ if (packet.length !== 32) fail(E.EINVAL);
57
+ if (scope <= generations[slot]) fail(E.ESTALE);
58
+ if ([...streams.keys()].some(key => (key - 1) % A.DOLLY_AUDIO_SLOTS === slot)) fail(E.EBUSY);
59
+ if (!context) {
60
+ if (typeof AudioContext !== "function") fail(E.ENOSYS);
61
+ context = new AudioContext({sampleRate: A.DOLLY_AUDIO_RATE, latencyHint: "interactive"});
62
+ }
63
+ if (context.state === "closed") fail(E.EIO);
64
+ stream = {scope, sequence, nodes: new Set(), end: context.currentTime, played: 0};
65
+ streams.set(scope, stream); generations[slot] = scope;
66
+ result = response(16);
67
+ const output = new DataView(result.buffer);
68
+ output.setBigUint64(0, BigInt(scope), true);
69
+ output.setUint32(8, A.DOLLY_AUDIO_RATE, true); output.setUint32(12, A.DOLLY_AUDIO_CHANNELS, true);
70
+ resume();
71
+ } else {
72
+ if (!stream) fail(E.EBADF);
73
+ if (sequence <= stream.sequence) fail(E.ESTALE);
74
+ stream.sequence = sequence;
75
+ if (op === A.DOLLY_AUDIO_WRITE) {
76
+ if (packet.length < 40) fail(E.EINVAL);
77
+ const frames = view.getUint32(32, true);
78
+ if (frames < A.DOLLY_AUDIO_MIN_FRAMES || frames > A.DOLLY_AUDIO_MAX_FRAMES ||
79
+ view.getUint32(36, true) || packet.length !== 40 + frames * 8) fail(E.EINVAL);
80
+ for (let at = 40; at < packet.length; at += 4) if (!Number.isFinite(view.getFloat32(at, true))) fail(E.EINVAL);
81
+ const pending = queued(stream);
82
+ if (pending + frames > A.DOLLY_AUDIO_QUEUE_FRAMES || stream.nodes.size >= A.DOLLY_AUDIO_MAX_BUFFERS) fail(E.EAGAIN);
83
+ const buffer = context.createBuffer(A.DOLLY_AUDIO_CHANNELS, frames, A.DOLLY_AUDIO_RATE);
84
+ for (let channel = 0; channel < A.DOLLY_AUDIO_CHANNELS; ++channel) {
85
+ const data = buffer.getChannelData(channel);
86
+ for (let i = 0; i < frames; ++i) data[i] = Math.max(-1, Math.min(1, view.getFloat32(40 + (i * 2 + channel) * 4, true)));
87
+ }
88
+ const source = context.createBufferSource(); source.buffer = buffer;
89
+ source.connect(context.destination);
90
+ const now = context.currentTime, start = Math.max(stream.end, now + 0.01);
91
+ const node = {source, frames, start, end: start + frames / A.DOLLY_AUDIO_RATE};
92
+ source.onended = () => finish(stream, node);
93
+ source.start(start); stream.end = node.end; stream.nodes.add(node);
94
+ result = response(4); new DataView(result.buffer).setUint32(0, frames, true);
95
+ } else if (op === A.DOLLY_AUDIO_STATUS) {
96
+ if (packet.length !== 32) fail(E.EINVAL);
97
+ const pending = queued(stream);
98
+ let played = stream.played;
99
+ for (const node of stream.nodes) played += Math.min(node.frames, Math.max(0, Math.floor((context.currentTime - node.start) * A.DOLLY_AUDIO_RATE)));
100
+ result = response(16);
101
+ const output = new DataView(result.buffer);
102
+ output.setUint32(0, pending, true); output.setUint32(4, context.state === "running" ? 1 : 2, true);
103
+ output.setBigUint64(8, BigInt(played), true);
104
+ } else if (op === A.DOLLY_AUDIO_CLOSE) {
105
+ if (packet.length !== 32) fail(E.EINVAL);
106
+ release(scope); result = response(0);
107
+ } else fail(E.ENOSYS);
108
+ }
109
+ return {bytes: result, error: 0};
110
+ } catch (error) {
111
+ if (!error.errno) console.warn("Dolly audio:", error);
112
+ return {bytes: response(0), error: error.errno ?? E.EIO};
113
+ }
114
+ }
115
+ async function close() {
116
+ for (const scope of [...streams.keys()]) release(scope);
117
+ if (context && context.state !== "closed") await context.close();
118
+ }
119
+ return {dispatch, release, resume, status, close};
120
+ }