@harmonia-audio/voice 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.
@@ -0,0 +1,638 @@
1
+ import { EventEmitter } from 'node:events';
2
+ import { Readable } from 'node:stream';
3
+ import { RtpHeader } from '@harmonia-audio/native';
4
+
5
+ /**
6
+ * Adapter methods for communicating with the Discord gateway.
7
+ *
8
+ * @example
9
+ * ```ts
10
+ * const adapter: DiscordGatewayAdapter = {
11
+ * sendPayload(payload) {
12
+ * guild.shard.send(payload);
13
+ * return true;
14
+ * },
15
+ * destroy() {
16
+ * // cleanup
17
+ * },
18
+ * };
19
+ * ```
20
+ */
21
+ interface DiscordGatewayAdapter {
22
+ sendPayload(payload: DiscordGatewayPayload): boolean;
23
+ destroy(): void;
24
+ }
25
+ /**
26
+ * Gateway payload to send for voice state updates.
27
+ *
28
+ * @example
29
+ * ```ts
30
+ * adapter.sendPayload({
31
+ * op: 4,
32
+ * d: { guild_id: '...', channel_id: '...', self_mute: false, self_deaf: false }
33
+ * });
34
+ * ```
35
+ */
36
+ interface DiscordGatewayPayload {
37
+ op: number;
38
+ d: Record<string, unknown>;
39
+ }
40
+ /**
41
+ * Factory function that creates an adapter and returns lifecycle methods.
42
+ *
43
+ * @example
44
+ * ```ts
45
+ * const adapterCreator: DiscordGatewayAdapterCreator = (methods) => {
46
+ * client.on('voiceStateUpdate', methods.onVoiceStateUpdate);
47
+ * client.on('voiceServerUpdate', methods.onVoiceServerUpdate);
48
+ * return adapter;
49
+ * };
50
+ * ```
51
+ */
52
+ type DiscordGatewayAdapterCreator = (methods: {
53
+ onVoiceStateUpdate(data: VoiceStateUpdateData): void;
54
+ onVoiceServerUpdate(data: VoiceServerUpdateData): void;
55
+ destroy(): void;
56
+ }) => DiscordGatewayAdapter;
57
+ /** Data from a voice state update event. */
58
+ interface VoiceStateUpdateData {
59
+ channel_id: string | null;
60
+ guild_id: string;
61
+ session_id: string;
62
+ user_id: string;
63
+ }
64
+ /** Data from a voice server update event. */
65
+ interface VoiceServerUpdateData {
66
+ token: string;
67
+ guild_id: string;
68
+ endpoint: string | null;
69
+ }
70
+
71
+ /**
72
+ * Voice connection states.
73
+ *
74
+ * @example
75
+ * ```ts
76
+ * if (connection.state === VoiceConnectionState.Ready) {
77
+ * // connection is ready
78
+ * }
79
+ * ```
80
+ */
81
+ declare enum VoiceConnectionState {
82
+ Idle = "idle",
83
+ Signalling = "signalling",
84
+ Connecting = "connecting",
85
+ Ready = "ready",
86
+ Resuming = "resuming",
87
+ Disconnected = "disconnected",
88
+ Destroyed = "destroyed"
89
+ }
90
+ /**
91
+ * Audio player states.
92
+ *
93
+ * @example
94
+ * ```ts
95
+ * if (player.state === AudioPlayerState.Playing) {
96
+ * player.pause();
97
+ * }
98
+ * ```
99
+ */
100
+ declare enum AudioPlayerState {
101
+ Idle = "idle",
102
+ Buffering = "buffering",
103
+ Playing = "playing",
104
+ Paused = "paused",
105
+ AutoPaused = "autopaused"
106
+ }
107
+ /**
108
+ * Events emitted by a voice connection.
109
+ *
110
+ * @example
111
+ * ```ts
112
+ * connection.on('stateChange', (oldState, newState) => {
113
+ * console.log(`${oldState} -> ${newState}`);
114
+ * });
115
+ * ```
116
+ */
117
+ interface VoiceConnectionEvents {
118
+ stateChange: [oldState: VoiceConnectionState, newState: VoiceConnectionState];
119
+ ready: [];
120
+ disconnected: [];
121
+ destroyed: [];
122
+ error: [error: Error];
123
+ }
124
+ /**
125
+ * Events emitted by an audio player.
126
+ *
127
+ * @example
128
+ * ```ts
129
+ * player.on('stateChange', (oldState, newState) => {
130
+ * console.log(`${oldState} -> ${newState}`);
131
+ * });
132
+ * ```
133
+ */
134
+ interface AudioPlayerEvents {
135
+ stateChange: [oldState: AudioPlayerState, newState: AudioPlayerState];
136
+ idle: [];
137
+ error: [error: Error];
138
+ }
139
+
140
+ /** Represents a playable audio source. */
141
+ interface PlayableResource {
142
+ read(): Buffer | null;
143
+ readonly ended: boolean;
144
+ destroy(): void;
145
+ }
146
+ /**
147
+ * Options for creating an audio player.
148
+ *
149
+ * @example
150
+ * ```ts
151
+ * const player = new AudioPlayer({ noSubscriber: 'pause' });
152
+ * ```
153
+ */
154
+ interface AudioPlayerOptions {
155
+ readonly noSubscriber?: "pause" | "stop" | "play";
156
+ }
157
+ /**
158
+ * Plays audio resources and dispatches packets to voice connections.
159
+ *
160
+ * @example
161
+ * ```ts
162
+ * import { AudioPlayer } from '@harmonia/voice';
163
+ *
164
+ * const player = new AudioPlayer();
165
+ * player.play(resource);
166
+ *
167
+ * player.on('idle', () => {
168
+ * console.log('Playback finished');
169
+ * });
170
+ * ```
171
+ */
172
+ declare class AudioPlayer extends EventEmitter {
173
+ private _state;
174
+ private resource;
175
+ private connections;
176
+ private playbackTimer;
177
+ private readonly noSubscriberBehavior;
178
+ constructor(options?: AudioPlayerOptions);
179
+ /**
180
+ * Current player state.
181
+ *
182
+ * @example
183
+ * ```ts
184
+ * if (player.state === AudioPlayerState.Playing) { ... }
185
+ * ```
186
+ */
187
+ get state(): AudioPlayerState;
188
+ /**
189
+ * Start playing an audio resource.
190
+ *
191
+ * @example
192
+ * ```ts
193
+ * player.play(audioResource);
194
+ * ```
195
+ */
196
+ play(resource: PlayableResource): void;
197
+ /**
198
+ * Pause playback.
199
+ *
200
+ * @example
201
+ * ```ts
202
+ * player.pause();
203
+ * ```
204
+ */
205
+ pause(): void;
206
+ /**
207
+ * Resume playback.
208
+ *
209
+ * @example
210
+ * ```ts
211
+ * player.resume();
212
+ * ```
213
+ */
214
+ resume(): void;
215
+ /**
216
+ * Stop playback and release the resource.
217
+ *
218
+ * @example
219
+ * ```ts
220
+ * player.stop();
221
+ * ```
222
+ */
223
+ stop(): void;
224
+ /** @internal */
225
+ addConnection(connection: VoiceConnection): void;
226
+ /** @internal */
227
+ removeConnection(connection: VoiceConnection): void;
228
+ private startPlaybackLoop;
229
+ private processFrame;
230
+ private stopInternal;
231
+ private transitionTo;
232
+ }
233
+
234
+ /**
235
+ * Options for the audio receiver.
236
+ *
237
+ * @example
238
+ * ```ts
239
+ * const receiver = connection.getReceiver();
240
+ * ```
241
+ */
242
+ interface AudioReceiverOptions {
243
+ readonly autoDecodeOpus?: boolean;
244
+ }
245
+ /**
246
+ * A readable stream of audio from a specific user.
247
+ *
248
+ * @example
249
+ * ```ts
250
+ * const stream = receiver.subscribe('user-id');
251
+ * stream.on('data', (pcm) => { ... });
252
+ * ```
253
+ */
254
+ declare class UserAudioStream extends Readable {
255
+ readonly userId: string;
256
+ constructor(userId: string);
257
+ _read(): void;
258
+ }
259
+ /**
260
+ * Receives and routes incoming audio from voice connections.
261
+ *
262
+ * @example
263
+ * ```ts
264
+ * const receiver = connection.getReceiver();
265
+ * const stream = receiver.subscribe('user-id');
266
+ *
267
+ * stream.on('data', (pcm: Buffer) => {
268
+ * // Process received audio
269
+ * });
270
+ * ```
271
+ */
272
+ declare class AudioReceiver extends EventEmitter {
273
+ private readonly connection;
274
+ private readonly ssrcMap;
275
+ private readonly userSsrcMap;
276
+ private readonly subscriptions;
277
+ constructor(connection: VoiceConnection);
278
+ /**
279
+ * Subscribe to audio from a specific user.
280
+ *
281
+ * @param userId - The Discord user ID to receive audio from
282
+ * @returns A readable stream of PCM audio from that user
283
+ *
284
+ * @example
285
+ * ```ts
286
+ * const stream = receiver.subscribe('123456789');
287
+ * stream.pipe(fileWriteStream);
288
+ * ```
289
+ */
290
+ subscribe(userId: string): UserAudioStream;
291
+ /**
292
+ * Unsubscribe from a user's audio.
293
+ *
294
+ * @example
295
+ * ```ts
296
+ * receiver.unsubscribe('123456789');
297
+ * ```
298
+ */
299
+ unsubscribe(userId: string): void;
300
+ /** @internal */
301
+ mapSsrcToUser(ssrc: number, userId: string): void;
302
+ /** @internal */
303
+ removeUser(userId: string): void;
304
+ /** @internal */
305
+ handlePacket(rawPacket: Buffer, decryptedPayload: Buffer): void;
306
+ /**
307
+ * Destroy all streams and clean up.
308
+ *
309
+ * @example
310
+ * ```ts
311
+ * receiver.destroy();
312
+ * ```
313
+ */
314
+ destroy(): this;
315
+ }
316
+
317
+ /**
318
+ * Options for creating a voice connection.
319
+ *
320
+ * @example
321
+ * ```ts
322
+ * const options: VoiceConnectionOptions = {
323
+ * guildId: '123456789',
324
+ * channelId: '987654321',
325
+ * selfDeaf: false,
326
+ * selfMute: false,
327
+ * };
328
+ * ```
329
+ */
330
+ interface VoiceConnectionOptions {
331
+ readonly guildId: string;
332
+ readonly channelId: string;
333
+ readonly selfDeaf?: boolean;
334
+ readonly selfMute?: boolean;
335
+ readonly adapterCreator: DiscordGatewayAdapterCreator;
336
+ }
337
+ /**
338
+ * Manages a voice connection to a Discord voice channel.
339
+ *
340
+ * @example
341
+ * ```ts
342
+ * import { VoiceConnection } from '@harmonia/voice';
343
+ *
344
+ * const connection = new VoiceConnection({
345
+ * guildId: '123',
346
+ * channelId: '456',
347
+ * adapterCreator: guild.voiceAdapterCreator,
348
+ * });
349
+ *
350
+ * connection.on('ready', () => {
351
+ * console.log('Connected to voice!');
352
+ * });
353
+ * ```
354
+ */
355
+ declare class VoiceConnection extends EventEmitter {
356
+ private _state;
357
+ private readonly guildId;
358
+ private channelId;
359
+ private readonly selfDeaf;
360
+ private readonly selfMute;
361
+ private adapter;
362
+ private sessionId;
363
+ private voiceToken;
364
+ private endpoint;
365
+ private ws;
366
+ private udpSocket;
367
+ private ssrc;
368
+ private remoteIp;
369
+ private remotePort;
370
+ private localIp;
371
+ private localPort;
372
+ private secretKey;
373
+ private encryptionMode;
374
+ private sequence;
375
+ private timestamp;
376
+ private nonceCounter;
377
+ private heartbeatInterval;
378
+ private heartbeatNonce;
379
+ private missedHeartbeats;
380
+ private lastHeartbeatAck;
381
+ private subscribedPlayer;
382
+ private audioThread;
383
+ private ringBuffer;
384
+ private readonly receiver;
385
+ private voiceStateResolve;
386
+ private voiceServerResolve;
387
+ constructor(options: VoiceConnectionOptions);
388
+ /**
389
+ * Current connection state.
390
+ *
391
+ * @example
392
+ * ```ts
393
+ * if (connection.state === VoiceConnectionState.Ready) { ... }
394
+ * ```
395
+ */
396
+ get state(): VoiceConnectionState;
397
+ /**
398
+ * Get the audio receiver for this connection.
399
+ *
400
+ * @example
401
+ * ```ts
402
+ * const receiver = connection.getReceiver();
403
+ * ```
404
+ */
405
+ getReceiver(): AudioReceiver;
406
+ /**
407
+ * Subscribe an audio player to this connection.
408
+ *
409
+ * @example
410
+ * ```ts
411
+ * connection.subscribe(player);
412
+ * ```
413
+ */
414
+ subscribe(player: AudioPlayer): void;
415
+ /**
416
+ * Unsubscribe the current audio player.
417
+ *
418
+ * @example
419
+ * ```ts
420
+ * connection.unsubscribe();
421
+ * ```
422
+ */
423
+ unsubscribe(): void;
424
+ /**
425
+ * Destroy the connection and clean up all resources.
426
+ *
427
+ * @example
428
+ * ```ts
429
+ * connection.destroy();
430
+ * ```
431
+ */
432
+ destroy(): void;
433
+ /**
434
+ * Send an Opus packet through the voice connection.
435
+ *
436
+ * @example
437
+ * ```ts
438
+ * connection.sendOpusPacket(opusBuffer);
439
+ * ```
440
+ */
441
+ sendOpusPacket(opusPacket: Buffer): void;
442
+ private sendVoiceStateUpdate;
443
+ private handleVoiceStateUpdate;
444
+ private handleVoiceServerUpdate;
445
+ private tryConnect;
446
+ private connectWebSocket;
447
+ private sendIdentify;
448
+ private handleGatewayMessage;
449
+ private handleReady;
450
+ private connectUdp;
451
+ private performIpDiscovery;
452
+ private handleUdpMessage;
453
+ private handleIpDiscoveryResponse;
454
+ private handleSessionDescription;
455
+ private handleSpeaking;
456
+ private handleClientConnect;
457
+ private handleClientDisconnect;
458
+ private handleHello;
459
+ private handleHeartbeatAck;
460
+ private startHeartbeat;
461
+ private stopHeartbeat;
462
+ /**
463
+ * Set the speaking flags.
464
+ *
465
+ * @example
466
+ * ```ts
467
+ * connection.setSpeaking(1); // speaking
468
+ * ```
469
+ */
470
+ setSpeaking(flags: number): void;
471
+ private wsSend;
472
+ private transitionTo;
473
+ private cleanup;
474
+ }
475
+
476
+ /**
477
+ * Options for joining a voice channel.
478
+ *
479
+ * @example
480
+ * ```ts
481
+ * const connection = joinVoiceChannel({
482
+ * guildId: '123',
483
+ * channelId: '456',
484
+ * adapterCreator: guild.voiceAdapterCreator,
485
+ * });
486
+ * ```
487
+ */
488
+ interface JoinVoiceChannelOptions {
489
+ readonly guildId: string;
490
+ readonly channelId: string;
491
+ readonly selfDeaf?: boolean;
492
+ readonly selfMute?: boolean;
493
+ readonly adapterCreator: DiscordGatewayAdapterCreator;
494
+ }
495
+ /**
496
+ * Join a Discord voice channel and return a VoiceConnection.
497
+ *
498
+ * @example
499
+ * ```ts
500
+ * import { joinVoiceChannel } from '@harmonia/voice';
501
+ *
502
+ * const connection = joinVoiceChannel({
503
+ * guildId: interaction.guildId,
504
+ * channelId: member.voice.channelId,
505
+ * adapterCreator: guild.voiceAdapterCreator,
506
+ * });
507
+ * ```
508
+ */
509
+ declare function joinVoiceChannel(options: JoinVoiceChannelOptions): VoiceConnection;
510
+
511
+ /**
512
+ * Get a voice connection by guild ID.
513
+ *
514
+ * @example
515
+ * ```ts
516
+ * const connection = getVoiceConnection('guild-id');
517
+ * ```
518
+ */
519
+ declare function getVoiceConnection(guildId: string): VoiceConnection | undefined;
520
+ /**
521
+ * Get all active voice connections.
522
+ *
523
+ * @example
524
+ * ```ts
525
+ * const all = getVoiceConnections();
526
+ * ```
527
+ */
528
+ declare function getVoiceConnections(): ReadonlyMap<string, VoiceConnection>;
529
+
530
+ /**
531
+ * Available encryption modes for voice connections.
532
+ *
533
+ * @example
534
+ * ```ts
535
+ * connection.setEncryptionMode(EncryptionMode.XSalsa20Poly1305);
536
+ * ```
537
+ */
538
+ declare enum EncryptionMode {
539
+ XSalsa20Poly1305 = "xsalsa20_poly1305",
540
+ XSalsa20Poly1305Suffix = "xsalsa20_poly1305_suffix",
541
+ XSalsa20Poly1305Lite = "xsalsa20_poly1305_lite",
542
+ AeadAes256Gcm = "aead_aes256_gcm",
543
+ AeadXChaCha20Poly1305 = "aead_xchacha20_poly1305_rtpsize"
544
+ }
545
+ /** Parsed RTP packet. */
546
+ interface RtpPacket {
547
+ header: RtpHeader;
548
+ payload: Buffer;
549
+ nonce: Buffer;
550
+ }
551
+
552
+ /**
553
+ * Error class for all voice-related errors in harmonia.
554
+ *
555
+ * @example
556
+ * ```ts
557
+ * try {
558
+ * await connection.connect();
559
+ * } catch (error) {
560
+ * if (error instanceof HarmoniaVoiceError) {
561
+ * console.error(error.code);
562
+ * }
563
+ * }
564
+ * ```
565
+ */
566
+ declare class HarmoniaVoiceError extends Error {
567
+ readonly code: string;
568
+ readonly context: Record<string, unknown>;
569
+ constructor(code: string, message: string, context?: Record<string, unknown>);
570
+ }
571
+
572
+ /**
573
+ * Options for creating an audio resource.
574
+ *
575
+ * @example
576
+ * ```ts
577
+ * const resource = createAudioResource('./song.pcm', {
578
+ * inputType: 'pcm',
579
+ * });
580
+ * ```
581
+ */
582
+ interface AudioResourceOptions {
583
+ readonly inputType?: "opus" | "pcm" | "raw";
584
+ readonly inlineVolume?: boolean;
585
+ readonly signal?: AbortSignal;
586
+ }
587
+ /**
588
+ * Audio resource that can be played by an AudioPlayer.
589
+ *
590
+ * @example
591
+ * ```ts
592
+ * const resource = createAudioResource(readableStream, { inputType: 'pcm' });
593
+ * player.play(resource);
594
+ * ```
595
+ */
596
+ declare class AudioResource implements PlayableResource {
597
+ private readonly stream;
598
+ private readonly packets;
599
+ private _ended;
600
+ private draining;
601
+ constructor(stream: Readable);
602
+ /**
603
+ * Read the next Opus packet.
604
+ *
605
+ * @example
606
+ * ```ts
607
+ * const packet = resource.read();
608
+ * ```
609
+ */
610
+ read(): Buffer | null;
611
+ /** Whether the resource has finished producing packets. */
612
+ get ended(): boolean;
613
+ /**
614
+ * Destroy the resource and release underlying streams.
615
+ *
616
+ * @example
617
+ * ```ts
618
+ * resource.destroy();
619
+ * ```
620
+ */
621
+ destroy(): void;
622
+ }
623
+ /**
624
+ * Create an AudioResource from various input types.
625
+ *
626
+ * @example
627
+ * ```ts
628
+ * import { createAudioResource } from '@harmonia/voice';
629
+ * import { createReadStream } from 'fs';
630
+ *
631
+ * const resource = createAudioResource(createReadStream('./audio.pcm'), {
632
+ * inputType: 'pcm',
633
+ * });
634
+ * ```
635
+ */
636
+ declare function createAudioResource(input: Readable | string | Buffer, options?: AudioResourceOptions): AudioResource;
637
+
638
+ export { AudioPlayer, type AudioPlayerEvents, type AudioPlayerOptions, AudioPlayerState, AudioReceiver, type AudioReceiverOptions, type AudioResourceOptions, type DiscordGatewayAdapter, type DiscordGatewayAdapterCreator, EncryptionMode, HarmoniaVoiceError, type JoinVoiceChannelOptions, type RtpPacket, VoiceConnection, type VoiceConnectionEvents, type VoiceConnectionOptions, VoiceConnectionState, createAudioResource, getVoiceConnection, getVoiceConnections, joinVoiceChannel };