@umicat/three-sdk 0.9.0 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/GameAudio.d.ts +20 -4
- package/dist/GameAudio.js +58 -21
- package/package.json +1 -1
package/dist/GameAudio.d.ts
CHANGED
|
@@ -25,6 +25,12 @@
|
|
|
25
25
|
* **4. iOS suspends the context when the app goes away** and does not bring it
|
|
26
26
|
* back, so sound works until the first phone call and then never again.
|
|
27
27
|
*
|
|
28
|
+
* Music is the one exception to rule 1, and for the same reason rule 1 exists.
|
|
29
|
+
* A three-minute track decodes to something like seventy megabytes of PCM;
|
|
30
|
+
* forty short effects as elements was fatal, but ONE element playing one long
|
|
31
|
+
* thing is exactly what elements are for. So effects are buffers and music
|
|
32
|
+
* streams, routed through the same graph so ducking still works.
|
|
33
|
+
*
|
|
28
34
|
* The game brings its own clips; nothing here knows what a game sounds like.
|
|
29
35
|
*/
|
|
30
36
|
export interface AudioClipSpec {
|
|
@@ -42,8 +48,9 @@ export interface GameAudioOptions {
|
|
|
42
48
|
base?: string;
|
|
43
49
|
/** File extension, including the dot. */
|
|
44
50
|
extension?: string;
|
|
45
|
-
/** A
|
|
46
|
-
*
|
|
51
|
+
/** A track to loop as music. Streamed, not decoded — see the note above.
|
|
52
|
+
* A name containing a dot is used as-is, so `'theme.mp3'` works alongside
|
|
53
|
+
* `.ogg` effects. */
|
|
47
54
|
music?: string;
|
|
48
55
|
musicVolume?: number;
|
|
49
56
|
}
|
|
@@ -51,13 +58,13 @@ export declare class GameAudio {
|
|
|
51
58
|
private ctx;
|
|
52
59
|
private master;
|
|
53
60
|
private musicGain;
|
|
54
|
-
private
|
|
61
|
+
private musicEl;
|
|
62
|
+
private musicName;
|
|
55
63
|
private readonly buffers;
|
|
56
64
|
private readonly lastPlayed;
|
|
57
65
|
private readonly clips;
|
|
58
66
|
private readonly base;
|
|
59
67
|
private readonly ext;
|
|
60
|
-
private readonly music?;
|
|
61
68
|
private readonly musicVolume;
|
|
62
69
|
private muted;
|
|
63
70
|
private readonly cleanups;
|
|
@@ -66,8 +73,17 @@ export declare class GameAudio {
|
|
|
66
73
|
* a cached flag is exactly the bug described above. */
|
|
67
74
|
get ready(): boolean;
|
|
68
75
|
private start;
|
|
76
|
+
/** `'theme.mp3'` stays as it is; `'coin'` becomes `coin.ogg`. */
|
|
77
|
+
private urlFor;
|
|
69
78
|
private loadAll;
|
|
70
79
|
private startMusic;
|
|
80
|
+
/**
|
|
81
|
+
* Change the track. Pass `null` for silence.
|
|
82
|
+
*
|
|
83
|
+
* Each scene gets its own: a lobby that sounds like the fight is a lobby you
|
|
84
|
+
* do not linger in.
|
|
85
|
+
*/
|
|
86
|
+
setMusic(name: string | null): void;
|
|
71
87
|
play(name: string): void;
|
|
72
88
|
/** Duck the music for a moment — for an ending that should be heard over it. */
|
|
73
89
|
duck(seconds?: number): void;
|
package/dist/GameAudio.js
CHANGED
|
@@ -25,6 +25,12 @@
|
|
|
25
25
|
* **4. iOS suspends the context when the app goes away** and does not bring it
|
|
26
26
|
* back, so sound works until the first phone call and then never again.
|
|
27
27
|
*
|
|
28
|
+
* Music is the one exception to rule 1, and for the same reason rule 1 exists.
|
|
29
|
+
* A three-minute track decodes to something like seventy megabytes of PCM;
|
|
30
|
+
* forty short effects as elements was fatal, but ONE element playing one long
|
|
31
|
+
* thing is exactly what elements are for. So effects are buffers and music
|
|
32
|
+
* streams, routed through the same graph so ducking still works.
|
|
33
|
+
*
|
|
28
34
|
* The game brings its own clips; nothing here knows what a game sounds like.
|
|
29
35
|
*/
|
|
30
36
|
export class GameAudio {
|
|
@@ -32,7 +38,8 @@ export class GameAudio {
|
|
|
32
38
|
this.ctx = null;
|
|
33
39
|
this.master = null;
|
|
34
40
|
this.musicGain = null;
|
|
35
|
-
this.
|
|
41
|
+
this.musicEl = null;
|
|
42
|
+
this.musicName = null;
|
|
36
43
|
this.buffers = new Map();
|
|
37
44
|
this.lastPlayed = new Map();
|
|
38
45
|
this.muted = false;
|
|
@@ -40,7 +47,7 @@ export class GameAudio {
|
|
|
40
47
|
this.clips = opts.clips;
|
|
41
48
|
this.base = opts.base ?? 'audio/';
|
|
42
49
|
this.ext = opts.extension ?? '.ogg';
|
|
43
|
-
this.
|
|
50
|
+
this.musicName = opts.music ?? null;
|
|
44
51
|
this.musicVolume = opts.musicVolume ?? 0.28;
|
|
45
52
|
const unlock = () => {
|
|
46
53
|
void this.start().then(() => {
|
|
@@ -92,42 +99,68 @@ export class GameAudio {
|
|
|
92
99
|
if (this.ready)
|
|
93
100
|
this.startMusic();
|
|
94
101
|
}
|
|
102
|
+
/** `'theme.mp3'` stays as it is; `'coin'` becomes `coin.ogg`. */
|
|
103
|
+
urlFor(name) {
|
|
104
|
+
return `${this.base}${name}${name.includes('.') ? '' : this.ext}`;
|
|
105
|
+
}
|
|
95
106
|
async loadAll() {
|
|
96
107
|
const load = async (name) => {
|
|
97
108
|
try {
|
|
98
|
-
const res = await fetch(
|
|
109
|
+
const res = await fetch(this.urlFor(name));
|
|
99
110
|
const bytes = await res.arrayBuffer();
|
|
100
111
|
const buf = await this.ctx.decodeAudioData(bytes);
|
|
101
112
|
// Tagged so a test can see WHICH clip played — a buffer has no name,
|
|
102
113
|
// and "some audio happened" is not a check.
|
|
103
114
|
buf.__name = name;
|
|
104
115
|
this.buffers.set(name, buf);
|
|
105
|
-
if (name === this.music && this.ready && !this.musicSource)
|
|
106
|
-
this.startMusic();
|
|
107
116
|
}
|
|
108
117
|
catch {
|
|
109
118
|
/* a clip that will not decode is not worth taking the game down for */
|
|
110
119
|
}
|
|
111
120
|
};
|
|
112
|
-
//
|
|
113
|
-
// behind every effect and left the first actions silent — caution about
|
|
121
|
+
// All at once. One at a time left the first actions silent — caution about
|
|
114
122
|
// decode cost, for work that is not on the main thread.
|
|
115
|
-
if (this.music)
|
|
116
|
-
await load(this.music);
|
|
117
123
|
await Promise.all(Object.keys(this.clips).map(load));
|
|
118
124
|
}
|
|
119
125
|
startMusic() {
|
|
120
|
-
if (!this.ctx || !this.musicGain || this.
|
|
126
|
+
if (!this.ctx || !this.musicGain || this.muted || !this.musicName)
|
|
121
127
|
return;
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
128
|
+
if (!this.musicEl) {
|
|
129
|
+
const el = new Audio(this.urlFor(this.musicName));
|
|
130
|
+
el.loop = true;
|
|
131
|
+
el.preload = 'auto';
|
|
132
|
+
el.crossOrigin = 'anonymous';
|
|
133
|
+
// Routed through the graph, not played on its own, so `duck()` and the
|
|
134
|
+
// master gain reach it like everything else.
|
|
135
|
+
try {
|
|
136
|
+
this.ctx.createMediaElementSource(el).connect(this.musicGain);
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
// Some engines refuse to route an element they consider tainted. Fall
|
|
140
|
+
// back to playing it directly rather than losing the music entirely.
|
|
141
|
+
el.volume = this.musicVolume;
|
|
142
|
+
}
|
|
143
|
+
this.musicEl = el;
|
|
144
|
+
}
|
|
145
|
+
void this.musicEl.play().catch(() => { });
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Change the track. Pass `null` for silence.
|
|
149
|
+
*
|
|
150
|
+
* Each scene gets its own: a lobby that sounds like the fight is a lobby you
|
|
151
|
+
* do not linger in.
|
|
152
|
+
*/
|
|
153
|
+
setMusic(name) {
|
|
154
|
+
if (name === this.musicName)
|
|
155
|
+
return;
|
|
156
|
+
this.musicName = name;
|
|
157
|
+
if (this.musicEl) {
|
|
158
|
+
this.musicEl.pause();
|
|
159
|
+
this.musicEl.src = '';
|
|
160
|
+
this.musicEl = null; // a new element: the old one's graph node is spent
|
|
161
|
+
}
|
|
162
|
+
if (name && this.ready && !this.muted)
|
|
163
|
+
this.startMusic();
|
|
131
164
|
}
|
|
132
165
|
play(name) {
|
|
133
166
|
const ctx = this.ctx;
|
|
@@ -168,7 +201,11 @@ export class GameAudio {
|
|
|
168
201
|
if (this.master && this.ctx) {
|
|
169
202
|
this.master.gain.setTargetAtTime(on ? 0 : 1, this.ctx.currentTime, 0.02);
|
|
170
203
|
}
|
|
171
|
-
|
|
204
|
+
// Pause the stream as well as silencing it: a muted track still costs a
|
|
205
|
+
// decoder and a download for something nobody can hear.
|
|
206
|
+
if (on)
|
|
207
|
+
this.musicEl?.pause();
|
|
208
|
+
else if (this.ready)
|
|
172
209
|
this.startMusic();
|
|
173
210
|
}
|
|
174
211
|
get isMuted() { return this.muted; }
|
|
@@ -176,7 +213,7 @@ export class GameAudio {
|
|
|
176
213
|
for (const c of this.cleanups)
|
|
177
214
|
c();
|
|
178
215
|
this.cleanups.length = 0;
|
|
179
|
-
this.
|
|
216
|
+
this.musicEl?.pause();
|
|
180
217
|
void this.ctx?.close();
|
|
181
218
|
this.ctx = null;
|
|
182
219
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umicat/three-sdk",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.1",
|
|
4
4
|
"description": "Three.js runtime for Umicat games: the scene3d design format, its loader with physics, a kinematic character controller, and the Umicat platform via @umicat/platform-sdk.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|