@scenography/plugin-audio 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.
@@ -0,0 +1,27 @@
1
+ import { existsProp, fail, isNonEmptyString, isNumberInRange, isOneOf, propIs, validate, when } from "@scenography/scene";
2
+ //#region src/data/validate.ts
3
+ const AUDIO_TYPES = new Set([
4
+ "stream",
5
+ "loop",
6
+ "playlist"
7
+ ]);
8
+ const isTrackList = (tracks) => Array.isArray(tracks) && tracks.length > 0 && tracks.every((t) => isNonEmptyString(t));
9
+ const validateAudio = (audio) => validate([
10
+ existsProp("id", "Audio source is missing an id."),
11
+ propIs("volume", isNumberInRange(0, 1), "volume must be a number between 0 and 1."),
12
+ when((a) => a.type === "stream" || a.type === "loop", existsProp("src", "Audio source is missing a src.")),
13
+ when((a) => a.type === "playlist", existsProp("tracks", "tracks must be a non-empty array of URLs."), propIs("tracks", isTrackList, "tracks must be a non-empty array of URLs.")),
14
+ when((a) => !isOneOf(AUDIO_TYPES)(a.type), fail("type", "unknown audio type."))
15
+ ])(audio);
16
+ //#endregion
17
+ //#region src/data/plugin.ts
18
+ /** The data half of the audio plugin: owns the `audio` scene key (a collection). */
19
+ const audioData = {
20
+ kind: "audio",
21
+ cardinality: "many",
22
+ validate: (source) => validateAudio(source)
23
+ };
24
+ //#endregion
25
+ export { validateAudio as n, audioData as t };
26
+
27
+ //# sourceMappingURL=data-ChoGUA4U.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"data-ChoGUA4U.mjs","names":[],"sources":["../src/data/validate.ts","../src/data/plugin.ts"],"sourcesContent":["import {\n existsProp,\n fail,\n isNonEmptyString,\n isNumberInRange,\n isOneOf,\n propIs,\n validate,\n when,\n type ValidationResult,\n} from \"@scenography/scene\";\n\nimport type { AudioSource } from \"./audio\";\n\nconst AUDIO_TYPES: ReadonlySet<string> = new Set([\"stream\", \"loop\", \"playlist\"]);\n\nconst isTrackList = (tracks: unknown): boolean =>\n Array.isArray(tracks) && tracks.length > 0 && tracks.every((t) => isNonEmptyString(t));\n\nexport const validateAudio = (audio: AudioSource): ValidationResult =>\n validate<AudioSource>([\n existsProp(\"id\", \"Audio source is missing an id.\"),\n propIs(\"volume\", isNumberInRange(0, 1), \"volume must be a number between 0 and 1.\"),\n when(\n (a: AudioSource) => a.type === \"stream\" || a.type === \"loop\",\n existsProp(\"src\", \"Audio source is missing a src.\"),\n ),\n when(\n (a: AudioSource) => a.type === \"playlist\",\n existsProp(\"tracks\", \"tracks must be a non-empty array of URLs.\"),\n propIs(\"tracks\", isTrackList, \"tracks must be a non-empty array of URLs.\"),\n ),\n when(\n (a: AudioSource) => !isOneOf(AUDIO_TYPES)(a.type),\n fail(\"type\", \"unknown audio type.\"),\n ),\n ])(audio);\n","import type { DataPlugin } from \"@scenography/scene\";\n\nimport { validateAudio } from \"./validate\";\nimport type { AudioSource } from \"./audio\";\n\n/** The data half of the audio plugin: owns the `audio` scene key (a collection). */\nexport const audioData: DataPlugin<AudioSource> = {\n kind: \"audio\",\n cardinality: \"many\",\n validate: (source) => validateAudio(source),\n};\n"],"mappings":";;AAcA,MAAM,cAAmC,IAAI,IAAI;CAAC;CAAU;CAAQ;AAAU,CAAC;AAE/E,MAAM,eAAe,WACnB,MAAM,QAAQ,MAAM,KAAK,OAAO,SAAS,KAAK,OAAO,OAAO,MAAM,iBAAiB,CAAC,CAAC;AAEvF,MAAa,iBAAiB,UAC5B,SAAsB;CACpB,WAAW,MAAM,gCAAgC;CACjD,OAAO,UAAU,gBAAgB,GAAG,CAAC,GAAG,0CAA0C;CAClF,MACG,MAAmB,EAAE,SAAS,YAAY,EAAE,SAAS,QACtD,WAAW,OAAO,gCAAgC,CACpD;CACA,MACG,MAAmB,EAAE,SAAS,YAC/B,WAAW,UAAU,2CAA2C,GAChE,OAAO,UAAU,aAAa,2CAA2C,CAC3E;CACA,MACG,MAAmB,CAAC,QAAQ,WAAW,CAAC,CAAC,EAAE,IAAI,GAChD,KAAK,QAAQ,qBAAqB,CACpC;AACF,CAAC,CAAC,CAAC,KAAK;;;;AC9BV,MAAa,YAAqC;CAChD,MAAM;CACN,aAAa;CACb,WAAW,WAAW,cAAc,MAAM;AAC5C"}
@@ -0,0 +1,38 @@
1
+ let _scenography_scene = require("@scenography/scene");
2
+ //#region src/data/validate.ts
3
+ const AUDIO_TYPES = new Set([
4
+ "stream",
5
+ "loop",
6
+ "playlist"
7
+ ]);
8
+ const isTrackList = (tracks) => Array.isArray(tracks) && tracks.length > 0 && tracks.every((t) => (0, _scenography_scene.isNonEmptyString)(t));
9
+ const validateAudio = (audio) => (0, _scenography_scene.validate)([
10
+ (0, _scenography_scene.existsProp)("id", "Audio source is missing an id."),
11
+ (0, _scenography_scene.propIs)("volume", (0, _scenography_scene.isNumberInRange)(0, 1), "volume must be a number between 0 and 1."),
12
+ (0, _scenography_scene.when)((a) => a.type === "stream" || a.type === "loop", (0, _scenography_scene.existsProp)("src", "Audio source is missing a src.")),
13
+ (0, _scenography_scene.when)((a) => a.type === "playlist", (0, _scenography_scene.existsProp)("tracks", "tracks must be a non-empty array of URLs."), (0, _scenography_scene.propIs)("tracks", isTrackList, "tracks must be a non-empty array of URLs.")),
14
+ (0, _scenography_scene.when)((a) => !(0, _scenography_scene.isOneOf)(AUDIO_TYPES)(a.type), (0, _scenography_scene.fail)("type", "unknown audio type."))
15
+ ])(audio);
16
+ //#endregion
17
+ //#region src/data/plugin.ts
18
+ /** The data half of the audio plugin: owns the `audio` scene key (a collection). */
19
+ const audioData = {
20
+ kind: "audio",
21
+ cardinality: "many",
22
+ validate: (source) => validateAudio(source)
23
+ };
24
+ //#endregion
25
+ Object.defineProperty(exports, "audioData", {
26
+ enumerable: true,
27
+ get: function() {
28
+ return audioData;
29
+ }
30
+ });
31
+ Object.defineProperty(exports, "validateAudio", {
32
+ enumerable: true,
33
+ get: function() {
34
+ return validateAudio;
35
+ }
36
+ });
37
+
38
+ //# sourceMappingURL=data-vyKDnIYf.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"data-vyKDnIYf.cjs","names":[],"sources":["../src/data/validate.ts","../src/data/plugin.ts"],"sourcesContent":["import {\n existsProp,\n fail,\n isNonEmptyString,\n isNumberInRange,\n isOneOf,\n propIs,\n validate,\n when,\n type ValidationResult,\n} from \"@scenography/scene\";\n\nimport type { AudioSource } from \"./audio\";\n\nconst AUDIO_TYPES: ReadonlySet<string> = new Set([\"stream\", \"loop\", \"playlist\"]);\n\nconst isTrackList = (tracks: unknown): boolean =>\n Array.isArray(tracks) && tracks.length > 0 && tracks.every((t) => isNonEmptyString(t));\n\nexport const validateAudio = (audio: AudioSource): ValidationResult =>\n validate<AudioSource>([\n existsProp(\"id\", \"Audio source is missing an id.\"),\n propIs(\"volume\", isNumberInRange(0, 1), \"volume must be a number between 0 and 1.\"),\n when(\n (a: AudioSource) => a.type === \"stream\" || a.type === \"loop\",\n existsProp(\"src\", \"Audio source is missing a src.\"),\n ),\n when(\n (a: AudioSource) => a.type === \"playlist\",\n existsProp(\"tracks\", \"tracks must be a non-empty array of URLs.\"),\n propIs(\"tracks\", isTrackList, \"tracks must be a non-empty array of URLs.\"),\n ),\n when(\n (a: AudioSource) => !isOneOf(AUDIO_TYPES)(a.type),\n fail(\"type\", \"unknown audio type.\"),\n ),\n ])(audio);\n","import type { DataPlugin } from \"@scenography/scene\";\n\nimport { validateAudio } from \"./validate\";\nimport type { AudioSource } from \"./audio\";\n\n/** The data half of the audio plugin: owns the `audio` scene key (a collection). */\nexport const audioData: DataPlugin<AudioSource> = {\n kind: \"audio\",\n cardinality: \"many\",\n validate: (source) => validateAudio(source),\n};\n"],"mappings":";;AAcA,MAAM,cAAmC,IAAI,IAAI;CAAC;CAAU;CAAQ;AAAU,CAAC;AAE/E,MAAM,eAAe,WACnB,MAAM,QAAQ,MAAM,KAAK,OAAO,SAAS,KAAK,OAAO,OAAO,OAAA,GAAA,mBAAA,iBAAA,CAAuB,CAAC,CAAC;AAEvF,MAAa,iBAAiB,WAAA,GAAA,mBAAA,SAAA,CACN;oCACT,MAAM,gCAAgC;gCAC1C,WAAA,GAAA,mBAAA,gBAAA,CAA0B,GAAG,CAAC,GAAG,0CAA0C;+BAE/E,MAAmB,EAAE,SAAS,YAAY,EAAE,SAAS,SAAA,GAAA,mBAAA,WAAA,CAC3C,OAAO,gCAAgC,CACpD;+BAEG,MAAmB,EAAE,SAAS,aAAA,GAAA,mBAAA,WAAA,CACpB,UAAU,2CAA2C,IAAA,GAAA,mBAAA,OAAA,CACzD,UAAU,aAAa,2CAA2C,CAC3E;+BAEG,MAAmB,EAAA,GAAA,mBAAA,QAAA,CAAS,WAAW,CAAC,CAAC,EAAE,IAAI,IAAA,GAAA,mBAAA,KAAA,CAC3C,QAAQ,qBAAqB,CACpC;AACF,CAAC,CAAC,CAAC,KAAK;;;;AC9BV,MAAa,YAAqC;CAChD,MAAM;CACN,aAAa;CACb,WAAW,WAAW,cAAc,MAAM;AAC5C"}
package/dist/data.cjs ADDED
@@ -0,0 +1,4 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_data = require("./data-vyKDnIYf.cjs");
3
+ exports.audioData = require_data.audioData;
4
+ exports.validateAudio = require_data.validateAudio;
@@ -0,0 +1,2 @@
1
+ import { i as AudioSourceType, n as validateAudio, r as AudioSource, t as audioData } from "./index-CR9ApbPg.cjs";
2
+ export { type AudioSource, type AudioSourceType, audioData, validateAudio };
@@ -0,0 +1,2 @@
1
+ import { i as AudioSourceType, n as validateAudio, r as AudioSource, t as audioData } from "./index-CR9ApbPg.mjs";
2
+ export { type AudioSource, type AudioSourceType, audioData, validateAudio };
package/dist/data.mjs ADDED
@@ -0,0 +1,2 @@
1
+ import { n as validateAudio, t as audioData } from "./data-ChoGUA4U.mjs";
2
+ export { audioData, validateAudio };
@@ -0,0 +1,48 @@
1
+ import { DataPlugin, ValidationResult } from "@scenography/scene";
2
+
3
+ //#region src/data/audio.d.ts
4
+ /** Fields shared by every audio source variant. */
5
+ interface AudioBase {
6
+ id: string;
7
+ /** Initial volume, `0`..`1`. Defaults to `1`. */
8
+ volume?: number;
9
+ /** Begin playing as soon as the visitor's first gesture unlocks audio. Defaults to `false`. */
10
+ autoplay?: boolean;
11
+ }
12
+ /**
13
+ * A background audio source. Extensible via `type`: a live online `stream`
14
+ * (radio), a single file on `loop`, or an ordered `playlist` of files. Playback
15
+ * (play/pause/fade) is driven by `id` from the runtime through `ctx.media` — the
16
+ * definition stays pure data.
17
+ */
18
+ type AudioSource = (AudioBase & {
19
+ type: "stream"; /** URL of the live stream / online radio. */
20
+ src: string;
21
+ }) | (AudioBase & {
22
+ type: "loop"; /** URL of the file played on repeat. */
23
+ src: string;
24
+ }) | (AudioBase & {
25
+ type: "playlist"; /** Track URLs played in sequence. */
26
+ tracks: string[]; /** Restart from the first track when the last ends. Defaults to `true`. */
27
+ loop?: boolean; /** Play tracks in random order. Defaults to `false`. */
28
+ shuffle?: boolean; /** Crossfade between tracks, in ms. Defaults to `0` (hard cut). */
29
+ crossfade?: number;
30
+ });
31
+ /** The discriminant values an `AudioSource` can take. */
32
+ type AudioSourceType = AudioSource["type"];
33
+ declare module "@scenography/scene" {
34
+ interface Scene {
35
+ /** Background audio sources — provided by `@scenography/plugin-audio`. */
36
+ audio?: AudioSource[];
37
+ }
38
+ }
39
+ //#endregion
40
+ //#region src/data/validate.d.ts
41
+ declare const validateAudio: (audio: AudioSource) => ValidationResult;
42
+ //#endregion
43
+ //#region src/data/plugin.d.ts
44
+ /** The data half of the audio plugin: owns the `audio` scene key (a collection). */
45
+ declare const audioData: DataPlugin<AudioSource>;
46
+ //#endregion
47
+ export { AudioSourceType as i, validateAudio as n, AudioSource as r, audioData as t };
48
+ //# sourceMappingURL=index-CR9ApbPg.d.cts.map
@@ -0,0 +1,48 @@
1
+ import { DataPlugin, ValidationResult } from "@scenography/scene";
2
+
3
+ //#region src/data/audio.d.ts
4
+ /** Fields shared by every audio source variant. */
5
+ interface AudioBase {
6
+ id: string;
7
+ /** Initial volume, `0`..`1`. Defaults to `1`. */
8
+ volume?: number;
9
+ /** Begin playing as soon as the visitor's first gesture unlocks audio. Defaults to `false`. */
10
+ autoplay?: boolean;
11
+ }
12
+ /**
13
+ * A background audio source. Extensible via `type`: a live online `stream`
14
+ * (radio), a single file on `loop`, or an ordered `playlist` of files. Playback
15
+ * (play/pause/fade) is driven by `id` from the runtime through `ctx.media` — the
16
+ * definition stays pure data.
17
+ */
18
+ type AudioSource = (AudioBase & {
19
+ type: "stream"; /** URL of the live stream / online radio. */
20
+ src: string;
21
+ }) | (AudioBase & {
22
+ type: "loop"; /** URL of the file played on repeat. */
23
+ src: string;
24
+ }) | (AudioBase & {
25
+ type: "playlist"; /** Track URLs played in sequence. */
26
+ tracks: string[]; /** Restart from the first track when the last ends. Defaults to `true`. */
27
+ loop?: boolean; /** Play tracks in random order. Defaults to `false`. */
28
+ shuffle?: boolean; /** Crossfade between tracks, in ms. Defaults to `0` (hard cut). */
29
+ crossfade?: number;
30
+ });
31
+ /** The discriminant values an `AudioSource` can take. */
32
+ type AudioSourceType = AudioSource["type"];
33
+ declare module "@scenography/scene" {
34
+ interface Scene {
35
+ /** Background audio sources — provided by `@scenography/plugin-audio`. */
36
+ audio?: AudioSource[];
37
+ }
38
+ }
39
+ //#endregion
40
+ //#region src/data/validate.d.ts
41
+ declare const validateAudio: (audio: AudioSource) => ValidationResult;
42
+ //#endregion
43
+ //#region src/data/plugin.d.ts
44
+ /** The data half of the audio plugin: owns the `audio` scene key (a collection). */
45
+ declare const audioData: DataPlugin<AudioSource>;
46
+ //#endregion
47
+ export { AudioSourceType as i, validateAudio as n, AudioSource as r, audioData as t };
48
+ //# sourceMappingURL=index-CR9ApbPg.d.mts.map
package/dist/index.cjs ADDED
@@ -0,0 +1,222 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_data = require("./data-vyKDnIYf.cjs");
3
+ let three = require("three");
4
+ //#region src/AudioEngine.ts
5
+ const range = (n) => Array.from({ length: n }, (_, i) => i);
6
+ const shuffled = (n) => {
7
+ const a = range(n);
8
+ for (let i = a.length - 1; i > 0; i--) {
9
+ const j = Math.floor(Math.random() * (i + 1));
10
+ [a[i], a[j]] = [a[j], a[i]];
11
+ }
12
+ return a;
13
+ };
14
+ /**
15
+ * Background-audio runtime over three.js `Audio` + an `AudioListener` on the
16
+ * camera (no extra dependency; positional audio stays reachable later). Streams
17
+ * and playlists ride an `HTMLAudioElement` routed through the listener's gain so
18
+ * volume/fades go through the Web Audio graph.
19
+ *
20
+ * Browsers block audio until a user gesture, so playback is unlocked on the
21
+ * first pointer/key event; `autoplay` sources start then.
22
+ */
23
+ var AudioEngine = class {
24
+ listener;
25
+ camera;
26
+ players = /* @__PURE__ */ new Map();
27
+ unlocked = false;
28
+ masterMuted = false;
29
+ constructor(camera) {
30
+ this.camera = camera;
31
+ this.listener = new three.AudioListener();
32
+ camera.add(this.listener);
33
+ window.addEventListener("pointerdown", this.unlock);
34
+ window.addEventListener("keydown", this.unlock);
35
+ }
36
+ unlock = () => {
37
+ if (this.unlocked) return;
38
+ this.unlocked = true;
39
+ this.listener.context.resume();
40
+ for (const player of this.players.values()) if (player.source.autoplay) this.start(player);
41
+ window.removeEventListener("pointerdown", this.unlock);
42
+ window.removeEventListener("keydown", this.unlock);
43
+ };
44
+ /** Diff incoming audio sources against mounted players, by `id`. */
45
+ reconcile(sources) {
46
+ const list = sources ?? [];
47
+ const next = new Set(list.map((s) => s.id));
48
+ for (const [id, player] of this.players) if (!next.has(id)) {
49
+ this.teardown(player);
50
+ this.players.delete(id);
51
+ }
52
+ for (const source of list) {
53
+ const hash = JSON.stringify(source);
54
+ const existing = this.players.get(source.id);
55
+ if (existing?.hash === hash) continue;
56
+ if (existing) {
57
+ this.teardown(existing);
58
+ this.players.delete(source.id);
59
+ }
60
+ const player = this.build(source, hash);
61
+ this.players.set(source.id, player);
62
+ if (this.unlocked && source.autoplay) this.start(player);
63
+ }
64
+ }
65
+ build(source, hash) {
66
+ const el = document.createElement("audio");
67
+ el.crossOrigin = "anonymous";
68
+ el.preload = "auto";
69
+ const order = source.type === "playlist" ? source.shuffle ? shuffled(source.tracks.length) : range(source.tracks.length) : [0];
70
+ const audio = new three.Audio(this.listener);
71
+ audio.setMediaElementSource(el);
72
+ const volume = source.volume ?? 1;
73
+ audio.setVolume(volume);
74
+ const player = {
75
+ source,
76
+ hash,
77
+ el,
78
+ audio,
79
+ volume,
80
+ current: volume,
81
+ order,
82
+ index: 0
83
+ };
84
+ if (source.type === "loop") {
85
+ el.loop = true;
86
+ el.src = source.src;
87
+ } else if (source.type === "stream") el.src = source.src;
88
+ else {
89
+ el.addEventListener("ended", () => this.onTrackEnded(player));
90
+ this.loadTrack(player, 0);
91
+ }
92
+ return player;
93
+ }
94
+ loadTrack(player, index) {
95
+ if (player.source.type !== "playlist") return;
96
+ const track = player.order[index];
97
+ const url = track !== void 0 ? player.source.tracks[track] : void 0;
98
+ if (url === void 0) return;
99
+ player.index = index;
100
+ player.el.src = url;
101
+ }
102
+ onTrackEnded(player) {
103
+ const source = player.source;
104
+ if (source.type !== "playlist") return;
105
+ const next = player.index + 1;
106
+ if (next < player.order.length) this.advance(player, next);
107
+ else if (source.loop ?? true) {
108
+ if (source.shuffle) player.order = shuffled(source.tracks.length);
109
+ this.advance(player, 0);
110
+ }
111
+ }
112
+ advance(player, index) {
113
+ this.loadTrack(player, index);
114
+ const crossfade = player.source.type === "playlist" ? player.source.crossfade ?? 0 : 0;
115
+ if (crossfade > 0) {
116
+ this.applyVolume(player, 0);
117
+ this.fadeTo(player, player.volume, crossfade);
118
+ }
119
+ player.el.play().catch(() => void 0);
120
+ }
121
+ start(player) {
122
+ this.listener.context.resume();
123
+ player.el.play().catch(() => void 0);
124
+ }
125
+ applyVolume(player, volume) {
126
+ player.current = volume;
127
+ player.audio.setVolume(volume);
128
+ }
129
+ fadeTo(player, to, duration) {
130
+ if (!duration || duration <= 0) {
131
+ player.fade = void 0;
132
+ this.applyVolume(player, to);
133
+ return;
134
+ }
135
+ player.fade = {
136
+ from: player.current,
137
+ to,
138
+ elapsed: 0,
139
+ duration
140
+ };
141
+ }
142
+ teardown(player) {
143
+ player.el.pause();
144
+ player.el.removeAttribute("src");
145
+ player.audio.disconnect();
146
+ }
147
+ /** Advance active volume fades. Called once per frame with the delta in ms. */
148
+ update(deltaMs) {
149
+ for (const player of this.players.values()) {
150
+ const fade = player.fade;
151
+ if (!fade) continue;
152
+ fade.elapsed += deltaMs;
153
+ const k = fade.duration > 0 ? Math.min(fade.elapsed / fade.duration, 1) : 1;
154
+ this.applyVolume(player, fade.from + (fade.to - fade.from) * k);
155
+ if (k >= 1) player.fade = void 0;
156
+ }
157
+ }
158
+ /** The `ctx.media` surface backed by this engine. */
159
+ controller = {
160
+ play: (id) => {
161
+ const player = this.players.get(id);
162
+ if (player) this.start(player);
163
+ },
164
+ pause: (id) => this.players.get(id)?.el.pause(),
165
+ stop: (id) => {
166
+ const player = this.players.get(id);
167
+ if (!player) return;
168
+ player.el.pause();
169
+ if (player.source.type !== "stream") player.el.currentTime = 0;
170
+ },
171
+ setVolume: (id, volume, opts) => {
172
+ const player = this.players.get(id);
173
+ if (!player) return;
174
+ player.volume = volume;
175
+ this.fadeTo(player, volume, opts?.duration ?? 0);
176
+ },
177
+ playing: (id) => {
178
+ const player = this.players.get(id);
179
+ return player ? !player.el.paused : false;
180
+ },
181
+ setMuted: (muted) => {
182
+ this.masterMuted = muted;
183
+ this.listener.setMasterVolume(muted ? 0 : 1);
184
+ },
185
+ muted: () => this.masterMuted
186
+ };
187
+ dispose() {
188
+ window.removeEventListener("pointerdown", this.unlock);
189
+ window.removeEventListener("keydown", this.unlock);
190
+ for (const player of this.players.values()) this.teardown(player);
191
+ this.players.clear();
192
+ this.camera.remove(this.listener);
193
+ }
194
+ };
195
+ //#endregion
196
+ //#region src/plugin.ts
197
+ /**
198
+ * Background audio — a *system* plugin. It attaches an `AudioListener` to the
199
+ * kernel camera and plays/loops/streams the sources in `scene.audio`, exposing
200
+ * `ctx.media` (play/pause/volume/mute). It manages its own audio nodes rather
201
+ * than scene-graph objects, so it lives outside the element reconciler.
202
+ */
203
+ const audio = () => ({
204
+ name: "audio",
205
+ setup: (ctx) => {
206
+ const engine = new AudioEngine(ctx.camera);
207
+ engine.reconcile(ctx.scene.audio);
208
+ return {
209
+ update: (deltaMs) => engine.update(deltaMs),
210
+ setScene: (scene) => engine.reconcile(scene.audio),
211
+ dispose: () => engine.dispose(),
212
+ api: engine.controller
213
+ };
214
+ }
215
+ });
216
+ //#endregion
217
+ exports.AudioEngine = AudioEngine;
218
+ exports.audio = audio;
219
+ exports.audioData = require_data.audioData;
220
+ exports.validateAudio = require_data.validateAudio;
221
+
222
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.cjs","names":["AudioListener","ThreeAudio"],"sources":["../src/AudioEngine.ts","../src/plugin.ts"],"sourcesContent":["import type { MediaController } from \"@scenography/engine\";\nimport { Audio as ThreeAudio, AudioListener, type Object3D } from \"three\";\n\nimport type { AudioSource } from \"./data\";\n\ninterface VolumeFade {\n from: number;\n to: number;\n elapsed: number;\n duration: number;\n}\n\ninterface Player {\n source: AudioSource;\n hash: string;\n el: HTMLAudioElement;\n audio: ThreeAudio;\n /** Configured base volume (the fade target for `play`/track changes). */\n volume: number;\n /** Currently applied volume. */\n current: number;\n fade?: VolumeFade;\n /** Playlist playback order (indices into `tracks`); single-entry otherwise. */\n order: number[];\n /** Position within `order`. */\n index: number;\n}\n\nconst range = (n: number): number[] => Array.from({ length: n }, (_, i) => i);\n\nconst shuffled = (n: number): number[] => {\n const a = range(n);\n for (let i = a.length - 1; i > 0; i--) {\n const j = Math.floor(Math.random() * (i + 1));\n [a[i], a[j]] = [a[j]!, a[i]!];\n }\n return a;\n};\n\n/**\n * Background-audio runtime over three.js `Audio` + an `AudioListener` on the\n * camera (no extra dependency; positional audio stays reachable later). Streams\n * and playlists ride an `HTMLAudioElement` routed through the listener's gain so\n * volume/fades go through the Web Audio graph.\n *\n * Browsers block audio until a user gesture, so playback is unlocked on the\n * first pointer/key event; `autoplay` sources start then.\n */\nexport class AudioEngine {\n private readonly listener: AudioListener;\n private readonly camera: Object3D;\n private readonly players = new Map<string, Player>();\n private unlocked = false;\n private masterMuted = false;\n\n constructor(camera: Object3D) {\n this.camera = camera;\n this.listener = new AudioListener();\n camera.add(this.listener);\n window.addEventListener(\"pointerdown\", this.unlock);\n window.addEventListener(\"keydown\", this.unlock);\n }\n\n private readonly unlock = (): void => {\n if (this.unlocked) return;\n this.unlocked = true;\n void this.listener.context.resume();\n for (const player of this.players.values()) {\n if (player.source.autoplay) this.start(player);\n }\n window.removeEventListener(\"pointerdown\", this.unlock);\n window.removeEventListener(\"keydown\", this.unlock);\n };\n\n /** Diff incoming audio sources against mounted players, by `id`. */\n reconcile(sources: AudioSource[] | undefined): void {\n const list = sources ?? [];\n const next = new Set(list.map((s) => s.id));\n for (const [id, player] of this.players) {\n if (!next.has(id)) {\n this.teardown(player);\n this.players.delete(id);\n }\n }\n\n for (const source of list) {\n const hash = JSON.stringify(source);\n const existing = this.players.get(source.id);\n if (existing?.hash === hash) continue;\n if (existing) {\n this.teardown(existing);\n this.players.delete(source.id);\n }\n const player = this.build(source, hash);\n this.players.set(source.id, player);\n if (this.unlocked && source.autoplay) this.start(player);\n }\n }\n\n private build(source: AudioSource, hash: string): Player {\n const el = document.createElement(\"audio\");\n el.crossOrigin = \"anonymous\";\n el.preload = \"auto\";\n\n const order =\n source.type === \"playlist\"\n ? source.shuffle\n ? shuffled(source.tracks.length)\n : range(source.tracks.length)\n : [0];\n\n const audio = new ThreeAudio(this.listener);\n audio.setMediaElementSource(el);\n const volume = source.volume ?? 1;\n audio.setVolume(volume);\n\n const player: Player = { source, hash, el, audio, volume, current: volume, order, index: 0 };\n\n if (source.type === \"loop\") {\n el.loop = true;\n el.src = source.src;\n } else if (source.type === \"stream\") {\n el.src = source.src;\n } else {\n el.addEventListener(\"ended\", () => this.onTrackEnded(player));\n this.loadTrack(player, 0);\n }\n\n return player;\n }\n\n private loadTrack(player: Player, index: number): void {\n if (player.source.type !== \"playlist\") return;\n const track = player.order[index];\n const url = track !== undefined ? player.source.tracks[track] : undefined;\n if (url === undefined) return;\n player.index = index;\n player.el.src = url;\n }\n\n private onTrackEnded(player: Player): void {\n const source = player.source;\n if (source.type !== \"playlist\") return;\n const next = player.index + 1;\n if (next < player.order.length) {\n this.advance(player, next);\n } else if (source.loop ?? true) {\n if (source.shuffle) player.order = shuffled(source.tracks.length);\n this.advance(player, 0);\n }\n }\n\n private advance(player: Player, index: number): void {\n this.loadTrack(player, index);\n const crossfade = player.source.type === \"playlist\" ? (player.source.crossfade ?? 0) : 0;\n if (crossfade > 0) {\n this.applyVolume(player, 0);\n this.fadeTo(player, player.volume, crossfade);\n }\n void player.el.play().catch(() => undefined);\n }\n\n private start(player: Player): void {\n void this.listener.context.resume();\n void player.el.play().catch(() => undefined);\n }\n\n private applyVolume(player: Player, volume: number): void {\n player.current = volume;\n player.audio.setVolume(volume);\n }\n\n private fadeTo(player: Player, to: number, duration: number): void {\n if (!duration || duration <= 0) {\n player.fade = undefined;\n this.applyVolume(player, to);\n return;\n }\n player.fade = { from: player.current, to, elapsed: 0, duration };\n }\n\n private teardown(player: Player): void {\n player.el.pause();\n player.el.removeAttribute(\"src\");\n player.audio.disconnect();\n }\n\n /** Advance active volume fades. Called once per frame with the delta in ms. */\n update(deltaMs: number): void {\n for (const player of this.players.values()) {\n const fade = player.fade;\n if (!fade) continue;\n fade.elapsed += deltaMs;\n const k = fade.duration > 0 ? Math.min(fade.elapsed / fade.duration, 1) : 1;\n this.applyVolume(player, fade.from + (fade.to - fade.from) * k);\n if (k >= 1) player.fade = undefined;\n }\n }\n\n /** The `ctx.media` surface backed by this engine. */\n readonly controller: MediaController = {\n play: (id) => {\n const player = this.players.get(id);\n if (player) this.start(player);\n },\n pause: (id) => this.players.get(id)?.el.pause(),\n stop: (id) => {\n const player = this.players.get(id);\n if (!player) return;\n player.el.pause();\n if (player.source.type !== \"stream\") player.el.currentTime = 0;\n },\n setVolume: (id, volume, opts) => {\n const player = this.players.get(id);\n if (!player) return;\n player.volume = volume;\n this.fadeTo(player, volume, opts?.duration ?? 0);\n },\n playing: (id) => {\n const player = this.players.get(id);\n return player ? !player.el.paused : false;\n },\n setMuted: (muted) => {\n this.masterMuted = muted;\n this.listener.setMasterVolume(muted ? 0 : 1);\n },\n muted: () => this.masterMuted,\n };\n\n dispose(): void {\n window.removeEventListener(\"pointerdown\", this.unlock);\n window.removeEventListener(\"keydown\", this.unlock);\n for (const player of this.players.values()) this.teardown(player);\n this.players.clear();\n this.camera.remove(this.listener);\n }\n}\n","import type { SystemPlugin, MediaController } from \"@scenography/engine\";\n\nimport { AudioEngine } from \"./AudioEngine\";\n\n/**\n * Background audio — a *system* plugin. It attaches an `AudioListener` to the\n * kernel camera and plays/loops/streams the sources in `scene.audio`, exposing\n * `ctx.media` (play/pause/volume/mute). It manages its own audio nodes rather\n * than scene-graph objects, so it lives outside the element reconciler.\n */\nexport const audio = (): SystemPlugin<MediaController> => ({\n name: \"audio\",\n setup: (ctx) => {\n const engine = new AudioEngine(ctx.camera);\n engine.reconcile(ctx.scene.audio);\n return {\n update: (deltaMs) => engine.update(deltaMs),\n setScene: (scene) => engine.reconcile(scene.audio),\n dispose: () => engine.dispose(),\n api: engine.controller,\n };\n },\n});\n"],"mappings":"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"}
@@ -0,0 +1,50 @@
1
+ import { i as AudioSourceType, n as validateAudio, r as AudioSource, t as audioData } from "./index-CR9ApbPg.cjs";
2
+ import { MediaController, SystemPlugin } from "@scenography/engine";
3
+ import { Object3D } from "three";
4
+
5
+ //#region src/plugin.d.ts
6
+ /**
7
+ * Background audio — a *system* plugin. It attaches an `AudioListener` to the
8
+ * kernel camera and plays/loops/streams the sources in `scene.audio`, exposing
9
+ * `ctx.media` (play/pause/volume/mute). It manages its own audio nodes rather
10
+ * than scene-graph objects, so it lives outside the element reconciler.
11
+ */
12
+ declare const audio: () => SystemPlugin<MediaController>;
13
+ //#endregion
14
+ //#region src/AudioEngine.d.ts
15
+ /**
16
+ * Background-audio runtime over three.js `Audio` + an `AudioListener` on the
17
+ * camera (no extra dependency; positional audio stays reachable later). Streams
18
+ * and playlists ride an `HTMLAudioElement` routed through the listener's gain so
19
+ * volume/fades go through the Web Audio graph.
20
+ *
21
+ * Browsers block audio until a user gesture, so playback is unlocked on the
22
+ * first pointer/key event; `autoplay` sources start then.
23
+ */
24
+ declare class AudioEngine {
25
+ private readonly listener;
26
+ private readonly camera;
27
+ private readonly players;
28
+ private unlocked;
29
+ private masterMuted;
30
+ constructor(camera: Object3D);
31
+ private readonly unlock;
32
+ /** Diff incoming audio sources against mounted players, by `id`. */
33
+ reconcile(sources: AudioSource[] | undefined): void;
34
+ private build;
35
+ private loadTrack;
36
+ private onTrackEnded;
37
+ private advance;
38
+ private start;
39
+ private applyVolume;
40
+ private fadeTo;
41
+ private teardown;
42
+ /** Advance active volume fades. Called once per frame with the delta in ms. */
43
+ update(deltaMs: number): void;
44
+ /** The `ctx.media` surface backed by this engine. */
45
+ readonly controller: MediaController;
46
+ dispose(): void;
47
+ }
48
+ //#endregion
49
+ export { AudioEngine, type AudioSource, type AudioSourceType, audio, audioData, validateAudio };
50
+ //# sourceMappingURL=index.d.cts.map
@@ -0,0 +1,50 @@
1
+ import { i as AudioSourceType, n as validateAudio, r as AudioSource, t as audioData } from "./index-CR9ApbPg.mjs";
2
+ import { Object3D } from "three";
3
+ import { MediaController, SystemPlugin } from "@scenography/engine";
4
+
5
+ //#region src/plugin.d.ts
6
+ /**
7
+ * Background audio — a *system* plugin. It attaches an `AudioListener` to the
8
+ * kernel camera and plays/loops/streams the sources in `scene.audio`, exposing
9
+ * `ctx.media` (play/pause/volume/mute). It manages its own audio nodes rather
10
+ * than scene-graph objects, so it lives outside the element reconciler.
11
+ */
12
+ declare const audio: () => SystemPlugin<MediaController>;
13
+ //#endregion
14
+ //#region src/AudioEngine.d.ts
15
+ /**
16
+ * Background-audio runtime over three.js `Audio` + an `AudioListener` on the
17
+ * camera (no extra dependency; positional audio stays reachable later). Streams
18
+ * and playlists ride an `HTMLAudioElement` routed through the listener's gain so
19
+ * volume/fades go through the Web Audio graph.
20
+ *
21
+ * Browsers block audio until a user gesture, so playback is unlocked on the
22
+ * first pointer/key event; `autoplay` sources start then.
23
+ */
24
+ declare class AudioEngine {
25
+ private readonly listener;
26
+ private readonly camera;
27
+ private readonly players;
28
+ private unlocked;
29
+ private masterMuted;
30
+ constructor(camera: Object3D);
31
+ private readonly unlock;
32
+ /** Diff incoming audio sources against mounted players, by `id`. */
33
+ reconcile(sources: AudioSource[] | undefined): void;
34
+ private build;
35
+ private loadTrack;
36
+ private onTrackEnded;
37
+ private advance;
38
+ private start;
39
+ private applyVolume;
40
+ private fadeTo;
41
+ private teardown;
42
+ /** Advance active volume fades. Called once per frame with the delta in ms. */
43
+ update(deltaMs: number): void;
44
+ /** The `ctx.media` surface backed by this engine. */
45
+ readonly controller: MediaController;
46
+ dispose(): void;
47
+ }
48
+ //#endregion
49
+ export { AudioEngine, type AudioSource, type AudioSourceType, audio, audioData, validateAudio };
50
+ //# sourceMappingURL=index.d.mts.map
package/dist/index.mjs ADDED
@@ -0,0 +1,218 @@
1
+ import { n as validateAudio, t as audioData } from "./data-ChoGUA4U.mjs";
2
+ import { Audio, AudioListener } from "three";
3
+ //#region src/AudioEngine.ts
4
+ const range = (n) => Array.from({ length: n }, (_, i) => i);
5
+ const shuffled = (n) => {
6
+ const a = range(n);
7
+ for (let i = a.length - 1; i > 0; i--) {
8
+ const j = Math.floor(Math.random() * (i + 1));
9
+ [a[i], a[j]] = [a[j], a[i]];
10
+ }
11
+ return a;
12
+ };
13
+ /**
14
+ * Background-audio runtime over three.js `Audio` + an `AudioListener` on the
15
+ * camera (no extra dependency; positional audio stays reachable later). Streams
16
+ * and playlists ride an `HTMLAudioElement` routed through the listener's gain so
17
+ * volume/fades go through the Web Audio graph.
18
+ *
19
+ * Browsers block audio until a user gesture, so playback is unlocked on the
20
+ * first pointer/key event; `autoplay` sources start then.
21
+ */
22
+ var AudioEngine = class {
23
+ listener;
24
+ camera;
25
+ players = /* @__PURE__ */ new Map();
26
+ unlocked = false;
27
+ masterMuted = false;
28
+ constructor(camera) {
29
+ this.camera = camera;
30
+ this.listener = new AudioListener();
31
+ camera.add(this.listener);
32
+ window.addEventListener("pointerdown", this.unlock);
33
+ window.addEventListener("keydown", this.unlock);
34
+ }
35
+ unlock = () => {
36
+ if (this.unlocked) return;
37
+ this.unlocked = true;
38
+ this.listener.context.resume();
39
+ for (const player of this.players.values()) if (player.source.autoplay) this.start(player);
40
+ window.removeEventListener("pointerdown", this.unlock);
41
+ window.removeEventListener("keydown", this.unlock);
42
+ };
43
+ /** Diff incoming audio sources against mounted players, by `id`. */
44
+ reconcile(sources) {
45
+ const list = sources ?? [];
46
+ const next = new Set(list.map((s) => s.id));
47
+ for (const [id, player] of this.players) if (!next.has(id)) {
48
+ this.teardown(player);
49
+ this.players.delete(id);
50
+ }
51
+ for (const source of list) {
52
+ const hash = JSON.stringify(source);
53
+ const existing = this.players.get(source.id);
54
+ if (existing?.hash === hash) continue;
55
+ if (existing) {
56
+ this.teardown(existing);
57
+ this.players.delete(source.id);
58
+ }
59
+ const player = this.build(source, hash);
60
+ this.players.set(source.id, player);
61
+ if (this.unlocked && source.autoplay) this.start(player);
62
+ }
63
+ }
64
+ build(source, hash) {
65
+ const el = document.createElement("audio");
66
+ el.crossOrigin = "anonymous";
67
+ el.preload = "auto";
68
+ const order = source.type === "playlist" ? source.shuffle ? shuffled(source.tracks.length) : range(source.tracks.length) : [0];
69
+ const audio = new Audio(this.listener);
70
+ audio.setMediaElementSource(el);
71
+ const volume = source.volume ?? 1;
72
+ audio.setVolume(volume);
73
+ const player = {
74
+ source,
75
+ hash,
76
+ el,
77
+ audio,
78
+ volume,
79
+ current: volume,
80
+ order,
81
+ index: 0
82
+ };
83
+ if (source.type === "loop") {
84
+ el.loop = true;
85
+ el.src = source.src;
86
+ } else if (source.type === "stream") el.src = source.src;
87
+ else {
88
+ el.addEventListener("ended", () => this.onTrackEnded(player));
89
+ this.loadTrack(player, 0);
90
+ }
91
+ return player;
92
+ }
93
+ loadTrack(player, index) {
94
+ if (player.source.type !== "playlist") return;
95
+ const track = player.order[index];
96
+ const url = track !== void 0 ? player.source.tracks[track] : void 0;
97
+ if (url === void 0) return;
98
+ player.index = index;
99
+ player.el.src = url;
100
+ }
101
+ onTrackEnded(player) {
102
+ const source = player.source;
103
+ if (source.type !== "playlist") return;
104
+ const next = player.index + 1;
105
+ if (next < player.order.length) this.advance(player, next);
106
+ else if (source.loop ?? true) {
107
+ if (source.shuffle) player.order = shuffled(source.tracks.length);
108
+ this.advance(player, 0);
109
+ }
110
+ }
111
+ advance(player, index) {
112
+ this.loadTrack(player, index);
113
+ const crossfade = player.source.type === "playlist" ? player.source.crossfade ?? 0 : 0;
114
+ if (crossfade > 0) {
115
+ this.applyVolume(player, 0);
116
+ this.fadeTo(player, player.volume, crossfade);
117
+ }
118
+ player.el.play().catch(() => void 0);
119
+ }
120
+ start(player) {
121
+ this.listener.context.resume();
122
+ player.el.play().catch(() => void 0);
123
+ }
124
+ applyVolume(player, volume) {
125
+ player.current = volume;
126
+ player.audio.setVolume(volume);
127
+ }
128
+ fadeTo(player, to, duration) {
129
+ if (!duration || duration <= 0) {
130
+ player.fade = void 0;
131
+ this.applyVolume(player, to);
132
+ return;
133
+ }
134
+ player.fade = {
135
+ from: player.current,
136
+ to,
137
+ elapsed: 0,
138
+ duration
139
+ };
140
+ }
141
+ teardown(player) {
142
+ player.el.pause();
143
+ player.el.removeAttribute("src");
144
+ player.audio.disconnect();
145
+ }
146
+ /** Advance active volume fades. Called once per frame with the delta in ms. */
147
+ update(deltaMs) {
148
+ for (const player of this.players.values()) {
149
+ const fade = player.fade;
150
+ if (!fade) continue;
151
+ fade.elapsed += deltaMs;
152
+ const k = fade.duration > 0 ? Math.min(fade.elapsed / fade.duration, 1) : 1;
153
+ this.applyVolume(player, fade.from + (fade.to - fade.from) * k);
154
+ if (k >= 1) player.fade = void 0;
155
+ }
156
+ }
157
+ /** The `ctx.media` surface backed by this engine. */
158
+ controller = {
159
+ play: (id) => {
160
+ const player = this.players.get(id);
161
+ if (player) this.start(player);
162
+ },
163
+ pause: (id) => this.players.get(id)?.el.pause(),
164
+ stop: (id) => {
165
+ const player = this.players.get(id);
166
+ if (!player) return;
167
+ player.el.pause();
168
+ if (player.source.type !== "stream") player.el.currentTime = 0;
169
+ },
170
+ setVolume: (id, volume, opts) => {
171
+ const player = this.players.get(id);
172
+ if (!player) return;
173
+ player.volume = volume;
174
+ this.fadeTo(player, volume, opts?.duration ?? 0);
175
+ },
176
+ playing: (id) => {
177
+ const player = this.players.get(id);
178
+ return player ? !player.el.paused : false;
179
+ },
180
+ setMuted: (muted) => {
181
+ this.masterMuted = muted;
182
+ this.listener.setMasterVolume(muted ? 0 : 1);
183
+ },
184
+ muted: () => this.masterMuted
185
+ };
186
+ dispose() {
187
+ window.removeEventListener("pointerdown", this.unlock);
188
+ window.removeEventListener("keydown", this.unlock);
189
+ for (const player of this.players.values()) this.teardown(player);
190
+ this.players.clear();
191
+ this.camera.remove(this.listener);
192
+ }
193
+ };
194
+ //#endregion
195
+ //#region src/plugin.ts
196
+ /**
197
+ * Background audio — a *system* plugin. It attaches an `AudioListener` to the
198
+ * kernel camera and plays/loops/streams the sources in `scene.audio`, exposing
199
+ * `ctx.media` (play/pause/volume/mute). It manages its own audio nodes rather
200
+ * than scene-graph objects, so it lives outside the element reconciler.
201
+ */
202
+ const audio = () => ({
203
+ name: "audio",
204
+ setup: (ctx) => {
205
+ const engine = new AudioEngine(ctx.camera);
206
+ engine.reconcile(ctx.scene.audio);
207
+ return {
208
+ update: (deltaMs) => engine.update(deltaMs),
209
+ setScene: (scene) => engine.reconcile(scene.audio),
210
+ dispose: () => engine.dispose(),
211
+ api: engine.controller
212
+ };
213
+ }
214
+ });
215
+ //#endregion
216
+ export { AudioEngine, audio, audioData, validateAudio };
217
+
218
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":["ThreeAudio"],"sources":["../src/AudioEngine.ts","../src/plugin.ts"],"sourcesContent":["import type { MediaController } from \"@scenography/engine\";\nimport { Audio as ThreeAudio, AudioListener, type Object3D } from \"three\";\n\nimport type { AudioSource } from \"./data\";\n\ninterface VolumeFade {\n from: number;\n to: number;\n elapsed: number;\n duration: number;\n}\n\ninterface Player {\n source: AudioSource;\n hash: string;\n el: HTMLAudioElement;\n audio: ThreeAudio;\n /** Configured base volume (the fade target for `play`/track changes). */\n volume: number;\n /** Currently applied volume. */\n current: number;\n fade?: VolumeFade;\n /** Playlist playback order (indices into `tracks`); single-entry otherwise. */\n order: number[];\n /** Position within `order`. */\n index: number;\n}\n\nconst range = (n: number): number[] => Array.from({ length: n }, (_, i) => i);\n\nconst shuffled = (n: number): number[] => {\n const a = range(n);\n for (let i = a.length - 1; i > 0; i--) {\n const j = Math.floor(Math.random() * (i + 1));\n [a[i], a[j]] = [a[j]!, a[i]!];\n }\n return a;\n};\n\n/**\n * Background-audio runtime over three.js `Audio` + an `AudioListener` on the\n * camera (no extra dependency; positional audio stays reachable later). Streams\n * and playlists ride an `HTMLAudioElement` routed through the listener's gain so\n * volume/fades go through the Web Audio graph.\n *\n * Browsers block audio until a user gesture, so playback is unlocked on the\n * first pointer/key event; `autoplay` sources start then.\n */\nexport class AudioEngine {\n private readonly listener: AudioListener;\n private readonly camera: Object3D;\n private readonly players = new Map<string, Player>();\n private unlocked = false;\n private masterMuted = false;\n\n constructor(camera: Object3D) {\n this.camera = camera;\n this.listener = new AudioListener();\n camera.add(this.listener);\n window.addEventListener(\"pointerdown\", this.unlock);\n window.addEventListener(\"keydown\", this.unlock);\n }\n\n private readonly unlock = (): void => {\n if (this.unlocked) return;\n this.unlocked = true;\n void this.listener.context.resume();\n for (const player of this.players.values()) {\n if (player.source.autoplay) this.start(player);\n }\n window.removeEventListener(\"pointerdown\", this.unlock);\n window.removeEventListener(\"keydown\", this.unlock);\n };\n\n /** Diff incoming audio sources against mounted players, by `id`. */\n reconcile(sources: AudioSource[] | undefined): void {\n const list = sources ?? [];\n const next = new Set(list.map((s) => s.id));\n for (const [id, player] of this.players) {\n if (!next.has(id)) {\n this.teardown(player);\n this.players.delete(id);\n }\n }\n\n for (const source of list) {\n const hash = JSON.stringify(source);\n const existing = this.players.get(source.id);\n if (existing?.hash === hash) continue;\n if (existing) {\n this.teardown(existing);\n this.players.delete(source.id);\n }\n const player = this.build(source, hash);\n this.players.set(source.id, player);\n if (this.unlocked && source.autoplay) this.start(player);\n }\n }\n\n private build(source: AudioSource, hash: string): Player {\n const el = document.createElement(\"audio\");\n el.crossOrigin = \"anonymous\";\n el.preload = \"auto\";\n\n const order =\n source.type === \"playlist\"\n ? source.shuffle\n ? shuffled(source.tracks.length)\n : range(source.tracks.length)\n : [0];\n\n const audio = new ThreeAudio(this.listener);\n audio.setMediaElementSource(el);\n const volume = source.volume ?? 1;\n audio.setVolume(volume);\n\n const player: Player = { source, hash, el, audio, volume, current: volume, order, index: 0 };\n\n if (source.type === \"loop\") {\n el.loop = true;\n el.src = source.src;\n } else if (source.type === \"stream\") {\n el.src = source.src;\n } else {\n el.addEventListener(\"ended\", () => this.onTrackEnded(player));\n this.loadTrack(player, 0);\n }\n\n return player;\n }\n\n private loadTrack(player: Player, index: number): void {\n if (player.source.type !== \"playlist\") return;\n const track = player.order[index];\n const url = track !== undefined ? player.source.tracks[track] : undefined;\n if (url === undefined) return;\n player.index = index;\n player.el.src = url;\n }\n\n private onTrackEnded(player: Player): void {\n const source = player.source;\n if (source.type !== \"playlist\") return;\n const next = player.index + 1;\n if (next < player.order.length) {\n this.advance(player, next);\n } else if (source.loop ?? true) {\n if (source.shuffle) player.order = shuffled(source.tracks.length);\n this.advance(player, 0);\n }\n }\n\n private advance(player: Player, index: number): void {\n this.loadTrack(player, index);\n const crossfade = player.source.type === \"playlist\" ? (player.source.crossfade ?? 0) : 0;\n if (crossfade > 0) {\n this.applyVolume(player, 0);\n this.fadeTo(player, player.volume, crossfade);\n }\n void player.el.play().catch(() => undefined);\n }\n\n private start(player: Player): void {\n void this.listener.context.resume();\n void player.el.play().catch(() => undefined);\n }\n\n private applyVolume(player: Player, volume: number): void {\n player.current = volume;\n player.audio.setVolume(volume);\n }\n\n private fadeTo(player: Player, to: number, duration: number): void {\n if (!duration || duration <= 0) {\n player.fade = undefined;\n this.applyVolume(player, to);\n return;\n }\n player.fade = { from: player.current, to, elapsed: 0, duration };\n }\n\n private teardown(player: Player): void {\n player.el.pause();\n player.el.removeAttribute(\"src\");\n player.audio.disconnect();\n }\n\n /** Advance active volume fades. Called once per frame with the delta in ms. */\n update(deltaMs: number): void {\n for (const player of this.players.values()) {\n const fade = player.fade;\n if (!fade) continue;\n fade.elapsed += deltaMs;\n const k = fade.duration > 0 ? Math.min(fade.elapsed / fade.duration, 1) : 1;\n this.applyVolume(player, fade.from + (fade.to - fade.from) * k);\n if (k >= 1) player.fade = undefined;\n }\n }\n\n /** The `ctx.media` surface backed by this engine. */\n readonly controller: MediaController = {\n play: (id) => {\n const player = this.players.get(id);\n if (player) this.start(player);\n },\n pause: (id) => this.players.get(id)?.el.pause(),\n stop: (id) => {\n const player = this.players.get(id);\n if (!player) return;\n player.el.pause();\n if (player.source.type !== \"stream\") player.el.currentTime = 0;\n },\n setVolume: (id, volume, opts) => {\n const player = this.players.get(id);\n if (!player) return;\n player.volume = volume;\n this.fadeTo(player, volume, opts?.duration ?? 0);\n },\n playing: (id) => {\n const player = this.players.get(id);\n return player ? !player.el.paused : false;\n },\n setMuted: (muted) => {\n this.masterMuted = muted;\n this.listener.setMasterVolume(muted ? 0 : 1);\n },\n muted: () => this.masterMuted,\n };\n\n dispose(): void {\n window.removeEventListener(\"pointerdown\", this.unlock);\n window.removeEventListener(\"keydown\", this.unlock);\n for (const player of this.players.values()) this.teardown(player);\n this.players.clear();\n this.camera.remove(this.listener);\n }\n}\n","import type { SystemPlugin, MediaController } from \"@scenography/engine\";\n\nimport { AudioEngine } from \"./AudioEngine\";\n\n/**\n * Background audio — a *system* plugin. It attaches an `AudioListener` to the\n * kernel camera and plays/loops/streams the sources in `scene.audio`, exposing\n * `ctx.media` (play/pause/volume/mute). It manages its own audio nodes rather\n * than scene-graph objects, so it lives outside the element reconciler.\n */\nexport const audio = (): SystemPlugin<MediaController> => ({\n name: \"audio\",\n setup: (ctx) => {\n const engine = new AudioEngine(ctx.camera);\n engine.reconcile(ctx.scene.audio);\n return {\n update: (deltaMs) => engine.update(deltaMs),\n setScene: (scene) => engine.reconcile(scene.audio),\n dispose: () => engine.dispose(),\n api: engine.controller,\n };\n },\n});\n"],"mappings":";;;AA4BA,MAAM,SAAS,MAAwB,MAAM,KAAK,EAAE,QAAQ,EAAE,IAAI,GAAG,MAAM,CAAC;AAE5E,MAAM,YAAY,MAAwB;CACxC,MAAM,IAAI,MAAM,CAAC;CACjB,KAAK,IAAI,IAAI,EAAE,SAAS,GAAG,IAAI,GAAG,KAAK;EACrC,MAAM,IAAI,KAAK,MAAM,KAAK,OAAO,KAAK,IAAI,EAAE;EAC5C,CAAC,EAAE,IAAI,EAAE,MAAM,CAAC,EAAE,IAAK,EAAE,EAAG;CAC9B;CACA,OAAO;AACT;;;;;;;;;;AAWA,IAAa,cAAb,MAAyB;CACvB;CACA;CACA,0BAA2B,IAAI,IAAoB;CACnD,WAAmB;CACnB,cAAsB;CAEtB,YAAY,QAAkB;EAC5B,KAAK,SAAS;EACd,KAAK,WAAW,IAAI,cAAc;EAClC,OAAO,IAAI,KAAK,QAAQ;EACxB,OAAO,iBAAiB,eAAe,KAAK,MAAM;EAClD,OAAO,iBAAiB,WAAW,KAAK,MAAM;CAChD;CAEA,eAAsC;EACpC,IAAI,KAAK,UAAU;EACnB,KAAK,WAAW;EAChB,KAAU,SAAS,QAAQ,OAAO;EAClC,KAAK,MAAM,UAAU,KAAK,QAAQ,OAAO,GACvC,IAAI,OAAO,OAAO,UAAU,KAAK,MAAM,MAAM;EAE/C,OAAO,oBAAoB,eAAe,KAAK,MAAM;EACrD,OAAO,oBAAoB,WAAW,KAAK,MAAM;CACnD;;CAGA,UAAU,SAA0C;EAClD,MAAM,OAAO,WAAW,CAAC;EACzB,MAAM,OAAO,IAAI,IAAI,KAAK,KAAK,MAAM,EAAE,EAAE,CAAC;EAC1C,KAAK,MAAM,CAAC,IAAI,WAAW,KAAK,SAC9B,IAAI,CAAC,KAAK,IAAI,EAAE,GAAG;GACjB,KAAK,SAAS,MAAM;GACpB,KAAK,QAAQ,OAAO,EAAE;EACxB;EAGF,KAAK,MAAM,UAAU,MAAM;GACzB,MAAM,OAAO,KAAK,UAAU,MAAM;GAClC,MAAM,WAAW,KAAK,QAAQ,IAAI,OAAO,EAAE;GAC3C,IAAI,UAAU,SAAS,MAAM;GAC7B,IAAI,UAAU;IACZ,KAAK,SAAS,QAAQ;IACtB,KAAK,QAAQ,OAAO,OAAO,EAAE;GAC/B;GACA,MAAM,SAAS,KAAK,MAAM,QAAQ,IAAI;GACtC,KAAK,QAAQ,IAAI,OAAO,IAAI,MAAM;GAClC,IAAI,KAAK,YAAY,OAAO,UAAU,KAAK,MAAM,MAAM;EACzD;CACF;CAEA,MAAc,QAAqB,MAAsB;EACvD,MAAM,KAAK,SAAS,cAAc,OAAO;EACzC,GAAG,cAAc;EACjB,GAAG,UAAU;EAEb,MAAM,QACJ,OAAO,SAAS,aACZ,OAAO,UACL,SAAS,OAAO,OAAO,MAAM,IAC7B,MAAM,OAAO,OAAO,MAAM,IAC5B,CAAC,CAAC;EAER,MAAM,QAAQ,IAAIA,MAAW,KAAK,QAAQ;EAC1C,MAAM,sBAAsB,EAAE;EAC9B,MAAM,SAAS,OAAO,UAAU;EAChC,MAAM,UAAU,MAAM;EAEtB,MAAM,SAAiB;GAAE;GAAQ;GAAM;GAAI;GAAO;GAAQ,SAAS;GAAQ;GAAO,OAAO;EAAE;EAE3F,IAAI,OAAO,SAAS,QAAQ;GAC1B,GAAG,OAAO;GACV,GAAG,MAAM,OAAO;EAClB,OAAO,IAAI,OAAO,SAAS,UACzB,GAAG,MAAM,OAAO;OACX;GACL,GAAG,iBAAiB,eAAe,KAAK,aAAa,MAAM,CAAC;GAC5D,KAAK,UAAU,QAAQ,CAAC;EAC1B;EAEA,OAAO;CACT;CAEA,UAAkB,QAAgB,OAAqB;EACrD,IAAI,OAAO,OAAO,SAAS,YAAY;EACvC,MAAM,QAAQ,OAAO,MAAM;EAC3B,MAAM,MAAM,UAAU,KAAA,IAAY,OAAO,OAAO,OAAO,SAAS,KAAA;EAChE,IAAI,QAAQ,KAAA,GAAW;EACvB,OAAO,QAAQ;EACf,OAAO,GAAG,MAAM;CAClB;CAEA,aAAqB,QAAsB;EACzC,MAAM,SAAS,OAAO;EACtB,IAAI,OAAO,SAAS,YAAY;EAChC,MAAM,OAAO,OAAO,QAAQ;EAC5B,IAAI,OAAO,OAAO,MAAM,QACtB,KAAK,QAAQ,QAAQ,IAAI;OACpB,IAAI,OAAO,QAAQ,MAAM;GAC9B,IAAI,OAAO,SAAS,OAAO,QAAQ,SAAS,OAAO,OAAO,MAAM;GAChE,KAAK,QAAQ,QAAQ,CAAC;EACxB;CACF;CAEA,QAAgB,QAAgB,OAAqB;EACnD,KAAK,UAAU,QAAQ,KAAK;EAC5B,MAAM,YAAY,OAAO,OAAO,SAAS,aAAc,OAAO,OAAO,aAAa,IAAK;EACvF,IAAI,YAAY,GAAG;GACjB,KAAK,YAAY,QAAQ,CAAC;GAC1B,KAAK,OAAO,QAAQ,OAAO,QAAQ,SAAS;EAC9C;EACA,OAAY,GAAG,KAAK,CAAC,CAAC,YAAY,KAAA,CAAS;CAC7C;CAEA,MAAc,QAAsB;EAClC,KAAU,SAAS,QAAQ,OAAO;EAClC,OAAY,GAAG,KAAK,CAAC,CAAC,YAAY,KAAA,CAAS;CAC7C;CAEA,YAAoB,QAAgB,QAAsB;EACxD,OAAO,UAAU;EACjB,OAAO,MAAM,UAAU,MAAM;CAC/B;CAEA,OAAe,QAAgB,IAAY,UAAwB;EACjE,IAAI,CAAC,YAAY,YAAY,GAAG;GAC9B,OAAO,OAAO,KAAA;GACd,KAAK,YAAY,QAAQ,EAAE;GAC3B;EACF;EACA,OAAO,OAAO;GAAE,MAAM,OAAO;GAAS;GAAI,SAAS;GAAG;EAAS;CACjE;CAEA,SAAiB,QAAsB;EACrC,OAAO,GAAG,MAAM;EAChB,OAAO,GAAG,gBAAgB,KAAK;EAC/B,OAAO,MAAM,WAAW;CAC1B;;CAGA,OAAO,SAAuB;EAC5B,KAAK,MAAM,UAAU,KAAK,QAAQ,OAAO,GAAG;GAC1C,MAAM,OAAO,OAAO;GACpB,IAAI,CAAC,MAAM;GACX,KAAK,WAAW;GAChB,MAAM,IAAI,KAAK,WAAW,IAAI,KAAK,IAAI,KAAK,UAAU,KAAK,UAAU,CAAC,IAAI;GAC1E,KAAK,YAAY,QAAQ,KAAK,QAAQ,KAAK,KAAK,KAAK,QAAQ,CAAC;GAC9D,IAAI,KAAK,GAAG,OAAO,OAAO,KAAA;EAC5B;CACF;;CAGA,aAAuC;EACrC,OAAO,OAAO;GACZ,MAAM,SAAS,KAAK,QAAQ,IAAI,EAAE;GAClC,IAAI,QAAQ,KAAK,MAAM,MAAM;EAC/B;EACA,QAAQ,OAAO,KAAK,QAAQ,IAAI,EAAE,CAAC,EAAE,GAAG,MAAM;EAC9C,OAAO,OAAO;GACZ,MAAM,SAAS,KAAK,QAAQ,IAAI,EAAE;GAClC,IAAI,CAAC,QAAQ;GACb,OAAO,GAAG,MAAM;GAChB,IAAI,OAAO,OAAO,SAAS,UAAU,OAAO,GAAG,cAAc;EAC/D;EACA,YAAY,IAAI,QAAQ,SAAS;GAC/B,MAAM,SAAS,KAAK,QAAQ,IAAI,EAAE;GAClC,IAAI,CAAC,QAAQ;GACb,OAAO,SAAS;GAChB,KAAK,OAAO,QAAQ,QAAQ,MAAM,YAAY,CAAC;EACjD;EACA,UAAU,OAAO;GACf,MAAM,SAAS,KAAK,QAAQ,IAAI,EAAE;GAClC,OAAO,SAAS,CAAC,OAAO,GAAG,SAAS;EACtC;EACA,WAAW,UAAU;GACnB,KAAK,cAAc;GACnB,KAAK,SAAS,gBAAgB,QAAQ,IAAI,CAAC;EAC7C;EACA,aAAa,KAAK;CACpB;CAEA,UAAgB;EACd,OAAO,oBAAoB,eAAe,KAAK,MAAM;EACrD,OAAO,oBAAoB,WAAW,KAAK,MAAM;EACjD,KAAK,MAAM,UAAU,KAAK,QAAQ,OAAO,GAAG,KAAK,SAAS,MAAM;EAChE,KAAK,QAAQ,MAAM;EACnB,KAAK,OAAO,OAAO,KAAK,QAAQ;CAClC;AACF;;;;;;;;;AClOA,MAAa,eAA8C;CACzD,MAAM;CACN,QAAQ,QAAQ;EACd,MAAM,SAAS,IAAI,YAAY,IAAI,MAAM;EACzC,OAAO,UAAU,IAAI,MAAM,KAAK;EAChC,OAAO;GACL,SAAS,YAAY,OAAO,OAAO,OAAO;GAC1C,WAAW,UAAU,OAAO,UAAU,MAAM,KAAK;GACjD,eAAe,OAAO,QAAQ;GAC9B,KAAK,OAAO;EACd;CACF;AACF"}
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@scenography/plugin-audio",
3
+ "version": "0.2.0",
4
+ "description": "Audio plugin for scenography: spatial/ambient audio sources with ctx.media runtime control.",
5
+ "license": "MIT",
6
+ "files": [
7
+ "dist"
8
+ ],
9
+ "type": "module",
10
+ "main": "./dist/index.cjs",
11
+ "module": "./dist/index.mjs",
12
+ "types": "./dist/index.d.cts",
13
+ "exports": {
14
+ ".": {
15
+ "import": {
16
+ "types": "./dist/index.d.mts",
17
+ "default": "./dist/index.mjs"
18
+ },
19
+ "require": {
20
+ "types": "./dist/index.d.cts",
21
+ "default": "./dist/index.cjs"
22
+ }
23
+ },
24
+ "./data": {
25
+ "import": {
26
+ "types": "./dist/data.d.mts",
27
+ "default": "./dist/data.mjs"
28
+ },
29
+ "require": {
30
+ "types": "./dist/data.d.cts",
31
+ "default": "./dist/data.cjs"
32
+ }
33
+ }
34
+ },
35
+ "dependencies": {
36
+ "@scenography/scene": "0.2.0"
37
+ },
38
+ "devDependencies": {
39
+ "@types/three": "^0.160.0",
40
+ "three": "^0.160.0",
41
+ "@scenography/engine": "0.2.0"
42
+ },
43
+ "peerDependencies": {
44
+ "three": ">=0.160.0",
45
+ "@scenography/engine": "0.2.0"
46
+ },
47
+ "scripts": {
48
+ "build": "tsdown",
49
+ "dev": "tsdown --watch",
50
+ "typecheck": "tsc --noEmit"
51
+ }
52
+ }