@aeonentertainment/mixer 0.0.0-stage → 0.2.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/LICENSE +21 -0
- package/README.md +93 -2
- package/dist/index.d.mts +462 -0
- package/dist/index.mjs +722 -0
- package/native/binding/index.d.ts +216 -0
- package/native/binding/index.darwin-arm64.node +0 -0
- package/native/binding/index.darwin-x64.node +0 -0
- package/native/binding/index.js +841 -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/native/binding/package.json +6 -0
- package/package.json +44 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Aeon Entertainment
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,94 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Aeon Mixer
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A low-latency audio mixer for [Aeon Karaoke](https://github.com/aeonentertainment/aeon-karaoke).
|
|
4
|
+
Phones on the LAN, wired mics and wireless mics are the microphones. The mixer takes those mic
|
|
5
|
+
streams and a backing track, and plays one mix to the room.
|
|
6
|
+
|
|
7
|
+
A native engine owns the audio device's output callback. TypeScript drives it through a Node
|
|
8
|
+
addon, and no audio crosses into JavaScript. Mouth to speaker, the target is 20 to 30 ms. The
|
|
9
|
+
package ships the addon prebuilt for macOS, Windows and Linux, so installing it needs no Rust
|
|
10
|
+
toolchain. Aeon Karaoke's device service is its one consumer.
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
Each [release](https://github.com/aeonentertainment/aeon-mixer/releases) carries one tarball.
|
|
15
|
+
Depend on its URL, and the lockfile pins its hash:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install https://github.com/aeonentertainment/aeon-mixer/releases/download/v0.2.0/aeonentertainment-mixer-0.2.0.tgz
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Use
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
import { openEngine } from "@aeonentertainment/mixer";
|
|
25
|
+
|
|
26
|
+
// Opened once for the life of the process; it never reopens the device between songs
|
|
27
|
+
const engine = await openEngine({ output: {} });
|
|
28
|
+
|
|
29
|
+
const mixer = await engine.createMixer({
|
|
30
|
+
slots: [{ kind: "rtp" }, { kind: "l16" }, { kind: "push" }],
|
|
31
|
+
backing: {
|
|
32
|
+
stems: [
|
|
33
|
+
{ kind: "instrumental", path: "song/instrumental.flac" },
|
|
34
|
+
{ kind: "guideVocals", path: "song/vocals.flac" },
|
|
35
|
+
],
|
|
36
|
+
key: -2,
|
|
37
|
+
speed: 1,
|
|
38
|
+
stemGains: { instrumental: 1, guideVocals: 0.3 },
|
|
39
|
+
},
|
|
40
|
+
rtpReturn: { host: "192.168.1.20", port: 40000, payloadType: 111, ssrc: 0x1234_5678 },
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
mixer.slots[0]?.port; // the UDP port this phone sends Opus over RTP to
|
|
44
|
+
mixer.on("level", (slot, rmsDb, peakDb) => {});
|
|
45
|
+
|
|
46
|
+
const { startedAt, backingOffsetMs } = await mixer.start();
|
|
47
|
+
await mixer.destroy();
|
|
48
|
+
await engine.close();
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
- **Slots** are the mics: 8 per engine by default, up to 16 with `openEngine({ slots })`. `rtp`
|
|
52
|
+
takes Opus over UDP from a phone or mediasoup, `l16` takes raw PCM over loopback from a house
|
|
53
|
+
mic, and `push` takes PCM the host hands to `mixer.push`. Each slot has gain, pan, mute, solo,
|
|
54
|
+
reverb, a pitch reading and a melody guide.
|
|
55
|
+
- **The backing** is any format Symphonia decodes, resampled to 48 kHz. The armed key and speed
|
|
56
|
+
are rendered before `start` resolves. Live, it supports seek, pause, resume, a key change and
|
|
57
|
+
stem gains, and `start`, `seek` and `resume` report the position the lyric clock follows.
|
|
58
|
+
- **`rtpReturn`** sends the mix to mediasoup's PlainTransport as 10 ms Opus with in-band FEC.
|
|
59
|
+
`host` is an IP address, never a name.
|
|
60
|
+
- **Reactions** play through `engine.reactions.play` between songs and during them.
|
|
61
|
+
- **The timer engine**, `openEngine({ output: { timer: true } })`, is for a box with no sound
|
|
62
|
+
card. It opens no device; its own thread renders a block at each deadline on the OS's
|
|
63
|
+
monotonic clock, and the mix leaves on `rtpReturn`. `measure-clock` prints its timing:
|
|
64
|
+
`cargo run --release -p devices --bin measure-clock` in `native/`.
|
|
65
|
+
- **The offline engine**, `openEngine({ output: { offline: true } })`, opens no device and
|
|
66
|
+
renders only when its caller runs `engine.pump(blocks)`. The tests run on it.
|
|
67
|
+
|
|
68
|
+
[docs/design.md](docs/design.md) section 7.2 has the full API. The reasons are in
|
|
69
|
+
[docs/decisions](docs/decisions).
|
|
70
|
+
|
|
71
|
+
## Development
|
|
72
|
+
|
|
73
|
+
You install Node 24, pnpm, a Rust toolchain, cmake and a C++17 compiler yourself (GCC, Clang or
|
|
74
|
+
the Visual Studio C++ build tools). cmake builds libopus from source, and the compiler builds the
|
|
75
|
+
DSP kernels in `native/kernels`.
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
pnpm install # dependencies, including Vite+
|
|
79
|
+
pnpm exec vp run build:native # the addon the TypeScript tests load
|
|
80
|
+
pnpm exec vp check # format, lint, type-check
|
|
81
|
+
pnpm exec vp test # the TypeScript suite, on the offline engine
|
|
82
|
+
pnpm exec vp pack # build dist/
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
In `native/`, `cargo fmt --check`, `cargo clippy --all-targets -- -D warnings` and `cargo test`
|
|
86
|
+
do the same for Rust. A pre-commit hook runs `vp check --fix` on staged files;
|
|
87
|
+
`pnpm exec vp hooks disable` turns it off for your clone.
|
|
88
|
+
|
|
89
|
+
Agents start at [AGENTS.md](AGENTS.md). Contributors start at [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
90
|
+
Security reports go through [SECURITY.md](SECURITY.md).
|
|
91
|
+
|
|
92
|
+
## License
|
|
93
|
+
|
|
94
|
+
[MIT](LICENSE)
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A song's stems being decoded and rendered for one mixer. JavaScript holds it from
|
|
3
|
+
* `createMixer` and installs it when the events thread says the job is done.
|
|
4
|
+
*/
|
|
5
|
+
declare class BackingRender {
|
|
6
|
+
/** The id the events thread's `renderProgress` and `renderFinished` events carry. */
|
|
7
|
+
get id(): number;
|
|
8
|
+
}
|
|
9
|
+
declare class Engine$1 {
|
|
10
|
+
/**
|
|
11
|
+
* Starts the clock on the silent graph and the events thread, which calls `on_events` with
|
|
12
|
+
* every batch it drains.
|
|
13
|
+
*/
|
|
14
|
+
constructor(options: OpenOptions, onEvents: (arg: Array<NativeEvent>) => void);
|
|
15
|
+
/**
|
|
16
|
+
* Builds a mixer with one slot per kind, in order: `"rtp"`, `"l16"` or `"push"`. The rtp
|
|
17
|
+
* and l16 slots bind their ports here and report them through `Mixer.port`; a return stream
|
|
18
|
+
* gets its socket and send task here too.
|
|
19
|
+
*/
|
|
20
|
+
createMixer(kinds: Array<string>, rtpReturn?: RtpReturnInput | undefined | null): Mixer$1;
|
|
21
|
+
get output(): OutputInfo$1;
|
|
22
|
+
/** The outputs this OS offers, the default marked. Empty on the offline and timer clocks. */
|
|
23
|
+
outputs(): Array<OutputEntry>;
|
|
24
|
+
/** Offline clock only: renders `blocks` blocks and returns them as interleaved stereo. */
|
|
25
|
+
pump(blocks: number): Float32Array;
|
|
26
|
+
/** Writes one slot of the snapshot and publishes the whole of it. */
|
|
27
|
+
setSlotParams(index: number, params: SlotParamsInput): void;
|
|
28
|
+
/**
|
|
29
|
+
* Moves `mixer`'s graph to the audio thread and returns the swap's id. The graph it replaces
|
|
30
|
+
* comes back through the retired ring as a `retired` event.
|
|
31
|
+
*/
|
|
32
|
+
swap(mixer: Mixer$1): number;
|
|
33
|
+
/** Swaps the silent graph in: the end of a mixer. */
|
|
34
|
+
swapSilent(): number;
|
|
35
|
+
start(): number;
|
|
36
|
+
/**
|
|
37
|
+
* Starts decoding and rendering a song's stems on the worker thread. The events thread
|
|
38
|
+
* reports the job's progress under the id the result carries.
|
|
39
|
+
*/
|
|
40
|
+
startRender(input: BackingInput): BackingRender;
|
|
41
|
+
seek(toFrame: number): number;
|
|
42
|
+
pause(): number;
|
|
43
|
+
resume(): number;
|
|
44
|
+
setKey(semitones: number): number;
|
|
45
|
+
stopAt(atFrame: number, fadeFrames: number): number;
|
|
46
|
+
/** Writes the stem gains of the snapshot and publishes the whole of it. */
|
|
47
|
+
setStemGains(gains: StemGainsInput): void;
|
|
48
|
+
/** Gives the live graph's slot a guide, or takes it away with nothing. No ack. */
|
|
49
|
+
setGuide(slot: number, guide?: GuideInput | undefined | null): void;
|
|
50
|
+
/** The live graph's slot forgets the stream it was hearing. No ack. */
|
|
51
|
+
resetSlot(slot: number): void;
|
|
52
|
+
/** Plays interleaved stereo on a free reaction voice; false when four already play. */
|
|
53
|
+
playReaction(stereo: Float32Array): boolean;
|
|
54
|
+
/**
|
|
55
|
+
* Stops the clock, then the network thread with every port still on it, then the events
|
|
56
|
+
* thread. Whatever graph is live is dropped here.
|
|
57
|
+
*/
|
|
58
|
+
close(): void;
|
|
59
|
+
}
|
|
60
|
+
declare class Mixer$1 {
|
|
61
|
+
/** Moves a finished render's stems into the graph as its backing path, before `swap`. */
|
|
62
|
+
installBacking(render: BackingRender): void;
|
|
63
|
+
/** The UDP port a slot listens on, or null for a push slot. */
|
|
64
|
+
port(index: number): number | null;
|
|
65
|
+
/**
|
|
66
|
+
* Hands interleaved stereo PCM to a push slot, which folds it to mono as every slot is
|
|
67
|
+
* (docs/design.md 8.4), and returns how many frames it took.
|
|
68
|
+
*/
|
|
69
|
+
push(index: number, stereo: Float32Array): number;
|
|
70
|
+
/**
|
|
71
|
+
* Records what reaches an `rtp` or `l16` slot's port to a new pcap file at `path`, or stops
|
|
72
|
+
* recording it for null.
|
|
73
|
+
*/
|
|
74
|
+
recordSlot(index: number, path?: string | undefined | null): void;
|
|
75
|
+
/** Every slot's ingest counters, as the audio thread last published them. */
|
|
76
|
+
stats(): Array<SlotStatsInfo>;
|
|
77
|
+
/** The return stream's counters, or null for a mixer without one. */
|
|
78
|
+
returnStats(): RtpReturnStats | null;
|
|
79
|
+
/**
|
|
80
|
+
* The end of the mixer on this thread: closes the slots' ports, drops the push feeds, stops
|
|
81
|
+
* the return stream, and drops a graph that never started.
|
|
82
|
+
*/
|
|
83
|
+
release(): void;
|
|
84
|
+
}
|
|
85
|
+
interface BackingInput {
|
|
86
|
+
instrumental: string;
|
|
87
|
+
guide?: string;
|
|
88
|
+
/** The armed key, rendered before start. */
|
|
89
|
+
semitones: number;
|
|
90
|
+
/** The armed speed factor, rendered before start. */
|
|
91
|
+
speed: number;
|
|
92
|
+
}
|
|
93
|
+
interface CorrectorInput {
|
|
94
|
+
enabled: boolean;
|
|
95
|
+
strength: number;
|
|
96
|
+
retuneMs: number;
|
|
97
|
+
}
|
|
98
|
+
interface GuideInput {
|
|
99
|
+
notes: Array<NoteInput>;
|
|
100
|
+
/** The key's pitch classes as bits, C in bit 0. */
|
|
101
|
+
keyMask: number;
|
|
102
|
+
}
|
|
103
|
+
type NativeEvent = {
|
|
104
|
+
kind: 'ack';
|
|
105
|
+
id: number;
|
|
106
|
+
positionFrames: number;
|
|
107
|
+
} | {
|
|
108
|
+
kind: 'level';
|
|
109
|
+
slot: number;
|
|
110
|
+
rmsDb: number;
|
|
111
|
+
peakDb: number;
|
|
112
|
+
} | {
|
|
113
|
+
kind: 'masterLevel';
|
|
114
|
+
rmsDb: number;
|
|
115
|
+
peakDb: number;
|
|
116
|
+
} | {
|
|
117
|
+
kind: 'pitch';
|
|
118
|
+
slot: number;
|
|
119
|
+
positionMs: number;
|
|
120
|
+
hz: number;
|
|
121
|
+
confidence: number;
|
|
122
|
+
} | {
|
|
123
|
+
kind: 'position';
|
|
124
|
+
frames: number;
|
|
125
|
+
} | {
|
|
126
|
+
kind: 'backingEnded';
|
|
127
|
+
} | {
|
|
128
|
+
kind: 'underrun';
|
|
129
|
+
} | {
|
|
130
|
+
kind: 'fault';
|
|
131
|
+
} | {
|
|
132
|
+
kind: 'retired';
|
|
133
|
+
} | {
|
|
134
|
+
kind: 'renderProgress';
|
|
135
|
+
job: number;
|
|
136
|
+
fraction: number;
|
|
137
|
+
} | {
|
|
138
|
+
kind: 'renderFinished';
|
|
139
|
+
job: number;
|
|
140
|
+
};
|
|
141
|
+
interface NoteInput {
|
|
142
|
+
midi: number;
|
|
143
|
+
startFrame: number;
|
|
144
|
+
endFrame: number;
|
|
145
|
+
}
|
|
146
|
+
interface OpenOptions {
|
|
147
|
+
clock: 'offline' | 'timer' | 'device';
|
|
148
|
+
/** A device clock's output: an id from `outputs`, or the default output. */
|
|
149
|
+
device?: string;
|
|
150
|
+
periodFrames: number;
|
|
151
|
+
/** The jitter buffer's playout delay for every `rtp` and `l16` slot. */
|
|
152
|
+
playoutDelayFrames: number;
|
|
153
|
+
}
|
|
154
|
+
interface OutputEntry {
|
|
155
|
+
id: string;
|
|
156
|
+
name: string;
|
|
157
|
+
isDefault: boolean;
|
|
158
|
+
}
|
|
159
|
+
interface OutputInfo$1 {
|
|
160
|
+
sampleRate: number;
|
|
161
|
+
periodFrames: number;
|
|
162
|
+
latencyMs: number;
|
|
163
|
+
}
|
|
164
|
+
interface ReverbInput {
|
|
165
|
+
enabled: boolean;
|
|
166
|
+
roomSize: number;
|
|
167
|
+
mix: number;
|
|
168
|
+
}
|
|
169
|
+
/** `MixerConfig.rtpReturn`, range-checked in `src/` before it crosses. */
|
|
170
|
+
interface RtpReturnInput {
|
|
171
|
+
/** An IP literal: a name would be resolved on the control thread. */
|
|
172
|
+
host: string;
|
|
173
|
+
port: number;
|
|
174
|
+
payloadType: number;
|
|
175
|
+
ssrc: number;
|
|
176
|
+
}
|
|
177
|
+
/** What `Mixer.stats` says about the return stream. */
|
|
178
|
+
interface RtpReturnStats {
|
|
179
|
+
/** Blocks the audio thread dropped because the network thread had not drained the ring. */
|
|
180
|
+
droppedBlocks: number;
|
|
181
|
+
}
|
|
182
|
+
interface SlotParamsInput {
|
|
183
|
+
/** Linear amplitude. */
|
|
184
|
+
gain: number;
|
|
185
|
+
/** -1 left to 1 right. */
|
|
186
|
+
pan: number;
|
|
187
|
+
mute: boolean;
|
|
188
|
+
solo: boolean;
|
|
189
|
+
corrector: CorrectorInput;
|
|
190
|
+
reverb: ReverbInput;
|
|
191
|
+
guideOffsetMs: number;
|
|
192
|
+
}
|
|
193
|
+
/** `SlotStats` as JavaScript reads it. */
|
|
194
|
+
interface SlotStatsInfo {
|
|
195
|
+
pushed: number;
|
|
196
|
+
lost: number;
|
|
197
|
+
late: number;
|
|
198
|
+
concealed: number;
|
|
199
|
+
resyncs: number;
|
|
200
|
+
jitterMs: number;
|
|
201
|
+
}
|
|
202
|
+
interface StemGainsInput {
|
|
203
|
+
instrumental: number;
|
|
204
|
+
guide: number;
|
|
205
|
+
}
|
|
206
|
+
declare const NativeEngine: typeof Engine$1;
|
|
207
|
+
type NativeEngine = Engine$1;
|
|
208
|
+
type NativeMixer = Mixer$1;
|
|
209
|
+
type Capability = "guideVocal" | "key" | "speed" | "seek" | "shortVersion" | "pause" | "level";
|
|
210
|
+
type IngestKind = "rtp" | "l16" | "push";
|
|
211
|
+
type StemKind = "instrumental" | "guideVocals";
|
|
212
|
+
interface OpenEngineOptions {
|
|
213
|
+
/**
|
|
214
|
+
* A device; `timer` for a box with no sound card, whose own thread renders on the OS's clock
|
|
215
|
+
* and whose mix leaves only on a mixer's `rtpReturn`; `offline` for tests, which pump it.
|
|
216
|
+
*/
|
|
217
|
+
output: {
|
|
218
|
+
device?: string;
|
|
219
|
+
periodFrames?: 64 | 128;
|
|
220
|
+
} | {
|
|
221
|
+
timer: true;
|
|
222
|
+
} | {
|
|
223
|
+
offline: true;
|
|
224
|
+
};
|
|
225
|
+
/** Fixed for the engine's life, default 8, at most 16. */
|
|
226
|
+
slots?: number;
|
|
227
|
+
/** The ingest jitter buffer, default 15. */
|
|
228
|
+
playoutDelayMs?: number;
|
|
229
|
+
}
|
|
230
|
+
interface OutputInfo {
|
|
231
|
+
readonly sampleRate: 48000;
|
|
232
|
+
readonly periodFrames: number;
|
|
233
|
+
readonly latencyMs: number;
|
|
234
|
+
}
|
|
235
|
+
interface MixerConfig {
|
|
236
|
+
slots: ReadonlyArray<{
|
|
237
|
+
kind: IngestKind;
|
|
238
|
+
}>;
|
|
239
|
+
backing?: {
|
|
240
|
+
stems: ReadonlyArray<{
|
|
241
|
+
kind: StemKind;
|
|
242
|
+
path: string;
|
|
243
|
+
}>;
|
|
244
|
+
/** Semitones, rendered before start. */
|
|
245
|
+
key: number;
|
|
246
|
+
/** A factor, rendered before start. */
|
|
247
|
+
speed: number;
|
|
248
|
+
stemGains: Record<StemKind, number>;
|
|
249
|
+
stopAtMs?: number;
|
|
250
|
+
};
|
|
251
|
+
rtpReturn?: {
|
|
252
|
+
host: string;
|
|
253
|
+
port: number;
|
|
254
|
+
payloadType: number;
|
|
255
|
+
ssrc: number;
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
interface SlotSettings {
|
|
259
|
+
/** Linear amplitude, 0 to 4. */
|
|
260
|
+
gain: number;
|
|
261
|
+
/** -1 left to 1 right. */
|
|
262
|
+
pan: number;
|
|
263
|
+
mute: boolean;
|
|
264
|
+
solo: boolean;
|
|
265
|
+
}
|
|
266
|
+
type SlotEffect = {
|
|
267
|
+
kind: "pitch-correction";
|
|
268
|
+
enabled: boolean;
|
|
269
|
+
strength: number;
|
|
270
|
+
retuneMs: number;
|
|
271
|
+
} | {
|
|
272
|
+
kind: "reverb";
|
|
273
|
+
enabled: boolean;
|
|
274
|
+
roomSize: number;
|
|
275
|
+
mix: number;
|
|
276
|
+
};
|
|
277
|
+
interface MelodyGuide {
|
|
278
|
+
notes: ReadonlyArray<{
|
|
279
|
+
midi: number;
|
|
280
|
+
startMs: number;
|
|
281
|
+
endMs: number;
|
|
282
|
+
}>;
|
|
283
|
+
/** The key's pitch classes as bits, C in bit 0. */
|
|
284
|
+
keyMask: number;
|
|
285
|
+
}
|
|
286
|
+
interface SlotStats {
|
|
287
|
+
pushed: number;
|
|
288
|
+
lost: number;
|
|
289
|
+
late: number;
|
|
290
|
+
concealed: number;
|
|
291
|
+
resyncs: number;
|
|
292
|
+
jitterMs: number;
|
|
293
|
+
}
|
|
294
|
+
type EngineEvents = {
|
|
295
|
+
fault: (error: Error) => void;
|
|
296
|
+
/** The mix after the clipper, reactions included, between songs too. */
|
|
297
|
+
masterLevel: (rmsDb: number, peakDb: number) => void;
|
|
298
|
+
};
|
|
299
|
+
type MixerEvents = {
|
|
300
|
+
level: (slot: number, rmsDb: number, peakDb: number) => void;
|
|
301
|
+
pitch: (slot: number, positionMs: number, hz: number, confidence: number) => void;
|
|
302
|
+
position: (positionMs: number) => void;
|
|
303
|
+
renderProgress: (fraction: number) => void;
|
|
304
|
+
backingEnded: () => void;
|
|
305
|
+
error: (error: Error) => void;
|
|
306
|
+
};
|
|
307
|
+
type BackingConfig = NonNullable<MixerConfig["backing"]>;
|
|
308
|
+
/** What the backing path needs from its engine: word of its render job, as the events thread reports it. */
|
|
309
|
+
interface RenderHost {
|
|
310
|
+
readonly native: NativeEngine;
|
|
311
|
+
/**
|
|
312
|
+
* Reports the render job's progress until it finishes, then resolves; rejects when the engine
|
|
313
|
+
* stops first.
|
|
314
|
+
*/
|
|
315
|
+
watchRender(id: number, onProgress: (fraction: number) => void): Promise<void>;
|
|
316
|
+
}
|
|
317
|
+
type Handler = (...args: never[]) => void;
|
|
318
|
+
/** Typed listeners for one object's events. `on` returns the unsubscribe. */
|
|
319
|
+
declare class Emitter<Events extends Record<keyof Events, Handler>> {
|
|
320
|
+
#private;
|
|
321
|
+
on<K extends keyof Events>(event: K, listener: Events[K]): () => void;
|
|
322
|
+
emit<K extends keyof Events>(event: K, ...args: Parameters<Events[K]>): void;
|
|
323
|
+
}
|
|
324
|
+
/** What a mixer needs from its engine. Internal to the package. */
|
|
325
|
+
interface MixerHost extends RenderHost {
|
|
326
|
+
readonly output: OutputInfo;
|
|
327
|
+
/**
|
|
328
|
+
* Sends a command carrying an id and resolves on its ack with the backing position. `onTimeout`
|
|
329
|
+
* hears the rejection when no ack comes in time.
|
|
330
|
+
*/
|
|
331
|
+
command(send: () => number, onTimeout: (error: Error) => void): Promise<number>;
|
|
332
|
+
/**
|
|
333
|
+
* Swaps `graph` in as the live graph, routing its events to `events`. Rejects while another
|
|
334
|
+
* mixer is live. `onTimeout` is `command`'s.
|
|
335
|
+
*/
|
|
336
|
+
swapIn(mixer: Mixer, graph: NativeMixer, events: Emitter<MixerEvents>, onTimeout: (error: Error) => void): Promise<number>;
|
|
337
|
+
/**
|
|
338
|
+
* Swaps the silent graph back in; resolves once the mixer's graph is back and dropped, or at
|
|
339
|
+
* once on a faulted engine, which hands nothing back.
|
|
340
|
+
*/
|
|
341
|
+
swapOut(mixer: Mixer): Promise<void>;
|
|
342
|
+
/** Hears the engine's fault until the returned function is called. */
|
|
343
|
+
onFault(listener: (error: Error) => void): () => void;
|
|
344
|
+
isLive(mixer: Mixer): boolean;
|
|
345
|
+
anyLive(): boolean;
|
|
346
|
+
underruns(): number;
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* One mixing graph for one performance. Its `rtp` and `l16` slots listen on their ports from
|
|
350
|
+
* here until `destroy`; its return stream sends the mix from `start` until `destroy`. Settings
|
|
351
|
+
* made while armed are kept here and written to the engine when `start` swaps the graph in, so
|
|
352
|
+
* arming the next song never touches the live one.
|
|
353
|
+
*/
|
|
354
|
+
export declare class Mixer {
|
|
355
|
+
#private;
|
|
356
|
+
/** Each slot's kind and, for `rtp` and `l16`, the UDP port it listens on; `null` for `push`. */
|
|
357
|
+
readonly slots: ReadonlyArray<{
|
|
358
|
+
index: number;
|
|
359
|
+
kind: IngestKind;
|
|
360
|
+
port: number | null;
|
|
361
|
+
}>;
|
|
362
|
+
constructor(host: MixerHost, kinds: ReadonlyArray<IngestKind>, events: Emitter<MixerEvents>, backing: BackingConfig | null, rtpReturn: RtpReturnInput | null);
|
|
363
|
+
setSlot(index: number, settings: Partial<SlotSettings>): void;
|
|
364
|
+
/** The slot's whole chain: an effect not in `effects` is disabled. */
|
|
365
|
+
setSlotEffects(index: number, effects: ReadonlyArray<SlotEffect>): void;
|
|
366
|
+
setSlotGuide(index: number, guide: MelodyGuide | null, guideOffsetMs?: number): void;
|
|
367
|
+
/**
|
|
368
|
+
* Forget the stream a slot was hearing; the next packets, or the next pushed audio, are a new
|
|
369
|
+
* singer. An armed mixer has heard nothing yet, so there is nothing to forget.
|
|
370
|
+
*/
|
|
371
|
+
resetSlot(index: number): void;
|
|
372
|
+
/**
|
|
373
|
+
* Writes every datagram that reaches an `rtp` or `l16` slot's port to a new pcap file at `path`,
|
|
374
|
+
* with the moment the network thread read it, until `null`, another path or `destroy`. The file
|
|
375
|
+
* is created here and never over an existing one, so a bad path throws. A recording holds a
|
|
376
|
+
* singer's voice and their phone's LAN address: it is made only when a person asks for one, and
|
|
377
|
+
* nothing in this package sends it anywhere. Start it after `start`, so a replay begins where
|
|
378
|
+
* the live slot did. docs/design.md 8.4.
|
|
379
|
+
*/
|
|
380
|
+
recordSlot(index: number, path: string | null): void;
|
|
381
|
+
/**
|
|
382
|
+
* Hands interleaved stereo PCM to a push slot, folded to mono as every slot is. Returns the
|
|
383
|
+
* frames taken; the rest did not fit and is dropped, since late audio is worse than lost audio.
|
|
384
|
+
* Throws on a slot of another kind.
|
|
385
|
+
*/
|
|
386
|
+
push(index: number, stereo: Float32Array): number;
|
|
387
|
+
/**
|
|
388
|
+
* Waits for the backing's render and swaps the graph in; a render that failed rejects with the
|
|
389
|
+
* message `error` carried. With the render already done, the swap is sent before this returns
|
|
390
|
+
* to the caller. Nothing is written to the engine while another mixer is live, so a refused
|
|
391
|
+
* start leaves the song playing untouched.
|
|
392
|
+
*/
|
|
393
|
+
start(): Promise<{
|
|
394
|
+
startedAt: number;
|
|
395
|
+
backingOffsetMs: number;
|
|
396
|
+
speedFactor: number;
|
|
397
|
+
}>;
|
|
398
|
+
/** Resolves with the position the seek landed on, once its fade out is done. */
|
|
399
|
+
seek(toMs: number): Promise<{
|
|
400
|
+
positionMs: number;
|
|
401
|
+
}>;
|
|
402
|
+
/** Resolves once the fade out is done and the playhead holds. */
|
|
403
|
+
pause(): Promise<void>;
|
|
404
|
+
resume(): Promise<{
|
|
405
|
+
positionMs: number;
|
|
406
|
+
}>;
|
|
407
|
+
/** The live key, on top of the armed one; its crossfade follows the ack. */
|
|
408
|
+
setBackingKey(semitones: number): Promise<void>;
|
|
409
|
+
/** A parameter write, held while armed like the slot settings. */
|
|
410
|
+
setStemGain(kind: StemKind, gain: number): void;
|
|
411
|
+
on<K extends keyof MixerEvents>(event: K, listener: MixerEvents[K]): () => void;
|
|
412
|
+
/**
|
|
413
|
+
* Each slot's ingest counters as the audio thread last published them, the engine's underruns,
|
|
414
|
+
* and the blocks the return stream dropped, `null` without one.
|
|
415
|
+
*/
|
|
416
|
+
stats(): {
|
|
417
|
+
slots: ReadonlyArray<SlotStats>;
|
|
418
|
+
underruns: number;
|
|
419
|
+
rtpReturn: {
|
|
420
|
+
droppedBlocks: number;
|
|
421
|
+
} | null;
|
|
422
|
+
};
|
|
423
|
+
/**
|
|
424
|
+
* Closes the slots' ports, stops the return stream, swaps the silent graph in when this one is
|
|
425
|
+
* live, and resolves once this graph is back and dropped.
|
|
426
|
+
*/
|
|
427
|
+
destroy(): Promise<void>;
|
|
428
|
+
}
|
|
429
|
+
/** An engine on the test clock. Its caller pumps it; nothing plays. */
|
|
430
|
+
type OfflineEngine = Engine & Required<Pick<Engine, "pump">>;
|
|
431
|
+
/**
|
|
432
|
+
* Opens the engine for the life of the process: the device, the timer clock on a box with no
|
|
433
|
+
* sound card, or the offline driver, running the silent graph until a mixer starts.
|
|
434
|
+
*/
|
|
435
|
+
export declare function openEngine(options: OpenEngineOptions & {
|
|
436
|
+
output: {
|
|
437
|
+
offline: true;
|
|
438
|
+
};
|
|
439
|
+
}): Promise<OfflineEngine>;
|
|
440
|
+
export declare function openEngine(options: OpenEngineOptions): Promise<Engine>;
|
|
441
|
+
export declare class Engine {
|
|
442
|
+
#private;
|
|
443
|
+
readonly capabilities: ReadonlySet<Capability>;
|
|
444
|
+
readonly output: OutputInfo;
|
|
445
|
+
/** Offline engines only: render blocks and return them as interleaved stereo. */
|
|
446
|
+
readonly pump?: (blocks: number) => Float32Array;
|
|
447
|
+
constructor(options: OpenEngineOptions);
|
|
448
|
+
/** Room-lifetime reaction voices, four at once. False when a fifth is dropped while four play. */
|
|
449
|
+
readonly reactions: {
|
|
450
|
+
play: (samples: Float32Array, channels: 1 | 2) => boolean;
|
|
451
|
+
};
|
|
452
|
+
outputDevices(): Promise<ReadonlyArray<{
|
|
453
|
+
id: string;
|
|
454
|
+
name: string;
|
|
455
|
+
isDefault: boolean;
|
|
456
|
+
}>>;
|
|
457
|
+
createMixer(config: MixerConfig): Promise<Mixer>;
|
|
458
|
+
on<K extends keyof EngineEvents>(event: K, listener: EngineEvents[K]): () => void;
|
|
459
|
+
/** Destroys the live mixer, stops the clock and joins the events thread. */
|
|
460
|
+
close(): Promise<void>;
|
|
461
|
+
}
|
|
462
|
+
export type { Capability, EngineEvents, IngestKind, MelodyGuide, MixerConfig, MixerEvents, OfflineEngine, OpenEngineOptions, OutputInfo, SlotEffect, SlotSettings, SlotStats, StemKind };
|