@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.
- package/dist/data-ChoGUA4U.mjs +27 -0
- package/dist/data-ChoGUA4U.mjs.map +1 -0
- package/dist/data-vyKDnIYf.cjs +38 -0
- package/dist/data-vyKDnIYf.cjs.map +1 -0
- package/dist/data.cjs +4 -0
- package/dist/data.d.cts +2 -0
- package/dist/data.d.mts +2 -0
- package/dist/data.mjs +2 -0
- package/dist/index-CR9ApbPg.d.cts +48 -0
- package/dist/index-CR9ApbPg.d.mts +48 -0
- package/dist/index.cjs +222 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +50 -0
- package/dist/index.d.mts +50 -0
- package/dist/index.mjs +218 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +52 -0
|
@@ -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
package/dist/data.d.cts
ADDED
package/dist/data.d.mts
ADDED
package/dist/data.mjs
ADDED
|
@@ -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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"}
|
package/dist/index.d.cts
ADDED
|
@@ -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
|
package/dist/index.d.mts
ADDED
|
@@ -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":"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"}
|
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
|
+
}
|