narraleaf-react 0.21.0 → 0.22.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.
@@ -44,7 +44,8 @@ export interface ISoundUserConfig {
44
44
  *
45
45
  * When {@link ISoundUserConfig.loop} is set together with {@link ISoundUserConfig.endTime},
46
46
  * this is also where each repeat restarts from, so the two together describe a loop region
47
- * rather than just a starting offset.
47
+ * rather than just a starting offset — unless {@link ISoundUserConfig.loopStart} moves the
48
+ * repeat's in point somewhere else.
48
49
  * @default 0
49
50
  */
50
51
  seek: number;
@@ -53,15 +54,35 @@ export interface ISoundUserConfig {
53
54
  * through to the end of the file.
54
55
  *
55
56
  * Without `loop` the clip simply stops there. With `loop` it jumps back to
56
- * {@link ISoundUserConfig.seek}, which is how a piece of background music with an intro loops
57
- * only its body. The jump is sample-accurate (it is the Web Audio node's own loop), so there is
58
- * no gap and no drift over long sessions.
57
+ * {@link ISoundUserConfig.loopStart}, or to {@link ISoundUserConfig.seek} when no loop in point
58
+ * was given which is how a piece of background music with an intro loops only its body. The
59
+ * jump is sample-accurate (it is the Web Audio node's own loop), so there is no gap and no
60
+ * drift over long sessions.
59
61
  *
60
62
  * Ignored for a clip that is streamed rather than decoded: an `<audio>` element has no loop
61
63
  * region, only a plain repeat.
62
64
  * @default undefined
63
65
  */
64
66
  endTime?: number;
67
+ /**
68
+ * Position in seconds the clip returns to on every repeat — the **loop in point**.
69
+ *
70
+ * Only meaningful together with `loop` and {@link ISoundUserConfig.endTime}: it is the start of
71
+ * the region that repeats, while {@link ISoundUserConfig.seek} stays the position the *first*
72
+ * pass begins at. Leaving it out makes each repeat return to `seek`, which is the behaviour of
73
+ * a clip that has no separate intro.
74
+ *
75
+ * Separating the two is what expresses the standard "intro then loop" piece of background
76
+ * music — play from the top once, then repeat only the body forever:
77
+ *
78
+ * ```ts
79
+ * Sound.bgm({src: "theme.mp3", loop: true, seek: 0, loopStart: 12, endTime: 90});
80
+ * ```
81
+ *
82
+ * A value outside `[seek, endTime)` describes no playable region and falls back to `seek`.
83
+ * @default undefined
84
+ */
85
+ loopStart?: number;
65
86
  /**
66
87
  * The type of the sound
67
88
  * @default SoundType.Sound
@@ -97,6 +118,14 @@ export declare class Sound extends Actionable<SoundDataRaw, Sound> {
97
118
  constructor(arg0: Partial<ISoundUserConfig> | string);
98
119
  /**
99
120
  * Start playing the sound and wait for it to finish.
121
+ *
122
+ * A clip of any {@link SoundType} may be played this way. `type` selects which volume slider
123
+ * governs the clip and nothing else, so putting an ambience track on `bgm` so the player's music
124
+ * slider controls it, and then playing it from an ordinary line, is a legitimate thing to want.
125
+ *
126
+ * It is *not* the same as {@link Scene.setBackgroundMusic}: a clip played here is not in the
127
+ * scene's background-music slot, so leaving the scene will not stop it and no cross-fade is
128
+ * arranged for it. That is true of every clip played this way, on any bus.
100
129
  * @param duration - Optional fade duration in milliseconds.
101
130
  * @chainable
102
131
  * @example
@@ -128,6 +128,14 @@ export declare class LiveGame {
128
128
  notify(message: string, duration?: number | null): NotificationToken;
129
129
  /**
130
130
  * Play a sound immediately and return the SoundToken.
131
+ *
132
+ * The clip starts at the volume its {@link Sound} was configured with — `Sound.voice({src, volume:
133
+ * 0.4})` starts at 0.4, not at full volume. There is no fade: the token's volume is already
134
+ * settled when this resolves and no ramp is left running, so a `setVolume` or a fade driven on
135
+ * the returned token afterwards wins outright.
136
+ *
137
+ * A source given as a string or `URL` becomes a default `Sound`, which is full volume — pass a
138
+ * `Sound` to say otherwise.
131
139
  */
132
140
  playSound(sound: Sound | string | URL): Promise<SoundToken>;
133
141
  /**
@@ -28,7 +28,28 @@ export declare class AudioManager {
28
28
  * Doing it here avoids "AudioContext is not defined" on the server.
29
29
  */
30
30
  initialize(): void;
31
+ /**
32
+ * The volume a clip started with no explicit target should reach.
33
+ *
34
+ * Full volume was never a sensible default here: a `Sound` carries the volume it was configured
35
+ * with, so a caller that says nothing about volume is asking for *that*, not for 1. Reading
36
+ * `state` rather than the user config is deliberate - it is the same value {@link SoundElement.play}
37
+ * and {@link SoundElement.resume} put in their `FadeOptions`, so a clip replayed after
38
+ * {@link AudioManager.setVolume} comes back at the volume it was last set to instead of jumping
39
+ * back to whatever the author first wrote down.
40
+ */
41
+ private static defaultFade;
31
42
  play(sound: SoundElement, options?: FadeOptions): Awaitable<void>;
43
+ /**
44
+ * Start a clip and hand the token back once playback is under way.
45
+ *
46
+ * With no `duration` the target volume is written to the token *synchronously* before this
47
+ * returns, and nothing is left running: no gain automation is in flight when the caller gets the
48
+ * token. That is what makes an explicit `token.setVolume` or a fade the caller drives itself
49
+ * afterwards the last writer, which several hosts rely on. Defaulting `duration` to anything
50
+ * above 0 would break that - `token.fade` below is deliberately not awaited, so a non-zero
51
+ * default would leave a ramp running past the return and over whatever the caller did next.
52
+ */
32
53
  playSoundToken(sound: SoundElement, options?: FadeOptions): Promise<SoundToken>;
33
54
  stop(sound: SoundElement, duration?: number): Awaitable<void>;
34
55
  setVolume(sound: SoundElement, volume: number, duration?: number): Awaitable<void>;
@@ -42,13 +63,51 @@ export declare class AudioManager {
42
63
  */
43
64
  seek(sound: SoundElement, time: number): Awaitable<void>;
44
65
  /**
45
- * The in/out points of a clip as the sound backend's play options.
66
+ * The in/out points of a clip.
46
67
  *
47
68
  * `endTime` is left off entirely when the author set none: passing `undefined` explicitly is the
48
69
  * same thing to the backend, but omitting it keeps `{...region}` spreads from writing a key that
49
70
  * reads as "there is a region here" to anything inspecting the object.
50
71
  */
51
72
  private static clipRegionOf;
73
+ /**
74
+ * The region as the sound backend's play options.
75
+ *
76
+ * A looping clip deliberately hands over **no** `endTime`. The backend turns `endTime` into a
77
+ * timer that stops the token after one pass, whether or not the clip loops, so passing it here
78
+ * is what kept the loop region from ever repeating. The region reaches a looping clip through
79
+ * {@link AudioManager.applyLoopRegion} instead; for a one-shot `endTime` *is* the out point and
80
+ * the backend's timer is exactly the right mechanism.
81
+ */
82
+ private static playRegionOf;
83
+ /**
84
+ * Write a looping clip's region onto the Web Audio node the backend is playing it through.
85
+ *
86
+ * **This is a shim against `@narraleaf/sound@0.1.0`'s internals, and the only place in this
87
+ * repo that reaches into them.** Two things in that version make the region unusable from the
88
+ * outside:
89
+ *
90
+ * - `SoundToken`'s constructor arms `setTimeout(stop, duration * 1000)` whenever a duration was
91
+ * given, without consulting `loop` — so a looping clip with an out point hard-stops after its
92
+ * first pass through the region.
93
+ * - `Sound.createToken` pins `loopStart` to the playback start offset, so "play the intro from
94
+ * 0s, then repeat 12s→90s forever" cannot be expressed at all.
95
+ *
96
+ * The fix belongs upstream and is small: honour `loop` before arming the duration timer, and
97
+ * accept an independent `loopStart` in `PlayOptions`. Until that ships, this manager withholds
98
+ * `endTime` from a looping clip's play options (which is what disarms the timer) and sets the
99
+ * loop region here.
100
+ *
101
+ * `SoundToken.sourceController` is TypeScript-`private` while `AudioSourceController.getSource`
102
+ * is public, so the node is reachable at runtime but not through the types — hence the cast and
103
+ * the shape check. If a later backend changes that shape this returns silently and the clip
104
+ * degrades to the behaviour it has today: the region plays once and the clip stops.
105
+ *
106
+ * Seeking survives this. `SoundToken.seek` rebuilds the buffer source and copies `loop`,
107
+ * `loopStart` and `loopEnd` off the old node onto the new one, so a jump inside a looping track
108
+ * keeps the region.
109
+ */
110
+ private static applyLoopRegion;
52
111
  private static clampToRegion;
53
112
  setRate(sound: SoundElement, rate: number): Awaitable<void>;
54
113
  getPosition(sound: SoundElement): number;