@flighthq/media 0.3.0-next.906.07cea63 → 0.3.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/audioMixer.d.ts +2 -1
- package/dist/audioMixer.d.ts.map +1 -1
- package/dist/audioMixer.js +56 -19
- package/dist/audioMixer.js.map +1 -1
- package/dist/contract.d.ts +1 -0
- package/dist/contract.d.ts.map +1 -1
- package/dist/contract.js +1 -0
- package/dist/contract.js.map +1 -1
- package/dist/enableAudioMixerGuards.d.ts +3 -0
- package/dist/enableAudioMixerGuards.d.ts.map +1 -0
- package/dist/enableAudioMixerGuards.js +27 -0
- package/dist/enableAudioMixerGuards.js.map +1 -0
- package/package.json +8 -5
- package/src/audioMixer.test.ts +117 -1
- package/src/enableAudioMixerGuards.test.ts +84 -0
- package/src/videoChannel.test.ts +2 -2
package/dist/audioMixer.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AudioBus, AudioBusOptions, AudioChannel, AudioMixer, AudioMixerOptions } from '@flighthq/types/contract';
|
|
1
|
+
import type { AudioBus, AudioBusMixerGuard, AudioBusOptions, AudioChannel, AudioMixer, AudioMixerOptions } from '@flighthq/types/contract';
|
|
2
2
|
export declare function addAudioBusToMixer(mixer: Readonly<AudioMixer>, bus: AudioBus): void;
|
|
3
3
|
export declare function createAudioBus(options?: Readonly<AudioBusOptions>): AudioBus;
|
|
4
4
|
export declare function createAudioMixer(context: AudioContext, options?: Readonly<AudioMixerOptions>): AudioMixer;
|
|
@@ -9,6 +9,7 @@ export declare function pauseAllAudioMixerChannels(mixer: Readonly<AudioMixer>):
|
|
|
9
9
|
export declare function resumeAllAudioMixerChannels(mixer: Readonly<AudioMixer>): void;
|
|
10
10
|
export declare function routeAudioChannelToMixerBus(mixer: Readonly<AudioMixer>, channel: AudioChannel, bus: AudioBus): void;
|
|
11
11
|
export declare function setAudioBusGain(bus: AudioBus, value: number): number;
|
|
12
|
+
export declare function setAudioBusMixerGuard(guard: AudioBusMixerGuard | null): void;
|
|
12
13
|
export declare function setAudioBusMuted(bus: AudioBus, muted: boolean): boolean;
|
|
13
14
|
export declare function setAudioBusPan(bus: AudioBus, value: number): number;
|
|
14
15
|
export declare function setAudioMixerMasterGain(mixer: AudioMixer, value: number): number;
|
package/dist/audioMixer.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"audioMixer.d.ts","sourceRoot":"","sources":["../src/audioMixer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"audioMixer.d.ts","sourceRoot":"","sources":["../src/audioMixer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,QAAQ,EACR,kBAAkB,EAElB,eAAe,EACf,YAAY,EACZ,UAAU,EACV,iBAAiB,EAClB,MAAM,0BAA0B,CAAC;AAIlC,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,QAAQ,CAAC,UAAU,CAAC,EAAE,GAAG,EAAE,QAAQ,GAAG,IAAI,CAmBnF;AAED,wBAAgB,cAAc,CAAC,OAAO,CAAC,EAAE,QAAQ,CAAC,eAAe,CAAC,GAAG,QAAQ,CAO5E;AAED,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,YAAY,EAAE,OAAO,CAAC,EAAE,QAAQ,CAAC,iBAAiB,CAAC,GAAG,UAAU,CAmBzG;AAED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,QAAQ,CAAC,UAAU,CAAC,GAAG,IAAI,CAenE;AAED,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,QAAQ,CAAC,UAAU,CAAC,EAC3B,GAAG,EAAE,QAAQ,EACb,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,GACjB,IAAI,CAYN;AAED,wBAAgB,2BAA2B,CAAC,KAAK,EAAE,QAAQ,CAAC,UAAU,CAAC,GAAG,SAAS,YAAY,EAAE,CAIhG;AAED,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,QAAQ,CAAC,UAAU,CAAC,GAAG,IAAI,CAU5E;AAED,wBAAgB,2BAA2B,CAAC,KAAK,EAAE,QAAQ,CAAC,UAAU,CAAC,GAAG,IAAI,CAY7E;AAED,wBAAgB,2BAA2B,CAAC,KAAK,EAAE,QAAQ,CAAC,UAAU,CAAC,EAAE,OAAO,EAAE,YAAY,EAAE,GAAG,EAAE,QAAQ,GAAG,IAAI,CAYnH;AASD,wBAAgB,eAAe,CAAC,GAAG,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAKpE;AAMD,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,kBAAkB,GAAG,IAAI,GAAG,IAAI,CAE5E;AAGD,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,QAAQ,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,CAKvE;AAGD,wBAAgB,cAAc,CAAC,GAAG,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAKnE;AAED,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAOhF;AAED,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,UAAU,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,CAOnF;AAED,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,QAAQ,CAAC,UAAU,CAAC,GAAG,IAAI,CAY3E;AA4CD,wBAAgB,+BAA+B,CAAC,KAAK,EAAE,QAAQ,CAAC,UAAU,CAAC,EAAE,OAAO,EAAE,YAAY,GAAG,IAAI,CAQxG"}
|
package/dist/audioMixer.js
CHANGED
|
@@ -41,6 +41,7 @@ export function createAudioMixer(context, options) {
|
|
|
41
41
|
};
|
|
42
42
|
mixerRuntimes.set(mixer, {
|
|
43
43
|
activeChannels: new Set(),
|
|
44
|
+
channelsPausedByMixer: new Set(),
|
|
44
45
|
buses: new Map(),
|
|
45
46
|
busGainNodes: new Map(),
|
|
46
47
|
busOutputNodes: new Map(),
|
|
@@ -94,22 +95,29 @@ export function pauseAllAudioMixerChannels(mixer) {
|
|
|
94
95
|
const runtime = mixerRuntimes.get(mixer);
|
|
95
96
|
if (runtime === undefined)
|
|
96
97
|
return;
|
|
97
|
-
// Actually stop each source node (not just flip the flag) so audio halts on pause.
|
|
98
|
+
// Actually stop each source node (not just flip the flag) so audio halts on pause. Only channels
|
|
99
|
+
// that were playing are recorded: one already paused by the caller is not this mixer's to resume.
|
|
98
100
|
for (const channel of runtime.activeChannels) {
|
|
99
|
-
if (channel.state
|
|
100
|
-
|
|
101
|
+
if (channel.state !== 'playing')
|
|
102
|
+
continue;
|
|
103
|
+
pauseAudioChannel(channel);
|
|
104
|
+
runtime.channelsPausedByMixer.add(channel);
|
|
101
105
|
}
|
|
102
106
|
}
|
|
103
107
|
export function resumeAllAudioMixerChannels(mixer) {
|
|
104
108
|
const runtime = mixerRuntimes.get(mixer);
|
|
105
109
|
if (runtime === undefined)
|
|
106
110
|
return;
|
|
107
|
-
// Restart the source node for channels this mixer paused; bus routing survives the restart
|
|
108
|
-
//
|
|
109
|
-
|
|
111
|
+
// Restart the source node for channels this mixer paused; bus routing survives the restart because
|
|
112
|
+
// the channel's destination node is preserved across stop/start. Resuming every paused channel
|
|
113
|
+
// instead would resurrect one the caller had deliberately paused on its own — a mixer-wide pause and
|
|
114
|
+
// resume around a menu would silently un-pause it. The state re-check covers a channel resumed or
|
|
115
|
+
// stopped individually while the mixer was paused.
|
|
116
|
+
for (const channel of runtime.channelsPausedByMixer) {
|
|
110
117
|
if (channel.state === 'paused')
|
|
111
118
|
resumeAudioChannel(channel);
|
|
112
119
|
}
|
|
120
|
+
runtime.channelsPausedByMixer.clear();
|
|
113
121
|
}
|
|
114
122
|
export function routeAudioChannelToMixerBus(mixer, channel, bus) {
|
|
115
123
|
const runtime = mixerRuntimes.get(mixer);
|
|
@@ -125,22 +133,37 @@ export function routeAudioChannelToMixerBus(mixer, channel, bus) {
|
|
|
125
133
|
connectAudioChannelToNode(channel, busGainNode);
|
|
126
134
|
}
|
|
127
135
|
}
|
|
136
|
+
// Sets the bus gain and pushes it to the gain node of every mixer holding this bus, found by reverse
|
|
137
|
+
// lookup through busToMixerRuntimes.
|
|
138
|
+
//
|
|
139
|
+
// A bus that belongs to NO mixer has no node to push to, so the new value is stored and returned while
|
|
140
|
+
// nothing becomes audible. That is a silent no-op the return value cannot express — it reports the value
|
|
141
|
+
// that was set, not whether anything is listening — so it routes through the guard seam instead of relying
|
|
142
|
+
// on a comment telling callers to add the bus first.
|
|
128
143
|
export function setAudioBusGain(bus, value) {
|
|
129
144
|
bus.gain = value;
|
|
130
|
-
|
|
131
|
-
// Since we store gain nodes per-bus inside each mixer runtime, iterate the known runtimes.
|
|
132
|
-
// The caller must have already added the bus to a mixer via addAudioBusToMixer or routeAudioChannelToMixerBus.
|
|
133
|
-
// We use a reverse lookup via busGainNodes — walk active mixers via the busGainNodes map on each runtime.
|
|
145
|
+
reportUnmixedBus(bus, 'gain');
|
|
134
146
|
updateBusGainNode(bus);
|
|
135
147
|
return bus.gain;
|
|
136
148
|
}
|
|
149
|
+
// The diagnostics seam for a bus-property write that cannot reach any audio node, not the caller-facing
|
|
150
|
+
// entry point — use enableAudioMixerGuards, which installs the @flighthq/log reporter through here. Null
|
|
151
|
+
// uninstalls it, and a null slot is the production default: the reverse-map lookup that detects the case
|
|
152
|
+
// runs only while a guard is installed.
|
|
153
|
+
export function setAudioBusMixerGuard(guard) {
|
|
154
|
+
_unmixedBusGuard = guard;
|
|
155
|
+
}
|
|
156
|
+
// Same unmixed-bus caveat as setAudioBusGain: muting a bus no mixer holds changes nothing audible.
|
|
137
157
|
export function setAudioBusMuted(bus, muted) {
|
|
138
158
|
bus.muted = muted;
|
|
159
|
+
reportUnmixedBus(bus, 'mute');
|
|
139
160
|
updateBusGainNode(bus);
|
|
140
161
|
return bus.muted;
|
|
141
162
|
}
|
|
163
|
+
// Same unmixed-bus caveat as setAudioBusGain: panning a bus no mixer holds changes nothing audible.
|
|
142
164
|
export function setAudioBusPan(bus, value) {
|
|
143
165
|
bus.pan = clamp(value, -1, 1);
|
|
166
|
+
reportUnmixedBus(bus, 'pan');
|
|
144
167
|
updateBusPannerNode(bus);
|
|
145
168
|
return bus.pan;
|
|
146
169
|
}
|
|
@@ -169,15 +192,10 @@ export function stopAllAudioMixerChannels(mixer) {
|
|
|
169
192
|
channel.currentTime = 0;
|
|
170
193
|
}
|
|
171
194
|
runtime.activeChannels.clear();
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
return;
|
|
177
|
-
runtime.activeChannels.delete(channel);
|
|
178
|
-
runtime.channelToBus.delete(channel);
|
|
179
|
-
// Reconnect the channel output to the context destination so it keeps playing if still active.
|
|
180
|
-
connectAudioChannelToNode(channel, runtime.context.destination);
|
|
195
|
+
// Bookkeeping, not a behavioural guard: resume already re-checks that a channel is still paused, so
|
|
196
|
+
// a stopped channel would be skipped anyway. Clearing keeps the record from retaining channels the
|
|
197
|
+
// mixer no longer owns for the rest of its life.
|
|
198
|
+
runtime.channelsPausedByMixer.clear();
|
|
181
199
|
}
|
|
182
200
|
const mixerRuntimes = new WeakMap();
|
|
183
201
|
// Reverse map from AudioBus to all mixer runtimes that contain it.
|
|
@@ -205,6 +223,25 @@ function unregisterBusFromReverseMap(bus, runtime) {
|
|
|
205
223
|
if (runtimes.size === 0)
|
|
206
224
|
busToMixerRuntimes.delete(bus);
|
|
207
225
|
}
|
|
226
|
+
export function unrouteAudioChannelFromMixerBus(mixer, channel) {
|
|
227
|
+
const runtime = mixerRuntimes.get(mixer);
|
|
228
|
+
if (runtime === undefined)
|
|
229
|
+
return;
|
|
230
|
+
runtime.activeChannels.delete(channel);
|
|
231
|
+
runtime.channelsPausedByMixer.delete(channel);
|
|
232
|
+
runtime.channelToBus.delete(channel);
|
|
233
|
+
// Reconnect the channel output to the context destination so it keeps playing if still active.
|
|
234
|
+
connectAudioChannelToNode(channel, runtime.context.destination);
|
|
235
|
+
}
|
|
236
|
+
// Reports a write to a bus that belongs to no mixer. Cheap by construction — the Map lookup happens only
|
|
237
|
+
// when a guard is installed.
|
|
238
|
+
function reportUnmixedBus(bus, operation) {
|
|
239
|
+
if (_unmixedBusGuard === null)
|
|
240
|
+
return;
|
|
241
|
+
if (busToMixerRuntimes.get(bus) === undefined)
|
|
242
|
+
_unmixedBusGuard(operation, bus);
|
|
243
|
+
}
|
|
244
|
+
let _unmixedBusGuard = null;
|
|
208
245
|
function updateBusGainNode(bus) {
|
|
209
246
|
const runtimes = busToMixerRuntimes.get(bus);
|
|
210
247
|
if (runtimes === undefined)
|
package/dist/audioMixer.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"audioMixer.js","sourceRoot":"","sources":["../src/audioMixer.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"audioMixer.js","sourceRoot":"","sources":["../src/audioMixer.ts"],"names":[],"mappings":"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"}
|
package/dist/contract.d.ts
CHANGED
package/dist/contract.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"contract.d.ts","sourceRoot":"","sources":["../src/contract.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAC;AAC/B,cAAc,cAAc,CAAC;AAC7B,cAAc,gBAAgB,CAAC"}
|
|
1
|
+
{"version":3,"file":"contract.d.ts","sourceRoot":"","sources":["../src/contract.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAC;AAC/B,cAAc,cAAc,CAAC;AAC7B,cAAc,0BAA0B,CAAC;AACzC,cAAc,gBAAgB,CAAC"}
|
package/dist/contract.js
CHANGED
package/dist/contract.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"contract.js","sourceRoot":"","sources":["../src/contract.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAC;AAC/B,cAAc,cAAc,CAAC;AAC7B,cAAc,gBAAgB,CAAC"}
|
|
1
|
+
{"version":3,"file":"contract.js","sourceRoot":"","sources":["../src/contract.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAC;AAC/B,cAAc,cAAc,CAAC;AAC7B,cAAc,0BAA0B,CAAC;AACzC,cAAc,gBAAgB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"enableAudioMixerGuards.d.ts","sourceRoot":"","sources":["../src/enableAudioMixerGuards.ts"],"names":[],"mappings":"AAOA,wBAAgB,uBAAuB,IAAI,IAAI,CAE9C;AAYD,wBAAgB,sBAAsB,IAAI,IAAI,CAE7C"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { logOnce } from '@flighthq/log/contract';
|
|
2
|
+
import { LogLevel } from '@flighthq/types/contract';
|
|
3
|
+
import { setAudioBusMixerGuard } from './audioMixer';
|
|
4
|
+
// Uninstalls the guard installed by enableAudioMixerGuards.
|
|
5
|
+
export function disableAudioMixerGuards() {
|
|
6
|
+
setAudioBusMixerGuard(null);
|
|
7
|
+
}
|
|
8
|
+
// Installs the caller-facing audio-mixer guard (opt-in, dev-only). setAudioBusGain, setAudioBusMuted and
|
|
9
|
+
// setAudioBusPan all write the bus field, return it, and push the value to the gain/panner node of every
|
|
10
|
+
// mixer holding that bus — but a bus belonging to NO mixer has no node to push to. The write is stored,
|
|
11
|
+
// the setter returns the value it just set, and nothing becomes audible. Nothing throws and the return
|
|
12
|
+
// value cannot express the difference, because it reports what was set rather than whether anything is
|
|
13
|
+
// listening, so "my gain change did nothing" is otherwise a silent debugging session.
|
|
14
|
+
//
|
|
15
|
+
// The guard warns once per operation through @flighthq/log. Not importing this module costs production
|
|
16
|
+
// nothing: the message text and the @flighthq/log dependency live only here, and the reverse-map lookup
|
|
17
|
+
// that detects the case runs only while the guard is installed. Idempotent.
|
|
18
|
+
export function enableAudioMixerGuards() {
|
|
19
|
+
setAudioBusMixerGuard(warnOnUnmixedBus);
|
|
20
|
+
}
|
|
21
|
+
function warnOnUnmixedBus(operation, bus) {
|
|
22
|
+
const setter = operation === 'gain' ? 'setAudioBusGain' : operation === 'mute' ? 'setAudioBusMuted' : 'setAudioBusPan';
|
|
23
|
+
logOnce(`media:unmixed-bus-${operation}`, LogLevel.Warn, {
|
|
24
|
+
message: `${setter}: bus "${bus.name ?? 'unnamed'}" belongs to no mixer, so the value was stored but reached no audio node and nothing changed audibly. Add the bus with addAudioBusToMixer(mixer, bus) — or route a channel through it with routeAudioChannelToMixerBus — before setting its properties.`,
|
|
25
|
+
}, 'media');
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=enableAudioMixerGuards.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"enableAudioMixerGuards.js","sourceRoot":"","sources":["../src/enableAudioMixerGuards.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,wBAAwB,CAAC;AAEjD,OAAO,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AAEpD,OAAO,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC;AAErD,4DAA4D;AAC5D,MAAM,UAAU,uBAAuB;IACrC,qBAAqB,CAAC,IAAI,CAAC,CAAC;AAC9B,CAAC;AAED,yGAAyG;AACzG,yGAAyG;AACzG,wGAAwG;AACxG,uGAAuG;AACvG,uGAAuG;AACvG,sFAAsF;AACtF,EAAE;AACF,uGAAuG;AACvG,wGAAwG;AACxG,4EAA4E;AAC5E,MAAM,UAAU,sBAAsB;IACpC,qBAAqB,CAAC,gBAAgB,CAAC,CAAC;AAC1C,CAAC;AAED,SAAS,gBAAgB,CAAC,SAAiC,EAAE,GAAuB;IAClF,MAAM,MAAM,GACV,SAAS,KAAK,MAAM,CAAC,CAAC,CAAC,iBAAiB,CAAC,CAAC,CAAC,SAAS,KAAK,MAAM,CAAC,CAAC,CAAC,kBAAkB,CAAC,CAAC,CAAC,gBAAgB,CAAC;IAC1G,OAAO,CACL,qBAAqB,SAAS,EAAE,EAChC,QAAQ,CAAC,IAAI,EACb;QACE,OAAO,EAAE,GAAG,MAAM,UAAU,GAAG,CAAC,IAAI,IAAI,SAAS,yPAAyP;KAC3S,EACD,OAAO,CACR,CAAC;AACJ,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flighthq/media",
|
|
3
|
-
"version": "0.3.0
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"author": "Joshua Granick and other contributors",
|
|
5
|
+
"license": "MIT",
|
|
4
6
|
"repository": {
|
|
5
7
|
"type": "git",
|
|
6
8
|
"url": "https://github.com/flighthq/flight.git",
|
|
@@ -36,10 +38,11 @@
|
|
|
36
38
|
"clean:dist": "tsx ../../scripts/clean-package-dist.ts"
|
|
37
39
|
},
|
|
38
40
|
"dependencies": {
|
|
39
|
-
"@flighthq/audio": "0.3.0
|
|
40
|
-
"@flighthq/
|
|
41
|
-
"@flighthq/signals": "0.3.0
|
|
42
|
-
"@flighthq/types": "0.3.0
|
|
41
|
+
"@flighthq/audio": "0.3.0",
|
|
42
|
+
"@flighthq/log": "0.3.0",
|
|
43
|
+
"@flighthq/signals": "0.3.0",
|
|
44
|
+
"@flighthq/types": "0.3.0",
|
|
45
|
+
"@flighthq/video": "0.3.0"
|
|
43
46
|
},
|
|
44
47
|
"devDependencies": {
|
|
45
48
|
"typescript": "^5.3.0"
|
package/src/audioMixer.test.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createAudioResource } from '@flighthq/audio/contract';
|
|
2
2
|
|
|
3
|
-
import { playAudioResource } from './audioChannel';
|
|
3
|
+
import { pauseAudioChannel, playAudioResource } from './audioChannel';
|
|
4
4
|
import {
|
|
5
5
|
addAudioBusToMixer,
|
|
6
6
|
createAudioBus,
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
resumeAllAudioMixerChannels,
|
|
13
13
|
routeAudioChannelToMixerBus,
|
|
14
14
|
setAudioBusGain,
|
|
15
|
+
setAudioBusMixerGuard,
|
|
15
16
|
setAudioBusMuted,
|
|
16
17
|
setAudioBusPan,
|
|
17
18
|
setAudioMixerMasterGain,
|
|
@@ -203,6 +204,84 @@ describe('resumeAllAudioMixerChannels', () => {
|
|
|
203
204
|
});
|
|
204
205
|
});
|
|
205
206
|
|
|
207
|
+
describe('resumeAllAudioMixerChannels scope', () => {
|
|
208
|
+
function playRouted(mixer: ReturnType<typeof createAudioMixer>, bus: ReturnType<typeof createAudioBus>) {
|
|
209
|
+
const channel = playAudioResource(ctx, createAudioResource(createMockAudioBuffer()))!;
|
|
210
|
+
routeAudioChannelToMixerBus(mixer, channel, bus);
|
|
211
|
+
return channel;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function mixerWithBus() {
|
|
215
|
+
const mixer = createAudioMixer(ctx);
|
|
216
|
+
const bus = createAudioBus({ name: 'sfx' });
|
|
217
|
+
addAudioBusToMixer(mixer, bus);
|
|
218
|
+
return { bus, mixer };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// A mixer-wide pause/resume around a menu must not resurrect a channel the caller had paused on its
|
|
222
|
+
// own. Resuming every paused channel — rather than the ones this mixer paused — silently did.
|
|
223
|
+
it('leaves a channel the caller paused independently still paused', () => {
|
|
224
|
+
const { bus, mixer } = mixerWithBus();
|
|
225
|
+
const mixerPaused = playRouted(mixer, bus);
|
|
226
|
+
const callerPaused = playRouted(mixer, bus);
|
|
227
|
+
|
|
228
|
+
pauseAudioChannel(callerPaused);
|
|
229
|
+
pauseAllAudioMixerChannels(mixer);
|
|
230
|
+
resumeAllAudioMixerChannels(mixer);
|
|
231
|
+
|
|
232
|
+
expect(mixerPaused.state).toBe('playing');
|
|
233
|
+
expect(callerPaused.state).toBe('paused');
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it('resumes the channels it paused', () => {
|
|
237
|
+
const { bus, mixer } = mixerWithBus();
|
|
238
|
+
const channel = playRouted(mixer, bus);
|
|
239
|
+
|
|
240
|
+
pauseAllAudioMixerChannels(mixer);
|
|
241
|
+
expect(channel.state).toBe('paused');
|
|
242
|
+
resumeAllAudioMixerChannels(mixer);
|
|
243
|
+
|
|
244
|
+
expect(channel.state).toBe('playing');
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
// Each transition that takes a channel away from the mixer must also drop it from the paused-by-mixer
|
|
248
|
+
// record, or a later resume restarts something the mixer no longer owns.
|
|
249
|
+
it('does not resume a channel unrouted while the mixer was paused', () => {
|
|
250
|
+
const { bus, mixer } = mixerWithBus();
|
|
251
|
+
const channel = playRouted(mixer, bus);
|
|
252
|
+
|
|
253
|
+
pauseAllAudioMixerChannels(mixer);
|
|
254
|
+
unrouteAudioChannelFromMixerBus(mixer, channel);
|
|
255
|
+
resumeAllAudioMixerChannels(mixer);
|
|
256
|
+
|
|
257
|
+
expect(channel.state).toBe('paused');
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it('does not resume a channel stopped while the mixer was paused', () => {
|
|
261
|
+
const { bus, mixer } = mixerWithBus();
|
|
262
|
+
const channel = playRouted(mixer, bus);
|
|
263
|
+
|
|
264
|
+
pauseAllAudioMixerChannels(mixer);
|
|
265
|
+
stopAllAudioMixerChannels(mixer);
|
|
266
|
+
resumeAllAudioMixerChannels(mixer);
|
|
267
|
+
|
|
268
|
+
expect(channel.state).toBe('stopped');
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
// The record is consumed by the resume, so a second resume has nothing of its own to restart.
|
|
272
|
+
it('does not re-resume on a second call after the caller paused again', () => {
|
|
273
|
+
const { bus, mixer } = mixerWithBus();
|
|
274
|
+
const channel = playRouted(mixer, bus);
|
|
275
|
+
|
|
276
|
+
pauseAllAudioMixerChannels(mixer);
|
|
277
|
+
resumeAllAudioMixerChannels(mixer);
|
|
278
|
+
pauseAudioChannel(channel);
|
|
279
|
+
resumeAllAudioMixerChannels(mixer);
|
|
280
|
+
|
|
281
|
+
expect(channel.state).toBe('paused');
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
|
|
206
285
|
describe('routeAudioChannelToMixerBus', () => {
|
|
207
286
|
it('adds the channel to the mixer active channels', () => {
|
|
208
287
|
const mixer = createAudioMixer(ctx);
|
|
@@ -222,6 +301,43 @@ describe('setAudioBusGain', () => {
|
|
|
222
301
|
});
|
|
223
302
|
});
|
|
224
303
|
|
|
304
|
+
describe('setAudioBusMixerGuard', () => {
|
|
305
|
+
it('reports a bus-property write that reaches no mixer, for each setter', () => {
|
|
306
|
+
const seen: string[] = [];
|
|
307
|
+
setAudioBusMixerGuard((operation) => seen.push(operation));
|
|
308
|
+
try {
|
|
309
|
+
// A bus that was never added to a mixer: every setter stores its value and returns it, but no
|
|
310
|
+
// audio node exists to receive it.
|
|
311
|
+
const orphan = createAudioBus({ name: 'orphan' });
|
|
312
|
+
expect(setAudioBusGain(orphan, 0.25)).toBe(0.25);
|
|
313
|
+
setAudioBusMuted(orphan, true);
|
|
314
|
+
setAudioBusPan(orphan, -1);
|
|
315
|
+
} finally {
|
|
316
|
+
setAudioBusMixerGuard(null);
|
|
317
|
+
}
|
|
318
|
+
expect(seen).toEqual(['gain', 'mute', 'pan']);
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
it('stays silent for a bus that belongs to a mixer, and once uninstalled', () => {
|
|
322
|
+
const seen: string[] = [];
|
|
323
|
+
setAudioBusMixerGuard((operation) => seen.push(operation));
|
|
324
|
+
try {
|
|
325
|
+
const mixer = createAudioMixer(ctx);
|
|
326
|
+
const bus = createAudioBus({ name: 'music' });
|
|
327
|
+
addAudioBusToMixer(mixer, bus);
|
|
328
|
+
setAudioBusGain(bus, 0.5);
|
|
329
|
+
setAudioBusMuted(bus, true);
|
|
330
|
+
setAudioBusPan(bus, 0.5);
|
|
331
|
+
expect(seen).toEqual([]);
|
|
332
|
+
} finally {
|
|
333
|
+
setAudioBusMixerGuard(null);
|
|
334
|
+
}
|
|
335
|
+
// Uninstalled: the orphan write is still a silent no-op, but nothing is reported.
|
|
336
|
+
setAudioBusGain(createAudioBus({ name: 'orphan2' }), 0.75);
|
|
337
|
+
expect(seen).toEqual([]);
|
|
338
|
+
});
|
|
339
|
+
});
|
|
340
|
+
|
|
225
341
|
describe('setAudioBusMuted', () => {
|
|
226
342
|
it('mutes the bus', () => {
|
|
227
343
|
const bus = createAudioBus();
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { addLogSink, createMemoryLogSink, getMemoryLogSinkEntries, removeLogSink } from '@flighthq/log/contract';
|
|
2
|
+
import type { LogEntry } from '@flighthq/types/contract';
|
|
3
|
+
|
|
4
|
+
import { addAudioBusToMixer, createAudioBus, createAudioMixer, setAudioBusGain } from './audioMixer';
|
|
5
|
+
import { disableAudioMixerGuards, enableAudioMixerGuards } from './enableAudioMixerGuards';
|
|
6
|
+
|
|
7
|
+
// The narrowest Web Audio stand-in a mixer + bus needs; the guard under test never touches audio itself.
|
|
8
|
+
class MockGainNode {
|
|
9
|
+
gain = { value: 1 };
|
|
10
|
+
connect(): void {}
|
|
11
|
+
disconnect(): void {}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
class MockAudioContext {
|
|
15
|
+
currentTime = 0;
|
|
16
|
+
destination = {};
|
|
17
|
+
state = 'running';
|
|
18
|
+
createGain(): GainNode {
|
|
19
|
+
return new MockGainNode() as unknown as GainNode;
|
|
20
|
+
}
|
|
21
|
+
createStereoPanner(): StereoPannerNode {
|
|
22
|
+
return { connect() {}, disconnect() {}, pan: { value: 0 } } as unknown as StereoPannerNode;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const ctx = new MockAudioContext() as unknown as AudioContext;
|
|
27
|
+
|
|
28
|
+
function captureLog(run: () => void): readonly LogEntry[] {
|
|
29
|
+
const sink = createMemoryLogSink(8);
|
|
30
|
+
addLogSink(sink.sink);
|
|
31
|
+
try {
|
|
32
|
+
run();
|
|
33
|
+
return getMemoryLogSinkEntries(sink);
|
|
34
|
+
} finally {
|
|
35
|
+
removeLogSink(sink.sink);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function messageOf(entry: Readonly<LogEntry>): string {
|
|
40
|
+
const data = entry.data;
|
|
41
|
+
return typeof data === 'string' ? data : String(data.message);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
describe('disableAudioMixerGuards', () => {
|
|
45
|
+
it('uninstalls the guard, returning the silent no-op to silence', () => {
|
|
46
|
+
const entries = captureLog(() => {
|
|
47
|
+
enableAudioMixerGuards();
|
|
48
|
+
disableAudioMixerGuards();
|
|
49
|
+
setAudioBusGain(createAudioBus({ name: 'orphan' }), 0.25);
|
|
50
|
+
});
|
|
51
|
+
expect(entries.length).toBe(0);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe('enableAudioMixerGuards', () => {
|
|
56
|
+
it('WARNS that a gain change on an unmixed bus reached no audio node', () => {
|
|
57
|
+
const entries = captureLog(() => {
|
|
58
|
+
enableAudioMixerGuards();
|
|
59
|
+
try {
|
|
60
|
+
// Returns the value it set, which is exactly why the failure is invisible without the guard.
|
|
61
|
+
expect(setAudioBusGain(createAudioBus({ name: 'orphan' }), 0.25)).toBe(0.25);
|
|
62
|
+
} finally {
|
|
63
|
+
disableAudioMixerGuards();
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
expect(entries.length).toBe(1);
|
|
67
|
+
expect(messageOf(entries[0])).toContain('addAudioBusToMixer');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('stays SILENT for a bus that belongs to a mixer', () => {
|
|
71
|
+
const entries = captureLog(() => {
|
|
72
|
+
enableAudioMixerGuards();
|
|
73
|
+
try {
|
|
74
|
+
const mixer = createAudioMixer(ctx);
|
|
75
|
+
const bus = createAudioBus({ name: 'music' });
|
|
76
|
+
addAudioBusToMixer(mixer, bus);
|
|
77
|
+
setAudioBusGain(bus, 0.5);
|
|
78
|
+
} finally {
|
|
79
|
+
disableAudioMixerGuards();
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
expect(entries.length).toBe(0);
|
|
83
|
+
});
|
|
84
|
+
});
|
package/src/videoChannel.test.ts
CHANGED
|
@@ -44,7 +44,7 @@ describe('getVideoChannelHeight', () => {
|
|
|
44
44
|
it('returns 0 when element is null', () => {
|
|
45
45
|
const channel = playVideoResource(createVideoResource(createMockVideoElement()));
|
|
46
46
|
expect(channel).not.toBeNull();
|
|
47
|
-
channel!.source = { element: null };
|
|
47
|
+
channel!.source = { element: null, objectUrl: null };
|
|
48
48
|
expect(getVideoChannelHeight(channel!)).toBe(0);
|
|
49
49
|
});
|
|
50
50
|
});
|
|
@@ -60,7 +60,7 @@ describe('getVideoChannelWidth', () => {
|
|
|
60
60
|
it('returns 0 when element is null', () => {
|
|
61
61
|
const channel = playVideoResource(createVideoResource(createMockVideoElement()));
|
|
62
62
|
expect(channel).not.toBeNull();
|
|
63
|
-
channel!.source = { element: null };
|
|
63
|
+
channel!.source = { element: null, objectUrl: null };
|
|
64
64
|
expect(getVideoChannelWidth(channel!)).toBe(0);
|
|
65
65
|
});
|
|
66
66
|
});
|