@flighthq/media 0.1.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/audioChannel.d.ts +16 -0
- package/dist/audioChannel.d.ts.map +1 -0
- package/dist/audioChannel.js +163 -0
- package/dist/audioChannel.js.map +1 -0
- package/dist/audioMixer.d.ts +18 -0
- package/dist/audioMixer.d.ts.map +1 -0
- package/dist/audioMixer.js +230 -0
- package/dist/audioMixer.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/videoChannel.d.ts +14 -0
- package/dist/videoChannel.d.ts.map +1 -0
- package/dist/videoChannel.js +128 -0
- package/dist/videoChannel.js.map +1 -0
- package/package.json +40 -0
- package/src/audioChannel.test.ts +207 -0
- package/src/audioMixer.test.ts +281 -0
- package/src/videoChannel.test.ts +200 -0
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@flighthq/media",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"default": "./dist/index.js"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist",
|
|
15
|
+
"src/**/*.test.ts",
|
|
16
|
+
"!dist/**/*.test.js",
|
|
17
|
+
"!dist/**/*.test.d.ts",
|
|
18
|
+
"!dist/**/*.test.js.map",
|
|
19
|
+
"!dist/**/*.test.d.ts.map"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "tsc -b",
|
|
23
|
+
"clean": "tsc -b --clean",
|
|
24
|
+
"test": "vitest run --config vitest.config.ts",
|
|
25
|
+
"test:watch": "vitest --watch --config vitest.config.ts",
|
|
26
|
+
"prepack": "npm run clean && npm run clean:dist && npm run build",
|
|
27
|
+
"clean:dist": "tsx ../../scripts/clean-package-dist.ts"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@flighthq/audio": "0.1.0",
|
|
31
|
+
"@flighthq/video": "0.1.0",
|
|
32
|
+
"@flighthq/signals": "0.1.0",
|
|
33
|
+
"@flighthq/types": "0.1.0"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"typescript": "^5.3.0"
|
|
37
|
+
},
|
|
38
|
+
"description": "Audio and video playback channels",
|
|
39
|
+
"sideEffects": false
|
|
40
|
+
}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { createAudioResource } from '@flighthq/audio';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
connectAudioChannelToNode,
|
|
5
|
+
fadeAudioChannelGain,
|
|
6
|
+
getAudioChannelCurrentTime,
|
|
7
|
+
getAudioChannelDuration,
|
|
8
|
+
getAudioChannelInputNode,
|
|
9
|
+
getAudioChannelOutputNode,
|
|
10
|
+
isAudioChannelPlaying,
|
|
11
|
+
pauseAudioChannel,
|
|
12
|
+
playAudioResource,
|
|
13
|
+
resumeAudioChannel,
|
|
14
|
+
setAudioChannelCurrentTime,
|
|
15
|
+
setAudioChannelGain,
|
|
16
|
+
setAudioChannelPlaybackRate,
|
|
17
|
+
stopAudioChannel,
|
|
18
|
+
} from './audioChannel';
|
|
19
|
+
|
|
20
|
+
class MockAudioBufferSourceNode {
|
|
21
|
+
buffer: AudioBuffer | null = null;
|
|
22
|
+
onended: (() => void) | null = null;
|
|
23
|
+
playbackRate = { value: 1 };
|
|
24
|
+
|
|
25
|
+
connect(): void {}
|
|
26
|
+
start(): void {}
|
|
27
|
+
stop(): void {}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
class MockAudioContext {
|
|
31
|
+
currentTime = 0;
|
|
32
|
+
destination = {};
|
|
33
|
+
state = 'running';
|
|
34
|
+
|
|
35
|
+
createBufferSource(): AudioBufferSourceNode {
|
|
36
|
+
return new MockAudioBufferSourceNode() as unknown as AudioBufferSourceNode;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
createGain(): GainNode {
|
|
40
|
+
return {
|
|
41
|
+
connect() {},
|
|
42
|
+
disconnect() {},
|
|
43
|
+
gain: { value: 1 },
|
|
44
|
+
} as unknown as GainNode;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
resume(): Promise<void> {
|
|
48
|
+
return Promise.resolve();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const ctx = new MockAudioContext() as unknown as AudioContext;
|
|
53
|
+
|
|
54
|
+
function createMockAudioBuffer(): AudioBuffer {
|
|
55
|
+
return { duration: 1 } as AudioBuffer;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
describe('connectAudioChannelToNode', () => {
|
|
59
|
+
it('reroutes the active gain node to the destination without error', () => {
|
|
60
|
+
const channel = playAudioResource(ctx, createAudioResource(createMockAudioBuffer()));
|
|
61
|
+
expect(channel).not.toBeNull();
|
|
62
|
+
const destination = {} as AudioNode;
|
|
63
|
+
expect(() => connectAudioChannelToNode(channel!, destination)).not.toThrow();
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe('fadeAudioChannelGain', () => {
|
|
68
|
+
it('updates the channel gain immediately when no gain node is active', () => {
|
|
69
|
+
const channel = playAudioResource(ctx, createAudioResource(createMockAudioBuffer()));
|
|
70
|
+
expect(channel).not.toBeNull();
|
|
71
|
+
stopAudioChannel(channel!);
|
|
72
|
+
fadeAudioChannelGain(channel!, 0.5, 500);
|
|
73
|
+
expect(channel!.gain).toBe(0.5);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe('getAudioChannelCurrentTime', () => {
|
|
78
|
+
it('returns the stored current time for an inactive channel', () => {
|
|
79
|
+
const channel = playAudioResource(ctx, createAudioResource(createMockAudioBuffer()), { currentTime: 250 });
|
|
80
|
+
expect(channel).not.toBeNull();
|
|
81
|
+
pauseAudioChannel(channel!);
|
|
82
|
+
expect(getAudioChannelCurrentTime(channel!)).toBe(250);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe('getAudioChannelDuration', () => {
|
|
87
|
+
it('returns the channel length', () => {
|
|
88
|
+
const channel = playAudioResource(ctx, createAudioResource(createMockAudioBuffer()));
|
|
89
|
+
expect(channel).not.toBeNull();
|
|
90
|
+
expect(getAudioChannelDuration(channel!)).toBe(1000);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe('getAudioChannelInputNode', () => {
|
|
95
|
+
it('returns the source node while playing', () => {
|
|
96
|
+
const channel = playAudioResource(ctx, createAudioResource(createMockAudioBuffer()));
|
|
97
|
+
expect(channel).not.toBeNull();
|
|
98
|
+
expect(getAudioChannelInputNode(channel!)).not.toBeNull();
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('returns null after stop', () => {
|
|
102
|
+
const channel = playAudioResource(ctx, createAudioResource(createMockAudioBuffer()));
|
|
103
|
+
expect(channel).not.toBeNull();
|
|
104
|
+
stopAudioChannel(channel!);
|
|
105
|
+
expect(getAudioChannelInputNode(channel!)).toBeNull();
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
describe('getAudioChannelOutputNode', () => {
|
|
110
|
+
it('returns the gain node while playing', () => {
|
|
111
|
+
const channel = playAudioResource(ctx, createAudioResource(createMockAudioBuffer()));
|
|
112
|
+
expect(channel).not.toBeNull();
|
|
113
|
+
expect(getAudioChannelOutputNode(channel!)).not.toBeNull();
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('returns null after stop', () => {
|
|
117
|
+
const channel = playAudioResource(ctx, createAudioResource(createMockAudioBuffer()));
|
|
118
|
+
expect(channel).not.toBeNull();
|
|
119
|
+
stopAudioChannel(channel!);
|
|
120
|
+
expect(getAudioChannelOutputNode(channel!)).toBeNull();
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
describe('isAudioChannelPlaying', () => {
|
|
125
|
+
it('returns true while playing', () => {
|
|
126
|
+
const channel = playAudioResource(ctx, createAudioResource(createMockAudioBuffer()));
|
|
127
|
+
expect(channel).not.toBeNull();
|
|
128
|
+
expect(isAudioChannelPlaying(channel!)).toBe(true);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('returns false when paused', () => {
|
|
132
|
+
const channel = playAudioResource(ctx, createAudioResource(createMockAudioBuffer()));
|
|
133
|
+
expect(channel).not.toBeNull();
|
|
134
|
+
pauseAudioChannel(channel!);
|
|
135
|
+
expect(isAudioChannelPlaying(channel!)).toBe(false);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
describe('pauseAudioChannel', () => {
|
|
140
|
+
it('preserves playback position and marks the channel as paused', () => {
|
|
141
|
+
const channel = playAudioResource(ctx, createAudioResource(createMockAudioBuffer()), { currentTime: 100 });
|
|
142
|
+
expect(channel).not.toBeNull();
|
|
143
|
+
pauseAudioChannel(channel!);
|
|
144
|
+
expect(channel!.currentTime).toBe(100);
|
|
145
|
+
expect(channel!.state).toBe('paused');
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
describe('playAudioResource', () => {
|
|
150
|
+
it('returns null when buffer is null', () => {
|
|
151
|
+
const source = createAudioResource();
|
|
152
|
+
expect(playAudioResource(ctx, source)).toBeNull();
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('returns a playing channel when buffer is available', () => {
|
|
156
|
+
const channel = playAudioResource(ctx, createAudioResource(createMockAudioBuffer()), { gain: 0.5 });
|
|
157
|
+
expect(channel).not.toBeNull();
|
|
158
|
+
expect(channel!.gain).toBe(0.5);
|
|
159
|
+
expect(channel!.state).toBe('playing');
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
describe('resumeAudioChannel', () => {
|
|
164
|
+
it('restarts playback from a paused channel', () => {
|
|
165
|
+
const channel = playAudioResource(ctx, createAudioResource(createMockAudioBuffer()));
|
|
166
|
+
expect(channel).not.toBeNull();
|
|
167
|
+
pauseAudioChannel(channel!);
|
|
168
|
+
resumeAudioChannel(channel!);
|
|
169
|
+
expect(channel!.state).toBe('playing');
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
describe('setAudioChannelCurrentTime', () => {
|
|
174
|
+
it('updates and clamps the channel current time', () => {
|
|
175
|
+
const channel = playAudioResource(ctx, createAudioResource(createMockAudioBuffer()));
|
|
176
|
+
expect(channel).not.toBeNull();
|
|
177
|
+
expect(setAudioChannelCurrentTime(channel!, 2000)).toBe(1000);
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
describe('setAudioChannelGain', () => {
|
|
182
|
+
it('updates the channel gain', () => {
|
|
183
|
+
const channel = playAudioResource(ctx, createAudioResource(createMockAudioBuffer()));
|
|
184
|
+
expect(channel).not.toBeNull();
|
|
185
|
+
expect(setAudioChannelGain(channel!, 0.25)).toBe(0.25);
|
|
186
|
+
expect(channel!.gain).toBe(0.25);
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
describe('setAudioChannelPlaybackRate', () => {
|
|
191
|
+
it('updates the channel playback rate', () => {
|
|
192
|
+
const channel = playAudioResource(ctx, createAudioResource(createMockAudioBuffer()));
|
|
193
|
+
expect(channel).not.toBeNull();
|
|
194
|
+
expect(setAudioChannelPlaybackRate(channel!, 2)).toBe(2);
|
|
195
|
+
expect(channel!.playbackRate).toBe(2);
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
describe('stopAudioChannel', () => {
|
|
200
|
+
it('stops playback and resets the current time', () => {
|
|
201
|
+
const channel = playAudioResource(ctx, createAudioResource(createMockAudioBuffer()), { currentTime: 500 });
|
|
202
|
+
expect(channel).not.toBeNull();
|
|
203
|
+
stopAudioChannel(channel!);
|
|
204
|
+
expect(channel!.currentTime).toBe(0);
|
|
205
|
+
expect(channel!.state).toBe('stopped');
|
|
206
|
+
});
|
|
207
|
+
});
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import { createAudioResource } from '@flighthq/audio';
|
|
2
|
+
|
|
3
|
+
import { playAudioResource } from './audioChannel';
|
|
4
|
+
import {
|
|
5
|
+
addAudioBusToMixer,
|
|
6
|
+
createAudioBus,
|
|
7
|
+
createAudioMixer,
|
|
8
|
+
destroyAudioMixer,
|
|
9
|
+
fadeAudioBusGain,
|
|
10
|
+
getAudioMixerActiveChannels,
|
|
11
|
+
pauseAllAudioMixerChannels,
|
|
12
|
+
resumeAllAudioMixerChannels,
|
|
13
|
+
routeAudioChannelToMixerBus,
|
|
14
|
+
setAudioBusGain,
|
|
15
|
+
setAudioBusMuted,
|
|
16
|
+
setAudioBusPan,
|
|
17
|
+
setAudioMixerMasterGain,
|
|
18
|
+
setAudioMixerMasterMuted,
|
|
19
|
+
stopAllAudioMixerChannels,
|
|
20
|
+
unrouteAudioChannelFromMixerBus,
|
|
21
|
+
} from './audioMixer';
|
|
22
|
+
|
|
23
|
+
const createdSourceNodes: MockAudioBufferSourceNode[] = [];
|
|
24
|
+
|
|
25
|
+
class MockStereoPannerNode {
|
|
26
|
+
pan = { value: 0 };
|
|
27
|
+
connect(): void {}
|
|
28
|
+
disconnect(): void {}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
class MockAudioBufferSourceNode {
|
|
32
|
+
buffer: AudioBuffer | null = null;
|
|
33
|
+
loopEnd = 0;
|
|
34
|
+
loopStart = 0;
|
|
35
|
+
onended: (() => void) | null = null;
|
|
36
|
+
playbackRate = { value: 1 };
|
|
37
|
+
stopCount = 0;
|
|
38
|
+
connect(): void {}
|
|
39
|
+
start(): void {}
|
|
40
|
+
stop(): void {
|
|
41
|
+
this.stopCount++;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
class MockGainNode {
|
|
46
|
+
gain = {
|
|
47
|
+
cancelScheduledValues: () => {},
|
|
48
|
+
linearRampToValueAtTime: () => {},
|
|
49
|
+
setValueAtTime: () => {},
|
|
50
|
+
value: 1,
|
|
51
|
+
};
|
|
52
|
+
connect(): void {}
|
|
53
|
+
disconnect(): void {}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
class MockAudioContext {
|
|
57
|
+
currentTime = 0;
|
|
58
|
+
destination = {};
|
|
59
|
+
state = 'running';
|
|
60
|
+
createBufferSource(): AudioBufferSourceNode {
|
|
61
|
+
const node = new MockAudioBufferSourceNode();
|
|
62
|
+
createdSourceNodes.push(node);
|
|
63
|
+
return node as unknown as AudioBufferSourceNode;
|
|
64
|
+
}
|
|
65
|
+
createGain(): GainNode {
|
|
66
|
+
return new MockGainNode() as unknown as GainNode;
|
|
67
|
+
}
|
|
68
|
+
createStereoPanner(): StereoPannerNode {
|
|
69
|
+
return new MockStereoPannerNode() as unknown as StereoPannerNode;
|
|
70
|
+
}
|
|
71
|
+
resume(): Promise<void> {
|
|
72
|
+
return Promise.resolve();
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const ctx = new MockAudioContext() as unknown as AudioContext;
|
|
77
|
+
|
|
78
|
+
function createMockAudioBuffer(): AudioBuffer {
|
|
79
|
+
return { duration: 1 } as AudioBuffer;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
describe('addAudioBusToMixer', () => {
|
|
83
|
+
it('registers the bus in the Web Audio graph without error', () => {
|
|
84
|
+
const mixer = createAudioMixer(ctx);
|
|
85
|
+
const bus = createAudioBus({ name: 'sfx', gain: 0.8 });
|
|
86
|
+
expect(() => addAudioBusToMixer(mixer, bus)).not.toThrow();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('is idempotent — calling twice does not duplicate the bus', () => {
|
|
90
|
+
const mixer = createAudioMixer(ctx);
|
|
91
|
+
const bus = createAudioBus({ name: 'music' });
|
|
92
|
+
addAudioBusToMixer(mixer, bus);
|
|
93
|
+
expect(() => addAudioBusToMixer(mixer, bus)).not.toThrow();
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe('createAudioBus', () => {
|
|
98
|
+
it('creates a bus with default values', () => {
|
|
99
|
+
const bus = createAudioBus();
|
|
100
|
+
expect(bus.gain).toBe(1);
|
|
101
|
+
expect(bus.muted).toBe(false);
|
|
102
|
+
expect(bus.name).toBe('');
|
|
103
|
+
expect(bus.pan).toBe(0);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('creates a bus with provided options', () => {
|
|
107
|
+
const bus = createAudioBus({ gain: 0.7, muted: true, name: 'sfx', pan: 0.3 });
|
|
108
|
+
expect(bus.gain).toBe(0.7);
|
|
109
|
+
expect(bus.muted).toBe(true);
|
|
110
|
+
expect(bus.name).toBe('sfx');
|
|
111
|
+
expect(bus.pan).toBe(0.3);
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
describe('createAudioMixer', () => {
|
|
116
|
+
it('creates a mixer with default master values', () => {
|
|
117
|
+
const mixer = createAudioMixer(ctx);
|
|
118
|
+
expect(mixer.masterGain).toBe(1);
|
|
119
|
+
expect(mixer.masterMuted).toBe(false);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('creates a mixer with provided options', () => {
|
|
123
|
+
const mixer = createAudioMixer(ctx, { masterGain: 0.5, masterMuted: true });
|
|
124
|
+
expect(mixer.masterGain).toBe(0.5);
|
|
125
|
+
expect(mixer.masterMuted).toBe(true);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
describe('destroyAudioMixer', () => {
|
|
130
|
+
it('stops routed channels, clears the active set, and is safe to call twice', () => {
|
|
131
|
+
const mixer = createAudioMixer(ctx);
|
|
132
|
+
const bus = createAudioBus();
|
|
133
|
+
const channel = playAudioResource(ctx, createAudioResource(createMockAudioBuffer()));
|
|
134
|
+
expect(channel).not.toBeNull();
|
|
135
|
+
routeAudioChannelToMixerBus(mixer, channel!, bus);
|
|
136
|
+
destroyAudioMixer(mixer);
|
|
137
|
+
expect(channel!.state).toBe('stopped');
|
|
138
|
+
expect(getAudioMixerActiveChannels(mixer)).toHaveLength(0);
|
|
139
|
+
expect(() => destroyAudioMixer(mixer)).not.toThrow();
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
describe('fadeAudioBusGain', () => {
|
|
144
|
+
it('updates bus gain immediately when no audio context time has passed', () => {
|
|
145
|
+
const mixer = createAudioMixer(ctx);
|
|
146
|
+
const bus = createAudioBus({ gain: 1 });
|
|
147
|
+
addAudioBusToMixer(mixer, bus);
|
|
148
|
+
fadeAudioBusGain(mixer, bus, 0.5, 500);
|
|
149
|
+
expect(bus.gain).toBe(0.5);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('updates bus gain data when bus is not in a mixer', () => {
|
|
153
|
+
const mixer = createAudioMixer(ctx);
|
|
154
|
+
const bus = createAudioBus({ gain: 1 });
|
|
155
|
+
fadeAudioBusGain(mixer, bus, 0.3, 200);
|
|
156
|
+
expect(bus.gain).toBe(0.3);
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
describe('getAudioMixerActiveChannels', () => {
|
|
161
|
+
it('returns an empty array for a new mixer', () => {
|
|
162
|
+
const mixer = createAudioMixer(ctx);
|
|
163
|
+
expect(getAudioMixerActiveChannels(mixer)).toHaveLength(0);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('returns routed channels', () => {
|
|
167
|
+
const mixer = createAudioMixer(ctx);
|
|
168
|
+
const bus = createAudioBus();
|
|
169
|
+
const channel = playAudioResource(ctx, createAudioResource(createMockAudioBuffer()));
|
|
170
|
+
expect(channel).not.toBeNull();
|
|
171
|
+
routeAudioChannelToMixerBus(mixer, channel!, bus);
|
|
172
|
+
expect(getAudioMixerActiveChannels(mixer)).toHaveLength(1);
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
describe('pauseAllAudioMixerChannels', () => {
|
|
177
|
+
it('stops the source node and marks playing channels as paused', () => {
|
|
178
|
+
createdSourceNodes.length = 0;
|
|
179
|
+
const mixer = createAudioMixer(ctx);
|
|
180
|
+
const bus = createAudioBus();
|
|
181
|
+
const channel = playAudioResource(ctx, createAudioResource(createMockAudioBuffer()));
|
|
182
|
+
expect(channel).not.toBeNull();
|
|
183
|
+
routeAudioChannelToMixerBus(mixer, channel!, bus);
|
|
184
|
+
pauseAllAudioMixerChannels(mixer);
|
|
185
|
+
expect(channel!.state).toBe('paused');
|
|
186
|
+
expect(createdSourceNodes[0]?.stopCount).toBeGreaterThan(0);
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
describe('resumeAllAudioMixerChannels', () => {
|
|
191
|
+
it('restarts the source node and marks paused channels as playing', () => {
|
|
192
|
+
createdSourceNodes.length = 0;
|
|
193
|
+
const mixer = createAudioMixer(ctx);
|
|
194
|
+
const bus = createAudioBus();
|
|
195
|
+
const channel = playAudioResource(ctx, createAudioResource(createMockAudioBuffer()));
|
|
196
|
+
expect(channel).not.toBeNull();
|
|
197
|
+
routeAudioChannelToMixerBus(mixer, channel!, bus);
|
|
198
|
+
pauseAllAudioMixerChannels(mixer);
|
|
199
|
+
resumeAllAudioMixerChannels(mixer);
|
|
200
|
+
expect(channel!.state).toBe('playing');
|
|
201
|
+
// A fresh source node is created on resume (the paused one was stopped and discarded).
|
|
202
|
+
expect(createdSourceNodes.length).toBeGreaterThan(1);
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
describe('routeAudioChannelToMixerBus', () => {
|
|
207
|
+
it('adds the channel to the mixer active channels', () => {
|
|
208
|
+
const mixer = createAudioMixer(ctx);
|
|
209
|
+
const bus = createAudioBus();
|
|
210
|
+
const channel = playAudioResource(ctx, createAudioResource(createMockAudioBuffer()));
|
|
211
|
+
expect(channel).not.toBeNull();
|
|
212
|
+
routeAudioChannelToMixerBus(mixer, channel!, bus);
|
|
213
|
+
expect(getAudioMixerActiveChannels(mixer)).toContain(channel!);
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
describe('setAudioBusGain', () => {
|
|
218
|
+
it('updates bus gain', () => {
|
|
219
|
+
const bus = createAudioBus();
|
|
220
|
+
expect(setAudioBusGain(bus, 0.5)).toBe(0.5);
|
|
221
|
+
expect(bus.gain).toBe(0.5);
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
describe('setAudioBusMuted', () => {
|
|
226
|
+
it('mutes the bus', () => {
|
|
227
|
+
const bus = createAudioBus();
|
|
228
|
+
expect(setAudioBusMuted(bus, true)).toBe(true);
|
|
229
|
+
expect(bus.muted).toBe(true);
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
describe('setAudioBusPan', () => {
|
|
234
|
+
it('sets bus pan and clamps to [-1, 1]', () => {
|
|
235
|
+
const bus = createAudioBus();
|
|
236
|
+
expect(setAudioBusPan(bus, 0.5)).toBe(0.5);
|
|
237
|
+
expect(setAudioBusPan(bus, 2)).toBe(1);
|
|
238
|
+
expect(setAudioBusPan(bus, -2)).toBe(-1);
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
describe('setAudioMixerMasterGain', () => {
|
|
243
|
+
it('updates the master gain', () => {
|
|
244
|
+
const mixer = createAudioMixer(ctx);
|
|
245
|
+
expect(setAudioMixerMasterGain(mixer, 0.5)).toBe(0.5);
|
|
246
|
+
expect(mixer.masterGain).toBe(0.5);
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
describe('setAudioMixerMasterMuted', () => {
|
|
251
|
+
it('mutes the master', () => {
|
|
252
|
+
const mixer = createAudioMixer(ctx);
|
|
253
|
+
expect(setAudioMixerMasterMuted(mixer, true)).toBe(true);
|
|
254
|
+
expect(mixer.masterMuted).toBe(true);
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
describe('stopAllAudioMixerChannels', () => {
|
|
259
|
+
it('stops all routed channels and clears the active set', () => {
|
|
260
|
+
const mixer = createAudioMixer(ctx);
|
|
261
|
+
const bus = createAudioBus();
|
|
262
|
+
const channel = playAudioResource(ctx, createAudioResource(createMockAudioBuffer()));
|
|
263
|
+
expect(channel).not.toBeNull();
|
|
264
|
+
routeAudioChannelToMixerBus(mixer, channel!, bus);
|
|
265
|
+
stopAllAudioMixerChannels(mixer);
|
|
266
|
+
expect(channel!.state).toBe('stopped');
|
|
267
|
+
expect(getAudioMixerActiveChannels(mixer)).toHaveLength(0);
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
describe('unrouteAudioChannelFromMixerBus', () => {
|
|
272
|
+
it('removes the channel from the mixer active channels', () => {
|
|
273
|
+
const mixer = createAudioMixer(ctx);
|
|
274
|
+
const bus = createAudioBus();
|
|
275
|
+
const channel = playAudioResource(ctx, createAudioResource(createMockAudioBuffer()));
|
|
276
|
+
expect(channel).not.toBeNull();
|
|
277
|
+
routeAudioChannelToMixerBus(mixer, channel!, bus);
|
|
278
|
+
unrouteAudioChannelFromMixerBus(mixer, channel!);
|
|
279
|
+
expect(getAudioMixerActiveChannels(mixer)).toHaveLength(0);
|
|
280
|
+
});
|
|
281
|
+
});
|