1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333 |
 
 
 
 
 
 
 
 
x1
x1
x1
x1
 
 
 
 
 
x1
x1
x278
x278
x278
x1
 
 
 
 
 
 
 
 
 
 
x1
x278
x278
x278
x278
x278
x278
x1
 
 
 
 
 
 
 
x2
 
 
 
 
 
 
 
x2
 
 
 
 
 
x1
 
 
 
 
x1
 
 
 
 
x17
 
 
 
 
x17
 
 
 
 
x1
 
 
 
 
 
 
 
 
 
 
 
x1
x17
 
 
x17
x18
x18
x17
x18
x18
 
x31
x31
x31
x17
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
x1
x279
x279
x279
 
x837
 
x279
x279
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
x1
 
x22
x24
x24
 
x41
x41
x41
x41
 
 
x22
x39
 
x117
x39
 
 
x41
x41
x41
x41
x41
x84
x84
x84
x84
x84
x84
x94
x41
 
x41
x41
x22
 
 
 
 
 
 
 
x1
x2
x2
x4
x2
x2
x2
 
 
 
 
 
 
 
x1
x2
x2
x4
x2
x2
x2
 
 
 
 
 
 
 
 
x1
x8
 
x8
x8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
x1
x5
x5
 
x15
x5
 
 
 
 
 
 
 
x1
x297
x297
 
 
 
 
 
 
 
 
 
 
x1
x11
x44111
x11
x11
 
 
 
 
 
 
 
 
 
x1
x20
x83810
x83810
x20
x20
 
 
 
 
 
 
 
 
 
x1
x279
x280
x280
 
x556
x142661
 
x556
x556
x279
 
 
 
 
 
 
 
 
 
x1
x31
x33
x33
 
x59
x31
x33
x33
 
x57
x31
x1 |
|
/**
* @file Audio mixer module
*
* Audio mixing, with ignoring own input and no recent input (suppose unstable connection).
*
* @author aKuad
*/
import { ONE_FRAME_SAMPLES } from "../static/AUDIO_PARAM.js";
import { dbfs_float } from "../static/util/dbfs.js";
import { LaneInfo } from "../static/packet_conv/LaneInfo.js";
import { LaneLoudness } from "../static/packet_conv/LaneLoudness.js";
/**
* Data structure for mixer lane
*/
class AudioMixerLane {
pcm: Float32Array;
lane_name: string;
gain_db: number;
last_input_ts: number;
dbfs: number;
/**
* Data structure of a lane data, parameters and status
*
* @param pcm Data - PCM
* @param lane_name Parameter - lane name (expected 1~3 characters, but not test in this class)
* @param gain_db Parameter - gain, volume control of the lane
* @param last_input_ts Status - last input time stamp
* @param dbfs Status - lane loudness in dBFS
*/
constructor(pcm: Float32Array, lane_name: string, gain_db: number, last_input_ts: number, dbfs: number) {
this.pcm = pcm;
this.lane_name = lane_name;
this.gain_db = gain_db;
this.last_input_ts = last_input_ts;
this.dbfs = dbfs;
}
}
/**
* Throw when try to create over #MAX_LANE_COUNT lanes
*
* Empty definition, because it for custom named error
*/
export class MaxLanesReachedError extends Error {}
/**
* Throw when non existing lane_id specified
*
* Empty definition, because it for custom named error
*/
export class NonExistingLaneIdError extends Error {}
/**
* Audio mixer module
*/
export class AudioMixer extends EventTarget{
/**
* Maximum number of lanes
* @constant
*/
#MAX_LANE_COUNT: number = 256;
/**
* Lanes data and properties, this dict key means `lane_id`
*/
#lanes: Map<number, AudioMixerLane>;
/**
* Over this milliseconds elapsed input will be ignored
*/
#no_input_detect_msec: number;
/**
* Under this dBFS input will be ignored
*/
#silent_threshold_dbfs: number;
/**
* Initialize with parameters setting
*
* @param {number} no_input_detect_msec No mixing what last input was over this milliseconds
* @param {number} silent_threshold_dbfs No mixing what under this dbfs audio
*
* @throws {RangeError} If `no_input_detect_sec` is negative value
* @throws {RangeError} If `silent_threshold_dbfs` is positive value
*/
constructor(no_input_detect_msec: number = 300, silent_threshold_dbfs: number = -Infinity) {
super();
// Arguments range checking
if(no_input_detect_msec < 0) {
throw new RangeError(`no_input_detect_sec must be positive, but got ${no_input_detect_msec}`);
}
if(silent_threshold_dbfs > 0.0) {
throw new RangeError(`silent_threshold_dbfs must be negative, but got ${silent_threshold_dbfs}`);
}
this.#lanes = new Map<number, AudioMixerLane>();
this.#no_input_detect_msec = no_input_detect_msec;
this.#silent_threshold_dbfs = silent_threshold_dbfs;
}
/**
* Dispatch on lane created
*
* @event AudioMixer#lane-created
* @type {MessageEvent}
* @property {LaneInfo} data LaneInfo of created lane
*/
/**
* Create a new lane to add an input
*
* @returns Lane ID what assigned to created lane
*
* @throws {MaxLanesReachedError} If lane count already reached to 256
*/
create_lane(): number {
const lane_id = this.#lookup_available_lane_id();
const new_lane = new AudioMixerLane(this.#create_silent_pcm(), "NEW", 0.0, Date.now(), -Infinity)
this.#lanes.set(lane_id, new_lane);
this.dispatchEvent(new MessageEvent<LaneInfo>("lane-created", {data: new LaneInfo(lane_id, "NEW", 0.0)}));
return lane_id;
}
/**
* Dispatch on lane name modified
*
* @event AudioMixer#lane-name-modified
* @type {MessageEvent}
* @property {LaneInfo} data LaneInfo of modified lane
*/
/**
* Input processing and return mixed audio
*
* @param lane_id Lane ID to control
* @param pcm Audio PCM to input as specified lane
*
* @returns Mixed audio PCM (without own input audio)
*/
lane_io(lane_id: number, pcm: Float32Array, lane_name: string): Float32Array {
// Argument range checking
if(pcm.length !== ONE_FRAME_SAMPLES) {
throw new RangeError(`Unexpected frame sample count - expected ${ONE_FRAME_SAMPLES}, but got ${pcm.length}`);
}
const lane = this.#try_get_lane(lane_id);
lane.pcm = pcm.map(e => e * 10 ** (lane.gain_db / 20));
lane.last_input_ts = Date.now();
lane.dbfs = dbfs_float(pcm);
// update lane_name if modified
if(lane_name !== lane.lane_name) {
lane.lane_name = lane_name;
this.dispatchEvent(new MessageEvent<LaneInfo>("lane-name-modified", {data: new LaneInfo(lane_id, lane_name, lane.gain_db)}));
}
// Audio mixing
const pcm_mixed = this.#create_silent_pcm();
const no_input_detect_msec = this.#no_input_detect_msec;
const silent_threshold_dbfs = this.#silent_threshold_dbfs;
const pcm_overlay = this.#pcm_overlay;
this.#lanes.forEach((lane_mix, lane_id_mix) => {
if(lane_id_mix === lane_id)
return; // Ignore own audio
if((Date.now() - lane_mix.last_input_ts) >= no_input_detect_msec)
return; // Ignore no recent input lane
if(lane_mix.dbfs <= silent_threshold_dbfs)
return; // Ignore silent input lane
pcm_overlay(pcm_mixed, lane_mix.pcm);
});
this.#pcm_clipping(pcm_mixed);
return pcm_mixed;
}
/**
* Get lanes status of lane name and gain
*
* @returns Current mixer status of lane name and gain
*/
get_lanes_info(): LaneInfo[] {
const lanes_info: Array<LaneInfo> = [];
this.#lanes.forEach((lane, lane_id) => {
lanes_info.push(new LaneInfo(lane_id, lane.lane_name, lane.gain_db));
});
return lanes_info;
}
/**
* Get lanes status of loudness
*
* @returns Current mixer status of loudness
*/
get_lanes_dbfs(): LaneLoudness[] {
const lanes_loudness: Array<LaneLoudness> = [];
this.#lanes.forEach((lane, lane_id) => {
lanes_loudness.push(new LaneLoudness(lane_id, lane.dbfs));
});
return lanes_loudness;
}
/**
* Set a lane gain (volume) at dB
*
* @param lane_id Lane ID to set gain
* @param gain_db Gain (in db) to set
*/
set_lane_gain_db(lane_id: number, gain_db: number): void {
const target_lane = this.#try_get_lane(lane_id);
target_lane.gain_db = gain_db;
}
/**
* Dispatch on lane deleted
*
* @event AudioMixer#lane-deleted
* @type {MessageEvent}
* @property {number} data Lane ID of deleted
*/
/**
* Delete a lane
*
* @param lane_id Lane ID to delete
*/
delete_lane(lane_id: number): void {
this.#try_get_lane(lane_id);
this.#lanes.delete(lane_id);
this.dispatchEvent(new MessageEvent<number>("lane-deleted", {data: lane_id}));
}
/**
* Create a new Float32Array as a silent audio frame
*
* @returns Silent (all 0) audio frame
*/
#create_silent_pcm(): Float32Array {
return new Float32Array(ONE_FRAME_SAMPLES);
}
/**
* 2 pcm overlaying (simple addition loop)
*
* Note: It causes mutation to argument, because of memory performance
*
* @param pcm_to_operate This array will be mutated as added value
* @param pcm_to_overlay This values add to `pcm_to_operate`
*/
#pcm_overlay(pcm_to_operate: Float32Array, pcm_to_overlay: Float32Array): void {
pcm_to_operate.forEach((_, i) => {
pcm_to_operate[i] += pcm_to_overlay[i];
});
}
/**
* PCM clipping - under -1 adjust to -1, over +1 adjust to +1
*
* Note: It causes mutation to argument, because of memory performance
*
* @param pcm PCM to process (it will be mutated to clipped PCM)
*/
#pcm_clipping(pcm: Float32Array): void {
pcm.forEach((_, i) => {
if(pcm[i] < -1) pcm[i] = -1;
if(pcm[i] > 1) pcm[i] = 1;
});
}
/**
* Return available smallest lane ID
*
* @returns Available smallest lane ID (0~255)
*
* @throws {MaxLanesReachedError} If lane count already reached to 256
*/
#lookup_available_lane_id(): number {
if(this.#lanes.size >= this.#MAX_LANE_COUNT) {
throw new MaxLanesReachedError("Already reached to maximum lane count");
}
const allowed_ids = new Set<number>();
for(let i = 0; i < this.#MAX_LANE_COUNT; i++) { allowed_ids.add(i); }
const available_ids = allowed_ids.difference(this.#lanes);
return Math.min(...available_ids);
}
/**
* Get an `AudioMixerLane` object from `lane_id`
*
* @param lane_id Lane ID to get `AudioMixerLane`
*
* @throws {RangeError} If specified lane_id is not existing
*/
#try_get_lane(lane_id: number): AudioMixerLane {
if(lane_id < 0 || lane_id >= this.#MAX_LANE_COUNT) {
throw new RangeError(`lane_id must be in 0~255, but got ${lane_id}`);
}
const lane = this.#lanes.get(lane_id);
if(lane === undefined) {
throw new NonExistingLaneIdError(`The lane id ${lane_id} is not existing`);
}
return lane;
}
}
|