narraleaf-react 0.21.1 → 0.23.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/game/nlcore/common/game.d.ts +4 -2
- package/dist/game/nlcore/common/types.d.ts +2 -1
- package/dist/game/nlcore/elements/sound.d.ts +61 -6
- package/dist/game/nlcore/game/audioBus.d.ts +268 -0
- package/dist/game/nlcore/game/liveGame.d.ts +8 -0
- package/dist/game/nlcore/game/preference.d.ts +10 -1
- package/dist/game/nlcore/game.d.ts +32 -0
- package/dist/game/nlcore/gameTypes.d.ts +41 -0
- package/dist/game/player/elements/player/PreferenceUpdateAnnouncer.d.ts +8 -0
- package/dist/game/player/elements/scene/backgroundMusic.d.ts +1 -0
- package/dist/game/player/lib/AudioManager.d.ts +207 -6
- package/dist/main.js +42 -43
- package/package.json +1 -1
|
@@ -1,34 +1,141 @@
|
|
|
1
|
-
import { Sound as SoundElement,
|
|
1
|
+
import { Sound as SoundElement, SoundBusId } from "../../nlcore/elements/sound";
|
|
2
2
|
import { SoundToken } from "@narraleaf/sound";
|
|
3
3
|
import { FadeOptions } from "../../nlcore/elements/type";
|
|
4
4
|
import { Awaitable } from "../../../util/data";
|
|
5
5
|
import { GameState } from "../gameState";
|
|
6
6
|
import { LogicAction } from "../../nlcore/action/logicAction";
|
|
7
|
+
import { AudioBusState } from "../../nlcore/game/audioBus";
|
|
7
8
|
export type AudioDataRaw = {
|
|
8
9
|
isPlaying: boolean;
|
|
9
10
|
position: number;
|
|
10
11
|
};
|
|
11
12
|
export type AudioManagerDataRaw = {
|
|
12
13
|
sounds: [string, AudioDataRaw][];
|
|
13
|
-
|
|
14
|
+
/**
|
|
15
|
+
* Bus volumes, keyed by bus id.
|
|
16
|
+
*
|
|
17
|
+
* The name is historical - these used to be the three fixed "groups". A save written before
|
|
18
|
+
* buses existed carries exactly `bgm`/`sound`/`voice`, which are still buses, so it restores
|
|
19
|
+
* unchanged.
|
|
20
|
+
*/
|
|
21
|
+
groups: [string, number][];
|
|
14
22
|
};
|
|
15
23
|
export declare class AudioManager {
|
|
16
24
|
private gameState;
|
|
25
|
+
/**
|
|
26
|
+
* How much of the backend's channel budget to ask for.
|
|
27
|
+
*
|
|
28
|
+
* `@narraleaf/sound` defaults to 128 *including* the master, and throws outright when a
|
|
29
|
+
* `createChannel` would cross it. Three buses never came close; a game that gives every member
|
|
30
|
+
* of a large voiced cast its own bus can, and the failure mode is a hard throw at boot. This is
|
|
31
|
+
* a per-channel `GainNode` and nothing else, so a generous ceiling costs effectively nothing.
|
|
32
|
+
*/
|
|
33
|
+
private static readonly MaxChannels;
|
|
34
|
+
/**
|
|
35
|
+
* Time constant of the bus-volume ramp, in seconds. ~20ms: long enough to turn a step into a
|
|
36
|
+
* slew nobody hears, short enough that a slider still feels attached to the sound.
|
|
37
|
+
*/
|
|
38
|
+
private static readonly BusRampTimeConstant;
|
|
39
|
+
/**
|
|
40
|
+
* When to pin the exact target after the ramp. `setTargetAtTime` approaches asymptotically and
|
|
41
|
+
* never lands, so five time constants in (within 0.7% - inaudible) the value is written
|
|
42
|
+
* outright. Without this a long drag would accumulate a drift the mixer's own bookkeeping does
|
|
43
|
+
* not have.
|
|
44
|
+
*/
|
|
45
|
+
private static readonly BusRampSettle;
|
|
17
46
|
private state;
|
|
18
47
|
private channels;
|
|
19
|
-
private
|
|
48
|
+
private busTree;
|
|
49
|
+
private busSubscription;
|
|
50
|
+
private unknownBuses;
|
|
20
51
|
private globalVolume;
|
|
21
52
|
private sound;
|
|
22
53
|
private ready;
|
|
23
54
|
private isReady;
|
|
24
55
|
private isInitializing;
|
|
25
56
|
constructor(gameState: GameState);
|
|
57
|
+
/**
|
|
58
|
+
* The volume of every bus, and the tree they are wired into.
|
|
59
|
+
*
|
|
60
|
+
* It lives on `Game`, not here: a bus volume is a player setting that exists before the audio
|
|
61
|
+
* context unlocks and outlives any one player mount. This manager is the thing that makes it
|
|
62
|
+
* audible, not the thing that remembers it.
|
|
63
|
+
*/
|
|
64
|
+
private get mixer();
|
|
26
65
|
/**
|
|
27
66
|
* Must be called ONCE on the client side to prepare audio subsystem.
|
|
28
67
|
* Doing it here avoids "AudioContext is not defined" on the server.
|
|
29
68
|
*/
|
|
30
69
|
initialize(): void;
|
|
70
|
+
/**
|
|
71
|
+
* Build the host's declared bus tree into real channels, once.
|
|
72
|
+
*
|
|
73
|
+
* Walked front to back the tree hands every bus out after its parent, so a child always has a
|
|
74
|
+
* live parent channel to be created from - that is what nests the gain nodes, and cascading
|
|
75
|
+
* gain then falls out of the audio graph rather than out of any arithmetic here.
|
|
76
|
+
*
|
|
77
|
+
* Done once, at boot, and never re-shaped: `Channel.remove()` stops every token in its subtree,
|
|
78
|
+
* so re-parenting a bus while the game runs would cut the music off. Volumes stay live.
|
|
79
|
+
*/
|
|
80
|
+
private realizeBusTree;
|
|
81
|
+
/**
|
|
82
|
+
* Write a bus's volume onto its gain node, ramping rather than stepping.
|
|
83
|
+
*
|
|
84
|
+
* `Channel.setVolume` assigns `gain.value` bare with no `cancelScheduledValues`, so a slider
|
|
85
|
+
* drag arrives as a staircase of discontinuities - the zipper. The backend exposes no ramping
|
|
86
|
+
* setter, only `getGainNode()`, so the ramp is driven from here.
|
|
87
|
+
*
|
|
88
|
+
* **Who owns the value.** `Channel.volume` remains authoritative bookkeeping and the
|
|
89
|
+
* `AudioParam` is authoritative for what is heard, and this method is the only writer of
|
|
90
|
+
* either, so the two can only disagree for the ~20ms a ramp is in flight. `channel.setVolume`
|
|
91
|
+
* is still called first and deliberately: it is what clamps to 0..1, what keeps
|
|
92
|
+
* `channel.getVolume()` truthful, and what `Channel.mute()`/`unmute()` re-read when they
|
|
93
|
+
* rewrite the gain themselves. Its bare write is then superseded in the same synchronous turn -
|
|
94
|
+
* `cancelScheduledValues` drops the implicit `setValueAtTime` that the assignment inserted at
|
|
95
|
+
* `currentTime`, and the ramp is scheduled from the value the parameter actually had. Nothing
|
|
96
|
+
* else in the engine touches a bus gain node, so there is no other automation to fight.
|
|
97
|
+
*
|
|
98
|
+
* Falls back to the plain assignment whenever the graph is not reachable - a backend without
|
|
99
|
+
* `getGainNode`, or a parameter without `setTargetAtTime`. Stepping is the behaviour that
|
|
100
|
+
* shipped; degrading to it is strictly no worse.
|
|
101
|
+
*/
|
|
102
|
+
private applyBusVolume;
|
|
103
|
+
/**
|
|
104
|
+
* The value a bus's gain parameter has right now, or `null` when the graph cannot be reached.
|
|
105
|
+
*/
|
|
106
|
+
private static readGain;
|
|
107
|
+
/**
|
|
108
|
+
* The channel a clip plays through.
|
|
109
|
+
*
|
|
110
|
+
* A bus id nothing declared is not fatal. Refusing to play would turn one typo in one clip's
|
|
111
|
+
* `type` into silence or a thrown action mid-scene; routing it to the always-seeded sfx bus
|
|
112
|
+
* keeps the game audible and says so once, per id, in the log. This is also where a bus name
|
|
113
|
+
* that {@link import("../../nlcore/game/audioBus").acceptsAudioBus} let through at story-build time is
|
|
114
|
+
* finally caught.
|
|
115
|
+
*/
|
|
116
|
+
private channelFor;
|
|
117
|
+
/**
|
|
118
|
+
* The volume a clip started with no explicit target should reach.
|
|
119
|
+
*
|
|
120
|
+
* Full volume was never a sensible default here: a `Sound` carries the volume it was configured
|
|
121
|
+
* with, so a caller that says nothing about volume is asking for *that*, not for 1. Reading
|
|
122
|
+
* `state` rather than the user config is deliberate - it is the same value {@link SoundElement.play}
|
|
123
|
+
* and {@link SoundElement.resume} put in their `FadeOptions`, so a clip replayed after
|
|
124
|
+
* {@link AudioManager.setVolume} comes back at the volume it was last set to instead of jumping
|
|
125
|
+
* back to whatever the author first wrote down.
|
|
126
|
+
*/
|
|
127
|
+
private static defaultFade;
|
|
31
128
|
play(sound: SoundElement, options?: FadeOptions): Awaitable<void>;
|
|
129
|
+
/**
|
|
130
|
+
* Start a clip and hand the token back once playback is under way.
|
|
131
|
+
*
|
|
132
|
+
* With no `duration` the target volume is written to the token *synchronously* before this
|
|
133
|
+
* returns, and nothing is left running: no gain automation is in flight when the caller gets the
|
|
134
|
+
* token. That is what makes an explicit `token.setVolume` or a fade the caller drives itself
|
|
135
|
+
* afterwards the last writer, which several hosts rely on. Defaulting `duration` to anything
|
|
136
|
+
* above 0 would break that - `token.fade` below is deliberately not awaited, so a non-zero
|
|
137
|
+
* default would leave a ramp running past the return and over whatever the caller did next.
|
|
138
|
+
*/
|
|
32
139
|
playSoundToken(sound: SoundElement, options?: FadeOptions): Promise<SoundToken>;
|
|
33
140
|
stop(sound: SoundElement, duration?: number): Awaitable<void>;
|
|
34
141
|
setVolume(sound: SoundElement, volume: number, duration?: number): Awaitable<void>;
|
|
@@ -42,13 +149,51 @@ export declare class AudioManager {
|
|
|
42
149
|
*/
|
|
43
150
|
seek(sound: SoundElement, time: number): Awaitable<void>;
|
|
44
151
|
/**
|
|
45
|
-
* The in/out points of a clip
|
|
152
|
+
* The in/out points of a clip.
|
|
46
153
|
*
|
|
47
154
|
* `endTime` is left off entirely when the author set none: passing `undefined` explicitly is the
|
|
48
155
|
* same thing to the backend, but omitting it keeps `{...region}` spreads from writing a key that
|
|
49
156
|
* reads as "there is a region here" to anything inspecting the object.
|
|
50
157
|
*/
|
|
51
158
|
private static clipRegionOf;
|
|
159
|
+
/**
|
|
160
|
+
* The region as the sound backend's play options.
|
|
161
|
+
*
|
|
162
|
+
* A looping clip deliberately hands over **no** `endTime`. The backend turns `endTime` into a
|
|
163
|
+
* timer that stops the token after one pass, whether or not the clip loops, so passing it here
|
|
164
|
+
* is what kept the loop region from ever repeating. The region reaches a looping clip through
|
|
165
|
+
* {@link AudioManager.applyLoopRegion} instead; for a one-shot `endTime` *is* the out point and
|
|
166
|
+
* the backend's timer is exactly the right mechanism.
|
|
167
|
+
*/
|
|
168
|
+
private static playRegionOf;
|
|
169
|
+
/**
|
|
170
|
+
* Write a looping clip's region onto the Web Audio node the backend is playing it through.
|
|
171
|
+
*
|
|
172
|
+
* **This is a shim against `@narraleaf/sound@0.1.0`'s internals, and the only place in this
|
|
173
|
+
* repo that reaches into them.** Two things in that version make the region unusable from the
|
|
174
|
+
* outside:
|
|
175
|
+
*
|
|
176
|
+
* - `SoundToken`'s constructor arms `setTimeout(stop, duration * 1000)` whenever a duration was
|
|
177
|
+
* given, without consulting `loop` — so a looping clip with an out point hard-stops after its
|
|
178
|
+
* first pass through the region.
|
|
179
|
+
* - `Sound.createToken` pins `loopStart` to the playback start offset, so "play the intro from
|
|
180
|
+
* 0s, then repeat 12s→90s forever" cannot be expressed at all.
|
|
181
|
+
*
|
|
182
|
+
* The fix belongs upstream and is small: honour `loop` before arming the duration timer, and
|
|
183
|
+
* accept an independent `loopStart` in `PlayOptions`. Until that ships, this manager withholds
|
|
184
|
+
* `endTime` from a looping clip's play options (which is what disarms the timer) and sets the
|
|
185
|
+
* loop region here.
|
|
186
|
+
*
|
|
187
|
+
* `SoundToken.sourceController` is TypeScript-`private` while `AudioSourceController.getSource`
|
|
188
|
+
* is public, so the node is reachable at runtime but not through the types — hence the cast and
|
|
189
|
+
* the shape check. If a later backend changes that shape this returns silently and the clip
|
|
190
|
+
* degrades to the behaviour it has today: the region plays once and the clip stops.
|
|
191
|
+
*
|
|
192
|
+
* Seeking survives this. `SoundToken.seek` rebuilds the buffer source and copies `loop`,
|
|
193
|
+
* `loopStart` and `loopEnd` off the old node onto the new one, so a jump inside a looping track
|
|
194
|
+
* keeps the region.
|
|
195
|
+
*/
|
|
196
|
+
private static applyLoopRegion;
|
|
52
197
|
private static clampToRegion;
|
|
53
198
|
setRate(sound: SoundElement, rate: number): Awaitable<void>;
|
|
54
199
|
getPosition(sound: SoundElement): number;
|
|
@@ -70,11 +215,67 @@ export declare class AudioManager {
|
|
|
70
215
|
* as it did before.
|
|
71
216
|
*/
|
|
72
217
|
preload(sound: SoundElement): Promise<void>;
|
|
218
|
+
/**
|
|
219
|
+
* Start a new game: stop everything and put the mixer back on the wire.
|
|
220
|
+
*
|
|
221
|
+
* The tree itself is **not** rebuilt - the channels are the same channels, because a bus is
|
|
222
|
+
* part of the game's declared shape, not part of its state. What is re-applied is every bus's
|
|
223
|
+
* current volume, read back off the mixer rather than off `SoundType`, which is what lets a
|
|
224
|
+
* host's own buses exist here at all.
|
|
225
|
+
*
|
|
226
|
+
* Note the seeded three are then immediately overwritten from the preferences, exactly as
|
|
227
|
+
* before; a bus the host declared keeps whatever volume the player left it at, because that is
|
|
228
|
+
* a setting and not something a new game should undo.
|
|
229
|
+
*/
|
|
73
230
|
reset(): void;
|
|
74
|
-
|
|
231
|
+
/**
|
|
232
|
+
* Set **the player's** volume for a bus, 0..1, live. 1 means "leave the author's mix alone".
|
|
233
|
+
*
|
|
234
|
+
* Reaches sounds that are **already playing**: a bus is a gain node every clip beneath it is
|
|
235
|
+
* routed through, so nothing has to be found, stopped or restarted for the change to be heard.
|
|
236
|
+
* Setting a bus that has not been realized yet is fine - the value is kept and applied when the
|
|
237
|
+
* audio context unlocks.
|
|
238
|
+
*
|
|
239
|
+
* Equivalent to `game.audioBuses.setVolume(...)`, which is the surface a host should prefer:
|
|
240
|
+
* it exists before the player mounts.
|
|
241
|
+
*/
|
|
242
|
+
setBusVolume(id: SoundBusId, volume: number): void;
|
|
243
|
+
/**
|
|
244
|
+
* The player's volume for a bus - what was last set, else 1. Not the author's declared mix
|
|
245
|
+
* (`game.audioBuses.getDeclaredVolume`) and not what is on the gain node
|
|
246
|
+
* (`getEffectiveVolume`).
|
|
247
|
+
*/
|
|
248
|
+
getBusVolume(id: SoundBusId): number;
|
|
249
|
+
/**
|
|
250
|
+
* Every bus with its parent and both of its volumes, parents first. `volume` is the half a
|
|
251
|
+
* host persists.
|
|
252
|
+
*/
|
|
253
|
+
getBuses(): AudioBusState[];
|
|
254
|
+
/**
|
|
255
|
+
* @deprecated Use {@link AudioManager.setBusVolume}. Kept because the three sound types are
|
|
256
|
+
* still bus ids and hosts call this with them.
|
|
257
|
+
*/
|
|
258
|
+
setGroupVolume(type: SoundBusId, volume: number): void;
|
|
259
|
+
/**
|
|
260
|
+
* @deprecated Use {@link AudioManager.getBusVolume}.
|
|
261
|
+
*/
|
|
262
|
+
getGroupVolume(type: SoundBusId): number;
|
|
75
263
|
setGlobalVolume(volume: number): void;
|
|
76
264
|
getGlobalVolume(): number;
|
|
77
|
-
getGroupVolume(type: SoundType): number;
|
|
78
265
|
destroy(): void;
|
|
266
|
+
/**
|
|
267
|
+
* The three volume preferences are aliases onto the three seeded buses, and this is the alias.
|
|
268
|
+
*
|
|
269
|
+
* They stay the way a player's music/sfx/voice sliders are driven - widening the preference key
|
|
270
|
+
* union to cover arbitrary bus ids is not possible without reshaping `GamePreference`, and is
|
|
271
|
+
* not needed: a host bus is driven through {@link AudioManager.setBusVolume} instead.
|
|
272
|
+
*
|
|
273
|
+
* They write the **player's** half only. This used to write the bus's whole gain, and because
|
|
274
|
+
* these preferences default to 1 it meant that a host declaring `{id: "sound", volume: 0.6}`
|
|
275
|
+
* had its mix silently overwritten with 1 the moment the audio subsystem started - the author
|
|
276
|
+
* set SFX to 60% and every player heard 100%. Custom buses were unaffected because nothing
|
|
277
|
+
* aliases them, so the three buses every existing project uses were the only ones that ignored
|
|
278
|
+
* the declaration. Now `soundVolume: 1` means "do not attenuate further" and 0.6 survives.
|
|
279
|
+
*/
|
|
79
280
|
private setupGroupVolume;
|
|
80
281
|
}
|