narraleaf-react 0.22.0 → 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.
@@ -1,33 +1,119 @@
1
- import { Sound as SoundElement, SoundType } from "../../nlcore/elements/sound";
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
- groups: [SoundType, number][];
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 channelVolumes;
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;
31
117
  /**
32
118
  * The volume a clip started with no explicit target should reach.
33
119
  *
@@ -129,11 +215,67 @@ export declare class AudioManager {
129
215
  * as it did before.
130
216
  */
131
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
+ */
132
230
  reset(): void;
133
- setGroupVolume(type: SoundType, volume: number): void;
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;
134
263
  setGlobalVolume(volume: number): void;
135
264
  getGlobalVolume(): number;
136
- getGroupVolume(type: SoundType): number;
137
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
+ */
138
280
  private setupGroupVolume;
139
281
  }