@aeonentertainment/mixer 0.4.0 → 0.6.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/README.md +6 -4
- package/dist/index.d.mts +130 -18
- package/dist/index.mjs +89 -32
- package/native/binding/index.d.ts +58 -8
- package/native/binding/index.darwin-arm64.node +0 -0
- package/native/binding/index.darwin-x64.node +0 -0
- package/native/binding/index.linux-arm64-gnu.node +0 -0
- package/native/binding/index.linux-x64-gnu.node +0 -0
- package/native/binding/index.win32-x64-msvc.node +0 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@ Each [release](https://github.com/aeonentertainment/aeon-mixer/releases) carries
|
|
|
15
15
|
Depend on its URL, and the lockfile pins its hash:
|
|
16
16
|
|
|
17
17
|
```bash
|
|
18
|
-
npm install https://github.com/aeonentertainment/aeon-mixer/releases/download/v0.
|
|
18
|
+
npm install https://github.com/aeonentertainment/aeon-mixer/releases/download/v0.6.0/aeonentertainment-mixer-0.6.0.tgz
|
|
19
19
|
```
|
|
20
20
|
|
|
21
21
|
## Use
|
|
@@ -50,9 +50,11 @@ await engine.close();
|
|
|
50
50
|
|
|
51
51
|
- **Slots** are the mics: 8 per engine by default, up to 16 with `openEngine({ slots })`. `rtp`
|
|
52
52
|
takes Opus over UDP from a phone or mediasoup, `l16` takes raw PCM over loopback from a house
|
|
53
|
-
mic,
|
|
54
|
-
|
|
55
|
-
reports.
|
|
53
|
+
mic's sender, `capture` takes a mic on this machine by an id from `engine.inputDevices()`, and
|
|
54
|
+
`push` takes PCM the host hands to `mixer.push`. Each slot has gain, pan, mute, solo, reverb,
|
|
55
|
+
a pitch reading, a melody guide and pitch correction toward it, whose delay `stats()` reports.
|
|
56
|
+
A slot can also take the room's echo of the mix out of its input and duck itself when its loop
|
|
57
|
+
through the speakers starts to howl; both add no delay, and `stats()` reports what they found.
|
|
56
58
|
- **The backing** is any format Symphonia decodes, resampled to 48 kHz. The armed key and speed
|
|
57
59
|
are rendered before `start` resolves. Live, it supports seek, pause, resume, a key change and
|
|
58
60
|
stem gains, and `start`, `seek` and `resume` report the position the lyric clock follows.
|
package/dist/index.d.mts
CHANGED
|
@@ -13,15 +13,22 @@ declare class Engine$1 {
|
|
|
13
13
|
*/
|
|
14
14
|
constructor(options: OpenOptions, onEvents: (arg: Array<NativeEvent>) => void);
|
|
15
15
|
/**
|
|
16
|
-
* Builds a mixer with one slot per
|
|
17
|
-
* and l16 slots bind their ports here and report them through
|
|
18
|
-
*
|
|
16
|
+
* Builds a mixer with one slot per input, in order: `"rtp"`, `"l16"`, `"push"` or
|
|
17
|
+
* `"capture"`. The rtp and l16 slots bind their ports here and report them through
|
|
18
|
+
* `Mixer.port`, the capture slots start their inputs here, and a return stream gets its socket
|
|
19
|
+
* and send task here too.
|
|
19
20
|
*/
|
|
20
|
-
createMixer(
|
|
21
|
+
createMixer(slots: Array<SlotInput>, rtpReturn?: RtpReturnInput | undefined | null): Mixer$1;
|
|
21
22
|
/** The output playing now. After a move the events thread sends the same as `outputChanged`. */
|
|
22
23
|
get output(): OutputInfo$1;
|
|
23
24
|
/** The outputs this OS offers, the default marked. Empty on the offline and timer clocks. */
|
|
24
25
|
outputs(): Array<OutputEntry>;
|
|
26
|
+
/**
|
|
27
|
+
* The inputs this OS offers, the default marked: the devices a `capture` slot may name. Empty
|
|
28
|
+
* on the offline clock, which opens none. A box on the timer clock plays nothing locally but
|
|
29
|
+
* may still have a mic plugged in.
|
|
30
|
+
*/
|
|
31
|
+
inputs(): Array<OutputEntry>;
|
|
25
32
|
/** Offline clock only: renders `blocks` blocks and returns them as interleaved stereo. */
|
|
26
33
|
pump(blocks: number): Float32Array;
|
|
27
34
|
/** Writes one slot of the snapshot and publishes the whole of it. */
|
|
@@ -61,7 +68,7 @@ declare class Engine$1 {
|
|
|
61
68
|
declare class Mixer$1 {
|
|
62
69
|
/** Moves a finished render's stems into the graph as its backing path, before `swap`. */
|
|
63
70
|
installBacking(render: BackingRender): void;
|
|
64
|
-
/** The UDP port a slot listens on, or null for a push slot. */
|
|
71
|
+
/** The UDP port a slot listens on, or null for a push or capture slot. */
|
|
65
72
|
port(index: number): number | null;
|
|
66
73
|
/**
|
|
67
74
|
* Hands interleaved stereo PCM to a push slot, which folds it to mono as every slot is
|
|
@@ -73,7 +80,10 @@ declare class Mixer$1 {
|
|
|
73
80
|
* recording it for null.
|
|
74
81
|
*/
|
|
75
82
|
recordSlot(index: number, path?: string | undefined | null): void;
|
|
76
|
-
/**
|
|
83
|
+
/**
|
|
84
|
+
* Every slot's ingest counters, corrector delay and echo findings, as the audio thread last
|
|
85
|
+
* published them.
|
|
86
|
+
*/
|
|
77
87
|
stats(): Array<SlotStatsInfo>;
|
|
78
88
|
/** The return stream's counters, or null for a mixer without one. */
|
|
79
89
|
returnStats(): RtpReturnStats | null;
|
|
@@ -91,11 +101,23 @@ interface BackingInput {
|
|
|
91
101
|
/** The armed speed factor, rendered before start. */
|
|
92
102
|
speed: number;
|
|
93
103
|
}
|
|
104
|
+
interface CleanupInput {
|
|
105
|
+
enabled: boolean;
|
|
106
|
+
}
|
|
94
107
|
interface CorrectorInput {
|
|
95
108
|
enabled: boolean;
|
|
96
109
|
strength: number;
|
|
97
110
|
retuneMs: number;
|
|
98
111
|
}
|
|
112
|
+
/** What a slot's echo canceller has found, as JavaScript reads it. */
|
|
113
|
+
interface EchoInfo {
|
|
114
|
+
/** From the master being rendered to its echo reaching the slot. */
|
|
115
|
+
pathMs: number;
|
|
116
|
+
/** How far under the master the echo arrives, over the last second. */
|
|
117
|
+
bleedDb: number;
|
|
118
|
+
/** How much quieter the slot came out than it went in, over the last second. */
|
|
119
|
+
removedDb: number;
|
|
120
|
+
}
|
|
99
121
|
interface GuideInput {
|
|
100
122
|
notes: Array<NoteInput>;
|
|
101
123
|
/** The key's pitch classes as bits, C in bit 0. */
|
|
@@ -155,8 +177,11 @@ interface OpenOptions {
|
|
|
155
177
|
/** A device clock's output: an id from `outputs` to pin, or none to follow the default output. */
|
|
156
178
|
device?: string;
|
|
157
179
|
periodFrames: number;
|
|
158
|
-
/**
|
|
159
|
-
|
|
180
|
+
/**
|
|
181
|
+
* Pins the jitter buffer's playout delay for every `rtp` and `l16` slot. Without it, each
|
|
182
|
+
* stream chooses its own.
|
|
183
|
+
*/
|
|
184
|
+
playoutDelayFrames?: number;
|
|
160
185
|
}
|
|
161
186
|
interface OutputDevice {
|
|
162
187
|
id: string;
|
|
@@ -192,6 +217,15 @@ interface RtpReturnStats {
|
|
|
192
217
|
/** Blocks the audio thread dropped because the network thread had not drained the ring. */
|
|
193
218
|
droppedBlocks: number;
|
|
194
219
|
}
|
|
220
|
+
/**
|
|
221
|
+
* One slot as `createMixer` asks for it. `device` and `channel` belong to a `capture` slot: an id
|
|
222
|
+
* from `inputs`, or the default input, and the device's channel from 0.
|
|
223
|
+
*/
|
|
224
|
+
interface SlotInput {
|
|
225
|
+
kind: string;
|
|
226
|
+
device?: string;
|
|
227
|
+
channel?: number;
|
|
228
|
+
}
|
|
195
229
|
interface SlotParamsInput {
|
|
196
230
|
/** Linear amplitude. */
|
|
197
231
|
gain: number;
|
|
@@ -202,6 +236,9 @@ interface SlotParamsInput {
|
|
|
202
236
|
corrector: CorrectorInput;
|
|
203
237
|
reverb: ReverbInput;
|
|
204
238
|
voice: VoiceInput;
|
|
239
|
+
echoCancel: SwitchInput;
|
|
240
|
+
cleanup: CleanupInput;
|
|
241
|
+
howlGuard: SwitchInput;
|
|
205
242
|
guideOffsetMs: number;
|
|
206
243
|
}
|
|
207
244
|
/** `SlotStats` as JavaScript reads it. */
|
|
@@ -209,16 +246,25 @@ interface SlotStatsInfo {
|
|
|
209
246
|
pushed: number;
|
|
210
247
|
lost: number;
|
|
211
248
|
late: number;
|
|
249
|
+
lateAtOpening: number;
|
|
212
250
|
concealed: number;
|
|
213
251
|
resyncs: number;
|
|
214
252
|
jitterMs: number;
|
|
215
253
|
correctorDelayMs: number;
|
|
216
254
|
correctorMaxDelayMs: number;
|
|
255
|
+
/** Null while the canceller is off or has yet to find and learn the room's echo. */
|
|
256
|
+
echo: EchoInfo | null;
|
|
257
|
+
playoutDelayMs: number;
|
|
258
|
+
howlCatches: number;
|
|
217
259
|
}
|
|
218
260
|
interface StemGainsInput {
|
|
219
261
|
instrumental: number;
|
|
220
262
|
guide: number;
|
|
221
263
|
}
|
|
264
|
+
/** An effect with no knob but on and off. */
|
|
265
|
+
interface SwitchInput {
|
|
266
|
+
enabled: boolean;
|
|
267
|
+
}
|
|
222
268
|
interface VoiceInput {
|
|
223
269
|
enabled: boolean;
|
|
224
270
|
formantSemitones: number;
|
|
@@ -227,7 +273,7 @@ declare const NativeEngine: typeof Engine$1;
|
|
|
227
273
|
type NativeEngine = Engine$1;
|
|
228
274
|
type NativeMixer = Mixer$1;
|
|
229
275
|
type Capability = "guideVocal" | "key" | "speed" | "seek" | "shortVersion" | "pause" | "level";
|
|
230
|
-
type IngestKind = "rtp" | "l16" | "push";
|
|
276
|
+
type IngestKind = "rtp" | "l16" | "push" | "capture";
|
|
231
277
|
type StemKind = "instrumental" | "guideVocals";
|
|
232
278
|
interface OpenEngineOptions {
|
|
233
279
|
/**
|
|
@@ -245,7 +291,10 @@ interface OpenEngineOptions {
|
|
|
245
291
|
};
|
|
246
292
|
/** Fixed for the engine's life, default 8, at most 16. */
|
|
247
293
|
slots?: number;
|
|
248
|
-
/**
|
|
294
|
+
/**
|
|
295
|
+
* Pins the ingest jitter buffer to this delay. Leave it out and each slot chooses its own from
|
|
296
|
+
* what its stream shows, opening at 15; `stats()` reports where each one is.
|
|
297
|
+
*/
|
|
249
298
|
playoutDelayMs?: number;
|
|
250
299
|
}
|
|
251
300
|
interface OutputInfo {
|
|
@@ -258,10 +307,20 @@ interface OutputInfo {
|
|
|
258
307
|
readonly name: string;
|
|
259
308
|
} | null;
|
|
260
309
|
}
|
|
310
|
+
/**
|
|
311
|
+
* One slot as a mixer asks for it. A `capture` slot is an input on this machine, a house mic:
|
|
312
|
+
* `device` is an id from `inputDevices()`, the default input when left out, and `channel` the
|
|
313
|
+
* device's channel from 0, default 0.
|
|
314
|
+
*/
|
|
315
|
+
type SlotConfig = {
|
|
316
|
+
kind: Exclude<IngestKind, "capture">;
|
|
317
|
+
} | {
|
|
318
|
+
kind: "capture";
|
|
319
|
+
device?: string;
|
|
320
|
+
channel?: number;
|
|
321
|
+
};
|
|
261
322
|
interface MixerConfig {
|
|
262
|
-
slots: ReadonlyArray<
|
|
263
|
-
kind: IngestKind;
|
|
264
|
-
}>;
|
|
323
|
+
slots: ReadonlyArray<SlotConfig>;
|
|
265
324
|
backing?: {
|
|
266
325
|
stems: ReadonlyArray<{
|
|
267
326
|
kind: StemKind;
|
|
@@ -308,6 +367,24 @@ type SlotEffect = {
|
|
|
308
367
|
kind: "voice";
|
|
309
368
|
enabled: boolean;
|
|
310
369
|
formantSemitones: number;
|
|
370
|
+
} |
|
|
371
|
+
/** Takes the room's echo of the mix out of the slot's input, with no added delay. */
|
|
372
|
+
{
|
|
373
|
+
kind: "echo-cancel";
|
|
374
|
+
enabled: boolean;
|
|
375
|
+
} |
|
|
376
|
+
/**
|
|
377
|
+
* A high-pass and gate ahead of the corrector and a compressor after the meter. None reads
|
|
378
|
+
* ahead or buffers; decision 0012 has their numbers and costs.
|
|
379
|
+
*/
|
|
380
|
+
{
|
|
381
|
+
kind: "cleanup";
|
|
382
|
+
enabled: boolean;
|
|
383
|
+
} |
|
|
384
|
+
/** Ducks the slot when its loop through the speakers starts to howl, with no added delay. */
|
|
385
|
+
{
|
|
386
|
+
kind: "howl-guard";
|
|
387
|
+
enabled: boolean;
|
|
311
388
|
};
|
|
312
389
|
interface MelodyGuide {
|
|
313
390
|
notes: ReadonlyArray<{
|
|
@@ -322,6 +399,8 @@ interface SlotStats {
|
|
|
322
399
|
pushed: number;
|
|
323
400
|
lost: number;
|
|
324
401
|
late: number;
|
|
402
|
+
/** Of `late`, the packets that would also have been late had the delay stayed where it opened. */
|
|
403
|
+
lateAtOpening: number;
|
|
325
404
|
concealed: number;
|
|
326
405
|
resyncs: number;
|
|
327
406
|
jitterMs: number;
|
|
@@ -332,6 +411,25 @@ interface SlotStats {
|
|
|
332
411
|
correctorDelayMs: number;
|
|
333
412
|
/** The most that delay can reach; 0 with correction off. It holds still, so a budget adds this. */
|
|
334
413
|
correctorMaxDelayMs: number;
|
|
414
|
+
/**
|
|
415
|
+
* What the echo canceller found: `pathMs` from the mix being rendered to its echo reaching the
|
|
416
|
+
* slot, `bleedDb` how far under the mix that echo arrives, `removedDb` how much quieter the slot
|
|
417
|
+
* came out than it went in, both over the last second. Null while it is off, or until it has
|
|
418
|
+
* found the echo and learned it.
|
|
419
|
+
*/
|
|
420
|
+
echo: {
|
|
421
|
+
pathMs: number;
|
|
422
|
+
bleedDb: number;
|
|
423
|
+
removedDb: number;
|
|
424
|
+
} | null;
|
|
425
|
+
/**
|
|
426
|
+
* The delay the stream's earliest packets wait now; zero for a `push` slot. A `capture` slot
|
|
427
|
+
* has no jitter buffer: this is how long its mic's samples wait from the input to the mix, the
|
|
428
|
+
* input's period included, and zero until it first plays.
|
|
429
|
+
*/
|
|
430
|
+
playoutDelayMs: number;
|
|
431
|
+
/** How often the howl guard has ducked this slot. */
|
|
432
|
+
howlCatches: number;
|
|
335
433
|
}
|
|
336
434
|
type EngineEvents = {
|
|
337
435
|
fault: (error: Error) => void;
|
|
@@ -393,25 +491,33 @@ interface MixerHost extends RenderHost {
|
|
|
393
491
|
swapOut(mixer: Mixer): Promise<void>;
|
|
394
492
|
/** Hears the engine's fault until the returned function is called. */
|
|
395
493
|
onFault(listener: (error: Error) => void): () => void;
|
|
494
|
+
/**
|
|
495
|
+
* Holds `release` until the returned function is called, and calls it when the engine closes:
|
|
496
|
+
* a mixer's capture inputs and ports outlive no engine, whatever state the mixer is in.
|
|
497
|
+
*/
|
|
498
|
+
hold(release: () => void): () => void;
|
|
396
499
|
isLive(mixer: Mixer): boolean;
|
|
397
500
|
anyLive(): boolean;
|
|
398
501
|
underruns(): number;
|
|
399
502
|
}
|
|
400
503
|
/**
|
|
401
|
-
* One mixing graph for one performance. Its `rtp` and `l16` slots listen on their ports
|
|
402
|
-
* here until `destroy`; its return stream sends the
|
|
504
|
+
* One mixing graph for one performance. Its `rtp` and `l16` slots listen on their ports, and its
|
|
505
|
+
* `capture` slots hold their inputs open, from here until `destroy`; its return stream sends the
|
|
506
|
+
* mix from `start` until `destroy`. Settings
|
|
403
507
|
* made while armed are kept here and written to the engine when `start` swaps the graph in, so
|
|
404
508
|
* arming the next song never touches the live one.
|
|
405
509
|
*/
|
|
406
510
|
export declare class Mixer {
|
|
407
511
|
#private;
|
|
408
|
-
/** Each slot's kind and, for `rtp` and `l16`, the UDP port it listens on; `null` for
|
|
512
|
+
/** Each slot's kind and, for `rtp` and `l16`, the UDP port it listens on; `null` for the rest. */
|
|
409
513
|
readonly slots: ReadonlyArray<{
|
|
410
514
|
index: number;
|
|
411
515
|
kind: IngestKind;
|
|
412
516
|
port: number | null;
|
|
413
517
|
}>;
|
|
414
|
-
constructor(host: MixerHost,
|
|
518
|
+
constructor(host: MixerHost, inputs: ReadonlyArray<SlotInput & {
|
|
519
|
+
kind: IngestKind;
|
|
520
|
+
}>, events: Emitter<MixerEvents>, backing: BackingConfig | null, rtpReturn: RtpReturnInput | null);
|
|
415
521
|
setSlot(index: number, settings: Partial<SlotSettings>): void;
|
|
416
522
|
/** The slot's whole chain: an effect not in `effects` is disabled. */
|
|
417
523
|
setSlotEffects(index: number, effects: ReadonlyArray<SlotEffect>): void;
|
|
@@ -510,9 +616,15 @@ export declare class Engine {
|
|
|
510
616
|
name: string;
|
|
511
617
|
isDefault: boolean;
|
|
512
618
|
}>>;
|
|
619
|
+
/** The inputs a `capture` slot may name, the default marked. None on an offline engine. */
|
|
620
|
+
inputDevices(): Promise<ReadonlyArray<{
|
|
621
|
+
id: string;
|
|
622
|
+
name: string;
|
|
623
|
+
isDefault: boolean;
|
|
624
|
+
}>>;
|
|
513
625
|
createMixer(config: MixerConfig): Promise<Mixer>;
|
|
514
626
|
on<K extends keyof EngineEvents>(event: K, listener: EngineEvents[K]): () => void;
|
|
515
627
|
/** Destroys the live mixer, stops the clock and joins the events thread. */
|
|
516
628
|
close(): Promise<void>;
|
|
517
629
|
}
|
|
518
|
-
export type { Capability, EngineEvents, IngestKind, MelodyGuide, MixerConfig, MixerEvents, OfflineEngine, OpenEngineOptions, OutputInfo, SlotEffect, SlotSettings, SlotStats, StemKind };
|
|
630
|
+
export type { Capability, EngineEvents, IngestKind, MelodyGuide, MixerConfig, MixerEvents, OfflineEngine, OpenEngineOptions, OutputInfo, SlotConfig, SlotEffect, SlotSettings, SlotStats, StemKind };
|
package/dist/index.mjs
CHANGED
|
@@ -72,14 +72,27 @@ function framesToMs(frames) {
|
|
|
72
72
|
const INGEST_KINDS = [
|
|
73
73
|
"rtp",
|
|
74
74
|
"l16",
|
|
75
|
-
"push"
|
|
75
|
+
"push",
|
|
76
|
+
"capture"
|
|
76
77
|
];
|
|
77
78
|
/** A kind as a JavaScript caller may have spelt it, so an unknown one never crosses. */
|
|
78
79
|
function ingestKind(name, value) {
|
|
79
80
|
const kind = INGEST_KINDS.find((known) => known === value);
|
|
80
|
-
if (kind === void 0) throw new RangeError(`${name} must be rtp, l16 or
|
|
81
|
+
if (kind === void 0) throw new RangeError(`${name} must be rtp, l16, push or capture, got ${value}`);
|
|
81
82
|
return kind;
|
|
82
83
|
}
|
|
84
|
+
/** A slot as the addon takes it: its kind, and for a `capture` slot its device and channel. */
|
|
85
|
+
function slotInput(name, slot) {
|
|
86
|
+
const kind = ingestKind(`${name}.kind`, slot.kind);
|
|
87
|
+
if (slot.kind !== "capture") return { kind };
|
|
88
|
+
const { device, channel = 0 } = slot;
|
|
89
|
+
if (device !== void 0 && (typeof device !== "string" || device === "")) throw new TypeError(`${name}.device must be an id from inputDevices()`);
|
|
90
|
+
return {
|
|
91
|
+
kind,
|
|
92
|
+
channel: integerIn(`${name}.channel`, channel, 0, 255),
|
|
93
|
+
...device === void 0 ? {} : { device }
|
|
94
|
+
};
|
|
95
|
+
}
|
|
83
96
|
/**
|
|
84
97
|
* Where the mix goes: an IP literal, since resolving a name would block the control thread, and a
|
|
85
98
|
* dynamic RTP payload type, since Opus has no static one.
|
|
@@ -108,6 +121,20 @@ function pcm(name, value, channels) {
|
|
|
108
121
|
return value;
|
|
109
122
|
}
|
|
110
123
|
// src/backing.ts
|
|
124
|
+
/**
|
|
125
|
+
* `config` checked, before anything opens: a mixer runs this ahead of its native mixer, so bad
|
|
126
|
+
* backing settings reject `createMixer` with no port bound and no input held.
|
|
127
|
+
*/
|
|
128
|
+
function backingSettings(config) {
|
|
129
|
+
return {
|
|
130
|
+
input: toInput(config),
|
|
131
|
+
stopAtFrames: config.stopAtMs === void 0 ? void 0 : msToFrames(finiteIn("stopAtMs", config.stopAtMs, 0, Number.MAX_SAFE_INTEGER)),
|
|
132
|
+
stemGains: {
|
|
133
|
+
instrumental: finiteIn("stemGains.instrumental", config.stemGains.instrumental, 0, 4),
|
|
134
|
+
guide: finiteIn("stemGains.guideVocals", config.stemGains.guideVocals, 0, 4)
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
}
|
|
111
138
|
/** `stopAtMs` fades the stems out over this before falling silent: docs/design.md section 4. */
|
|
112
139
|
const STOP_FADE_MS = 3e3;
|
|
113
140
|
/**
|
|
@@ -130,18 +157,15 @@ var Backing = class {
|
|
|
130
157
|
#render;
|
|
131
158
|
#graph;
|
|
132
159
|
/**
|
|
133
|
-
*
|
|
160
|
+
* Starts the render of checked `settings` on the worker thread. The stems move into `graph` as
|
|
134
161
|
* soon as the job is done, so a render that fails is reported then and not at `start`.
|
|
135
162
|
*/
|
|
136
|
-
constructor(host,
|
|
163
|
+
constructor(host, settings, graph, report) {
|
|
137
164
|
this.#graph = graph;
|
|
138
|
-
const input =
|
|
165
|
+
const { input, stopAtFrames, stemGains } = settings;
|
|
139
166
|
this.speed = input.speed;
|
|
140
|
-
this.stopAtFrames =
|
|
141
|
-
this.#stemGains =
|
|
142
|
-
instrumental: finiteIn("stemGains.instrumental", config.stemGains.instrumental, 0, 4),
|
|
143
|
-
guide: finiteIn("stemGains.guideVocals", config.stemGains.guideVocals, 0, 4)
|
|
144
|
-
};
|
|
167
|
+
this.stopAtFrames = stopAtFrames;
|
|
168
|
+
this.#stemGains = stemGains;
|
|
145
169
|
this.#render = host.native.startRender(input);
|
|
146
170
|
this.rendered = host.watchRender(this.#render.id, (fraction) => {
|
|
147
171
|
this.#fraction = fraction;
|
|
@@ -234,17 +258,21 @@ function defaultSlotParams() {
|
|
|
234
258
|
enabled: false,
|
|
235
259
|
formantSemitones: 0
|
|
236
260
|
},
|
|
261
|
+
echoCancel: { enabled: false },
|
|
262
|
+
cleanup: { enabled: false },
|
|
263
|
+
howlGuard: { enabled: false },
|
|
237
264
|
guideOffsetMs: 0
|
|
238
265
|
};
|
|
239
266
|
}
|
|
240
267
|
/**
|
|
241
|
-
* One mixing graph for one performance. Its `rtp` and `l16` slots listen on their ports
|
|
242
|
-
* here until `destroy`; its return stream sends the
|
|
268
|
+
* One mixing graph for one performance. Its `rtp` and `l16` slots listen on their ports, and its
|
|
269
|
+
* `capture` slots hold their inputs open, from here until `destroy`; its return stream sends the
|
|
270
|
+
* mix from `start` until `destroy`. Settings
|
|
243
271
|
* made while armed are kept here and written to the engine when `start` swaps the graph in, so
|
|
244
272
|
* arming the next song never touches the live one.
|
|
245
273
|
*/
|
|
246
274
|
var Mixer = class {
|
|
247
|
-
/** Each slot's kind and, for `rtp` and `l16`, the UDP port it listens on; `null` for
|
|
275
|
+
/** Each slot's kind and, for `rtp` and `l16`, the UDP port it listens on; `null` for the rest. */
|
|
248
276
|
slots;
|
|
249
277
|
#host;
|
|
250
278
|
#native;
|
|
@@ -253,6 +281,7 @@ var Mixer = class {
|
|
|
253
281
|
#guides;
|
|
254
282
|
#backing;
|
|
255
283
|
#offFault;
|
|
284
|
+
#letGo;
|
|
256
285
|
#phase = "armed";
|
|
257
286
|
#failed = false;
|
|
258
287
|
/**
|
|
@@ -264,18 +293,20 @@ var Mixer = class {
|
|
|
264
293
|
this.#failed = true;
|
|
265
294
|
this.#events.emit("error", error);
|
|
266
295
|
};
|
|
267
|
-
constructor(host,
|
|
296
|
+
constructor(host, inputs, events, backing, rtpReturn) {
|
|
268
297
|
this.#host = host;
|
|
269
298
|
this.#events = events;
|
|
270
|
-
|
|
271
|
-
this
|
|
299
|
+
const settings = backing === null ? null : backingSettings(backing);
|
|
300
|
+
this.#native = host.native.createMixer([...inputs], rtpReturn);
|
|
301
|
+
this.#letGo = host.hold(() => this.#release());
|
|
302
|
+
this.slots = inputs.map(({ kind }, index) => ({
|
|
272
303
|
index,
|
|
273
304
|
kind,
|
|
274
305
|
port: this.#native.port(index)
|
|
275
306
|
}));
|
|
276
|
-
this.#params =
|
|
277
|
-
this.#guides =
|
|
278
|
-
this.#backing =
|
|
307
|
+
this.#params = inputs.map(defaultSlotParams);
|
|
308
|
+
this.#guides = inputs.map(() => null);
|
|
309
|
+
this.#backing = settings === null ? null : new Backing(host, settings, this.#native, {
|
|
279
310
|
progress: (fraction) => {
|
|
280
311
|
if (this.#phase !== "destroyed") this.#events.emit("renderProgress", fraction);
|
|
281
312
|
},
|
|
@@ -316,10 +347,19 @@ var Mixer = class {
|
|
|
316
347
|
mix: finiteIn("mix", effect.mix, 0, 1)
|
|
317
348
|
};
|
|
318
349
|
break;
|
|
319
|
-
case "voice":
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
350
|
+
case "voice":
|
|
351
|
+
next.voice = {
|
|
352
|
+
enabled: effect.enabled,
|
|
353
|
+
formantSemitones: finiteIn("formantSemitones", effect.formantSemitones, -12, 12)
|
|
354
|
+
};
|
|
355
|
+
break;
|
|
356
|
+
case "echo-cancel":
|
|
357
|
+
next.echoCancel = { enabled: effect.enabled };
|
|
358
|
+
break;
|
|
359
|
+
case "cleanup":
|
|
360
|
+
next.cleanup = { enabled: effect.enabled };
|
|
361
|
+
break;
|
|
362
|
+
case "howl-guard": next.howlGuard = { enabled: effect.enabled };
|
|
323
363
|
}
|
|
324
364
|
this.#write(slot, next);
|
|
325
365
|
}
|
|
@@ -351,7 +391,8 @@ var Mixer = class {
|
|
|
351
391
|
*/
|
|
352
392
|
recordSlot(index, path) {
|
|
353
393
|
const slot = this.#slot(index);
|
|
354
|
-
|
|
394
|
+
const kind = this.slots[slot]?.kind;
|
|
395
|
+
if (kind !== "rtp" && kind !== "l16") throw new Error(`slot ${slot} is a ${String(kind)} slot; only rtp and l16 slots record`);
|
|
355
396
|
if (this.#phase === "destroyed") throw new Error("the mixer is destroyed");
|
|
356
397
|
if (path === "") throw new RangeError("path must name a file");
|
|
357
398
|
this.#native.recordSlot(slot, path);
|
|
@@ -473,6 +514,7 @@ var Mixer = class {
|
|
|
473
514
|
#release() {
|
|
474
515
|
this.#phase = "destroyed";
|
|
475
516
|
this.#offFault();
|
|
517
|
+
this.#letGo();
|
|
476
518
|
this.#native.release();
|
|
477
519
|
}
|
|
478
520
|
#command(send) {
|
|
@@ -498,11 +540,14 @@ var Mixer = class {
|
|
|
498
540
|
}
|
|
499
541
|
};
|
|
500
542
|
function defaultEffects() {
|
|
501
|
-
const { corrector, reverb, voice } = defaultSlotParams();
|
|
543
|
+
const { corrector, reverb, voice, cleanup, echoCancel, howlGuard } = defaultSlotParams();
|
|
502
544
|
return {
|
|
503
545
|
corrector,
|
|
504
546
|
reverb,
|
|
505
|
-
voice
|
|
547
|
+
voice,
|
|
548
|
+
cleanup,
|
|
549
|
+
echoCancel,
|
|
550
|
+
howlGuard
|
|
506
551
|
};
|
|
507
552
|
}
|
|
508
553
|
function toGuideInput(guide) {
|
|
@@ -544,6 +589,8 @@ var Engine = class {
|
|
|
544
589
|
#events = new Emitter();
|
|
545
590
|
/** Swaps in flight, oldest first; a `retired` event completes the oldest. */
|
|
546
591
|
#retirements = [];
|
|
592
|
+
/** Each mixer's release until it is destroyed; `close` calls whatever is left. */
|
|
593
|
+
#held = /* @__PURE__ */ new Set();
|
|
547
594
|
/** Render jobs on the worker thread, by the id their progress events carry. */
|
|
548
595
|
#renders = /* @__PURE__ */ new Map();
|
|
549
596
|
#live = null;
|
|
@@ -551,7 +598,7 @@ var Engine = class {
|
|
|
551
598
|
#state = "running";
|
|
552
599
|
constructor(options) {
|
|
553
600
|
this.#slots = integerIn("slots", options.slots ?? 8, 1, 16);
|
|
554
|
-
const
|
|
601
|
+
const pinned = options.playoutDelayMs === void 0 ? {} : { playoutDelayFrames: msToFrames(finiteIn("playoutDelayMs", options.playoutDelayMs, 5, 100)) };
|
|
555
602
|
const deliver = (batch) => {
|
|
556
603
|
this.#deliver(batch);
|
|
557
604
|
};
|
|
@@ -559,13 +606,13 @@ var Engine = class {
|
|
|
559
606
|
this.#native = new NativeEngine({
|
|
560
607
|
clock: "offline",
|
|
561
608
|
periodFrames: 128,
|
|
562
|
-
|
|
609
|
+
...pinned
|
|
563
610
|
}, deliver);
|
|
564
611
|
this.pump = (blocks) => this.#native.pump(integerIn("blocks", blocks, 1, 1 << 20));
|
|
565
612
|
} else if ("timer" in options.output) this.#native = new NativeEngine({
|
|
566
613
|
clock: "timer",
|
|
567
614
|
periodFrames: 128,
|
|
568
|
-
|
|
615
|
+
...pinned
|
|
569
616
|
}, deliver);
|
|
570
617
|
else {
|
|
571
618
|
const { device, periodFrames = 128 } = options.output;
|
|
@@ -573,7 +620,7 @@ var Engine = class {
|
|
|
573
620
|
this.#native = new NativeEngine({
|
|
574
621
|
clock: "device",
|
|
575
622
|
periodFrames,
|
|
576
|
-
|
|
623
|
+
...pinned,
|
|
577
624
|
...device === void 0 ? {} : { device }
|
|
578
625
|
}, deliver);
|
|
579
626
|
}
|
|
@@ -592,6 +639,10 @@ var Engine = class {
|
|
|
592
639
|
swapIn: (mixer, graph, events, onTimeout) => this.#swapIn(mixer, graph, events, onTimeout),
|
|
593
640
|
swapOut: (mixer) => this.#swapOut(mixer),
|
|
594
641
|
onFault: (listener) => this.#events.on("fault", listener),
|
|
642
|
+
hold: (release) => {
|
|
643
|
+
this.#held.add(release);
|
|
644
|
+
return () => this.#held.delete(release);
|
|
645
|
+
},
|
|
595
646
|
isLive: (mixer) => this.#live?.mixer === mixer,
|
|
596
647
|
anyLive: () => this.#live !== null,
|
|
597
648
|
underruns: () => this.#underruns
|
|
@@ -615,12 +666,17 @@ var Engine = class {
|
|
|
615
666
|
this.#assertRunning();
|
|
616
667
|
return this.#native.outputs();
|
|
617
668
|
}
|
|
669
|
+
/** The inputs a `capture` slot may name, the default marked. None on an offline engine. */
|
|
670
|
+
async inputDevices() {
|
|
671
|
+
this.#assertRunning();
|
|
672
|
+
return this.#native.inputs();
|
|
673
|
+
}
|
|
618
674
|
async createMixer(config) {
|
|
619
675
|
this.#assertRunning();
|
|
620
676
|
if (config.slots.length > this.#slots) throw new RangeError(`the engine has ${this.#slots} slots; the mixer asks for ${config.slots.length}`);
|
|
621
|
-
const
|
|
677
|
+
const slots = config.slots.map((slot, i) => slotInput(`slots[${i}]`, slot));
|
|
622
678
|
const destination = config.rtpReturn === void 0 ? null : rtpReturn(config.rtpReturn);
|
|
623
|
-
return new Mixer(this.#host,
|
|
679
|
+
return new Mixer(this.#host, slots, new Emitter(), config.backing ?? null, destination);
|
|
624
680
|
}
|
|
625
681
|
on(event, listener) {
|
|
626
682
|
return this.#events.on(event, listener);
|
|
@@ -631,6 +687,7 @@ var Engine = class {
|
|
|
631
687
|
if (this.#live && this.#state === "running") await this.#live.mixer.destroy();
|
|
632
688
|
this.#state = "closed";
|
|
633
689
|
this.#native.close();
|
|
690
|
+
for (const release of this.#held) release();
|
|
634
691
|
this.#stop(/* @__PURE__ */ new Error("the engine is closed"));
|
|
635
692
|
}
|
|
636
693
|
#swapIn(mixer, graph, events, onTimeout) {
|
|
@@ -25,15 +25,22 @@ export declare class Engine {
|
|
|
25
25
|
*/
|
|
26
26
|
constructor(options: OpenOptions, onEvents: (arg: Array<NativeEvent>) => void)
|
|
27
27
|
/**
|
|
28
|
-
* Builds a mixer with one slot per
|
|
29
|
-
* and l16 slots bind their ports here and report them through
|
|
30
|
-
*
|
|
28
|
+
* Builds a mixer with one slot per input, in order: `"rtp"`, `"l16"`, `"push"` or
|
|
29
|
+
* `"capture"`. The rtp and l16 slots bind their ports here and report them through
|
|
30
|
+
* `Mixer.port`, the capture slots start their inputs here, and a return stream gets its socket
|
|
31
|
+
* and send task here too.
|
|
31
32
|
*/
|
|
32
|
-
createMixer(
|
|
33
|
+
createMixer(slots: Array<SlotInput>, rtpReturn?: RtpReturnInput | undefined | null): Mixer
|
|
33
34
|
/** The output playing now. After a move the events thread sends the same as `outputChanged`. */
|
|
34
35
|
get output(): OutputInfo
|
|
35
36
|
/** The outputs this OS offers, the default marked. Empty on the offline and timer clocks. */
|
|
36
37
|
outputs(): Array<OutputEntry>
|
|
38
|
+
/**
|
|
39
|
+
* The inputs this OS offers, the default marked: the devices a `capture` slot may name. Empty
|
|
40
|
+
* on the offline clock, which opens none. A box on the timer clock plays nothing locally but
|
|
41
|
+
* may still have a mic plugged in.
|
|
42
|
+
*/
|
|
43
|
+
inputs(): Array<OutputEntry>
|
|
37
44
|
/** Offline clock only: renders `blocks` blocks and returns them as interleaved stereo. */
|
|
38
45
|
pump(blocks: number): Float32Array
|
|
39
46
|
/** Writes one slot of the snapshot and publishes the whole of it. */
|
|
@@ -74,7 +81,7 @@ export declare class Engine {
|
|
|
74
81
|
export declare class Mixer {
|
|
75
82
|
/** Moves a finished render's stems into the graph as its backing path, before `swap`. */
|
|
76
83
|
installBacking(render: BackingRender): void
|
|
77
|
-
/** The UDP port a slot listens on, or null for a push slot. */
|
|
84
|
+
/** The UDP port a slot listens on, or null for a push or capture slot. */
|
|
78
85
|
port(index: number): number | null
|
|
79
86
|
/**
|
|
80
87
|
* Hands interleaved stereo PCM to a push slot, which folds it to mono as every slot is
|
|
@@ -86,7 +93,10 @@ export declare class Mixer {
|
|
|
86
93
|
* recording it for null.
|
|
87
94
|
*/
|
|
88
95
|
recordSlot(index: number, path?: string | undefined | null): void
|
|
89
|
-
/**
|
|
96
|
+
/**
|
|
97
|
+
* Every slot's ingest counters, corrector delay and echo findings, as the audio thread last
|
|
98
|
+
* published them.
|
|
99
|
+
*/
|
|
90
100
|
stats(): Array<SlotStatsInfo>
|
|
91
101
|
/** The return stream's counters, or null for a mixer without one. */
|
|
92
102
|
returnStats(): RtpReturnStats | null
|
|
@@ -106,6 +116,10 @@ export interface BackingInput {
|
|
|
106
116
|
speed: number
|
|
107
117
|
}
|
|
108
118
|
|
|
119
|
+
export interface CleanupInput {
|
|
120
|
+
enabled: boolean
|
|
121
|
+
}
|
|
122
|
+
|
|
109
123
|
/** What drives the audio thread. */
|
|
110
124
|
export declare const enum ClockKind {
|
|
111
125
|
/** The offline driver, pumped from JavaScript. */
|
|
@@ -122,6 +136,16 @@ export interface CorrectorInput {
|
|
|
122
136
|
retuneMs: number
|
|
123
137
|
}
|
|
124
138
|
|
|
139
|
+
/** What a slot's echo canceller has found, as JavaScript reads it. */
|
|
140
|
+
export interface EchoInfo {
|
|
141
|
+
/** From the master being rendered to its echo reaching the slot. */
|
|
142
|
+
pathMs: number
|
|
143
|
+
/** How far under the master the echo arrives, over the last second. */
|
|
144
|
+
bleedDb: number
|
|
145
|
+
/** How much quieter the slot came out than it went in, over the last second. */
|
|
146
|
+
removedDb: number
|
|
147
|
+
}
|
|
148
|
+
|
|
125
149
|
export interface GuideInput {
|
|
126
150
|
notes: Array<NoteInput>
|
|
127
151
|
/** The key's pitch classes as bits, C in bit 0. */
|
|
@@ -153,8 +177,11 @@ export interface OpenOptions {
|
|
|
153
177
|
/** A device clock's output: an id from `outputs` to pin, or none to follow the default output. */
|
|
154
178
|
device?: string
|
|
155
179
|
periodFrames: number
|
|
156
|
-
/**
|
|
157
|
-
|
|
180
|
+
/**
|
|
181
|
+
* Pins the jitter buffer's playout delay for every `rtp` and `l16` slot. Without it, each
|
|
182
|
+
* stream chooses its own.
|
|
183
|
+
*/
|
|
184
|
+
playoutDelayFrames?: number
|
|
158
185
|
}
|
|
159
186
|
|
|
160
187
|
export interface OutputDevice {
|
|
@@ -197,6 +224,16 @@ export interface RtpReturnStats {
|
|
|
197
224
|
droppedBlocks: number
|
|
198
225
|
}
|
|
199
226
|
|
|
227
|
+
/**
|
|
228
|
+
* One slot as `createMixer` asks for it. `device` and `channel` belong to a `capture` slot: an id
|
|
229
|
+
* from `inputs`, or the default input, and the device's channel from 0.
|
|
230
|
+
*/
|
|
231
|
+
export interface SlotInput {
|
|
232
|
+
kind: string
|
|
233
|
+
device?: string
|
|
234
|
+
channel?: number
|
|
235
|
+
}
|
|
236
|
+
|
|
200
237
|
export interface SlotParamsInput {
|
|
201
238
|
/** Linear amplitude. */
|
|
202
239
|
gain: number
|
|
@@ -207,6 +244,9 @@ export interface SlotParamsInput {
|
|
|
207
244
|
corrector: CorrectorInput
|
|
208
245
|
reverb: ReverbInput
|
|
209
246
|
voice: VoiceInput
|
|
247
|
+
echoCancel: SwitchInput
|
|
248
|
+
cleanup: CleanupInput
|
|
249
|
+
howlGuard: SwitchInput
|
|
210
250
|
guideOffsetMs: number
|
|
211
251
|
}
|
|
212
252
|
|
|
@@ -215,11 +255,16 @@ export interface SlotStatsInfo {
|
|
|
215
255
|
pushed: number
|
|
216
256
|
lost: number
|
|
217
257
|
late: number
|
|
258
|
+
lateAtOpening: number
|
|
218
259
|
concealed: number
|
|
219
260
|
resyncs: number
|
|
220
261
|
jitterMs: number
|
|
221
262
|
correctorDelayMs: number
|
|
222
263
|
correctorMaxDelayMs: number
|
|
264
|
+
/** Null while the canceller is off or has yet to find and learn the room's echo. */
|
|
265
|
+
echo: EchoInfo | null
|
|
266
|
+
playoutDelayMs: number
|
|
267
|
+
howlCatches: number
|
|
223
268
|
}
|
|
224
269
|
|
|
225
270
|
export interface StemGainsInput {
|
|
@@ -227,6 +272,11 @@ export interface StemGainsInput {
|
|
|
227
272
|
guide: number
|
|
228
273
|
}
|
|
229
274
|
|
|
275
|
+
/** An effect with no knob but on and off. */
|
|
276
|
+
export interface SwitchInput {
|
|
277
|
+
enabled: boolean
|
|
278
|
+
}
|
|
279
|
+
|
|
230
280
|
export interface VoiceInput {
|
|
231
281
|
enabled: boolean
|
|
232
282
|
formantSemitones: number
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aeonentertainment/mixer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Low-latency LAN audio mixer for Aeon Karaoke: phone and house mics, a backing track, one mix to the room.",
|
|
5
5
|
"bugs": {
|
|
6
6
|
"url": "https://github.com/aeonentertainment/aeon-mixer/issues"
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"@napi-rs/cli": "^3.10.9",
|
|
27
27
|
"@types/node": "^26.6.6",
|
|
28
28
|
"typescript": "^7.0.2",
|
|
29
|
-
"vite-plus": "1.
|
|
29
|
+
"vite-plus": "1.5.1"
|
|
30
30
|
},
|
|
31
31
|
"devEngines": {
|
|
32
32
|
"packageManager": {
|