@cartesia/cartesia-js 0.0.3 → 1.0.0-alpha.1

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.
Files changed (103) hide show
  1. package/.turbo/turbo-build.log +68 -38
  2. package/CHANGELOG.md +12 -0
  3. package/README.md +123 -16
  4. package/dist/chunk-3FL2SNIR.js +17 -0
  5. package/dist/chunk-3GBZUGUD.js +17 -0
  6. package/dist/chunk-4RMSIQLG.js +25 -0
  7. package/dist/chunk-BCQ63627.js +32 -0
  8. package/dist/chunk-JOHSCOLW.js +106 -0
  9. package/dist/chunk-LYPTISWL.js +75 -0
  10. package/dist/chunk-NDNN326Q.js +207 -0
  11. package/dist/chunk-WBK6LLXX.js +58 -0
  12. package/dist/chunk-WE63M7PJ.js +119 -0
  13. package/dist/{chunk-HNLIBHEN.mjs → chunk-WIFMLPT5.js} +31 -16
  14. package/dist/chunk-X7SJMF2R.js +22 -0
  15. package/dist/index.cjs +652 -0
  16. package/dist/index.d.cts +10 -0
  17. package/dist/index.d.ts +10 -0
  18. package/dist/index.js +20 -0
  19. package/dist/lib/client.cjs +89 -0
  20. package/dist/lib/client.d.cts +11 -0
  21. package/dist/lib/client.d.ts +2 -0
  22. package/dist/lib/client.js +7 -42
  23. package/dist/lib/constants.cjs +42 -0
  24. package/dist/lib/constants.d.cts +4 -0
  25. package/dist/lib/constants.d.ts +2 -3
  26. package/dist/lib/constants.js +8 -37
  27. package/dist/lib/index.cjs +531 -0
  28. package/dist/lib/index.d.cts +16 -0
  29. package/dist/lib/index.d.ts +6 -2
  30. package/dist/lib/index.js +13 -409
  31. package/dist/react/index.cjs +846 -0
  32. package/dist/react/index.d.cts +33 -0
  33. package/dist/react/index.d.ts +20 -13
  34. package/dist/react/index.js +161 -501
  35. package/dist/react/utils.cjs +57 -0
  36. package/dist/react/utils.d.cts +7 -0
  37. package/dist/react/utils.d.ts +7 -0
  38. package/dist/react/utils.js +7 -0
  39. package/dist/tts/index.cjs +470 -0
  40. package/dist/tts/index.d.cts +17 -0
  41. package/dist/tts/index.d.ts +17 -0
  42. package/dist/tts/index.js +12 -0
  43. package/dist/tts/player.cjs +198 -0
  44. package/dist/tts/player.d.cts +43 -0
  45. package/dist/tts/player.d.ts +43 -0
  46. package/dist/tts/player.js +8 -0
  47. package/dist/tts/source.cjs +167 -0
  48. package/dist/tts/source.d.cts +53 -0
  49. package/dist/tts/source.d.ts +53 -0
  50. package/dist/tts/source.js +7 -0
  51. package/dist/{audio/utils.js → tts/utils.cjs} +13 -54
  52. package/dist/tts/utils.d.cts +67 -0
  53. package/dist/tts/utils.d.ts +67 -0
  54. package/dist/{audio/utils.mjs → tts/utils.js} +2 -6
  55. package/dist/tts/websocket.cjs +453 -0
  56. package/dist/tts/websocket.d.cts +53 -0
  57. package/dist/tts/websocket.d.ts +53 -0
  58. package/dist/tts/websocket.js +11 -0
  59. package/dist/types/index.cjs +18 -0
  60. package/dist/types/index.d.cts +55 -0
  61. package/dist/types/index.d.ts +50 -1
  62. package/dist/types/index.js +1 -18
  63. package/dist/voices/index.cjs +155 -0
  64. package/dist/voices/index.d.cts +12 -0
  65. package/dist/voices/index.d.ts +12 -0
  66. package/dist/voices/index.js +9 -0
  67. package/package.json +11 -7
  68. package/src/index.ts +4 -0
  69. package/src/lib/client.ts +14 -1
  70. package/src/lib/constants.ts +13 -3
  71. package/src/lib/index.ts +6 -3
  72. package/src/react/index.ts +167 -75
  73. package/src/react/utils.ts +11 -0
  74. package/src/tts/index.ts +17 -0
  75. package/src/tts/player.ts +109 -0
  76. package/src/tts/source.ts +98 -0
  77. package/src/{audio → tts}/utils.ts +19 -97
  78. package/src/tts/websocket.ts +210 -0
  79. package/src/types/index.ts +63 -0
  80. package/src/voices/index.ts +47 -0
  81. package/dist/audio/index.d.mts +0 -5
  82. package/dist/audio/index.d.ts +0 -5
  83. package/dist/audio/index.js +0 -396
  84. package/dist/audio/index.mjs +0 -9
  85. package/dist/audio/utils.d.mts +0 -5
  86. package/dist/audio/utils.d.ts +0 -5
  87. package/dist/chunk-3CYTAFLF.mjs +0 -262
  88. package/dist/chunk-FRIBQZPN.mjs +0 -113
  89. package/dist/chunk-XSFPHPPG.mjs +0 -18
  90. package/dist/index-DSBmfK9-.d.mts +0 -158
  91. package/dist/index-qwAyxV5I.d.ts +0 -158
  92. package/dist/lib/client.d.mts +0 -9
  93. package/dist/lib/client.mjs +0 -7
  94. package/dist/lib/constants.d.mts +0 -5
  95. package/dist/lib/constants.mjs +0 -10
  96. package/dist/lib/index.d.mts +0 -12
  97. package/dist/lib/index.mjs +0 -19
  98. package/dist/react/index.d.mts +0 -26
  99. package/dist/react/index.mjs +0 -130
  100. package/dist/types/index.d.mts +0 -6
  101. package/index.ts +0 -3
  102. package/src/audio/index.ts +0 -282
  103. /package/dist/{types/index.mjs → chunk-FXPGR372.js} +0 -0
@@ -1,103 +1,152 @@
1
1
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
- import CartesiaAudio, { type Chunk, type StreamEventData } from "../audio";
3
- import { base64ToArray, bufferToWav } from "../audio/utils";
4
- import { SAMPLE_RATE } from "../lib/constants";
2
+ import { Cartesia } from "../lib";
3
+ import Player from "../tts/player";
4
+ import type Source from "../tts/source";
5
+ import type WebSocket from "../tts/websocket";
6
+ import { pingServer } from "./utils";
5
7
 
6
- export type UseAudioOptions = {
8
+ export type UseTTSOptions = {
7
9
  apiKey: string | null;
8
10
  baseUrl?: string;
11
+ sampleRate: number;
9
12
  };
10
13
 
11
- interface UseAudioReturn {
12
- stream: (options: object) => void;
14
+ export type PlaybackStatus = "inactive" | "playing" | "paused" | "finished";
15
+ export type BufferStatus = "inactive" | "buffering" | "buffered";
16
+
17
+ export type Metrics = {
18
+ modelLatency: number | null;
19
+ };
20
+
21
+ export interface UseTTSReturn {
22
+ buffer: (options: object) => Promise<void>;
13
23
  play: (bufferDuration?: number) => Promise<void>;
14
- download: () => Blob | null;
15
- isPlaying: boolean;
24
+ pause: () => Promise<void>;
25
+ resume: () => Promise<void>;
26
+ toggle: () => Promise<void>;
27
+ source: Source | null;
28
+ playbackStatus: PlaybackStatus;
29
+ bufferStatus: BufferStatus;
30
+ isWaiting: boolean;
16
31
  isConnected: boolean;
17
- isStreamed: boolean;
18
- chunks: Chunk[];
19
- messages: StreamEventData["message"][];
32
+ metrics: Metrics;
20
33
  }
34
+
35
+ const PING_INTERVAL = 5000;
36
+ const DEFAULT_BUFFER_DURATION = 0.01;
37
+
38
+ type Message = {
39
+ step_time: number;
40
+ };
41
+
21
42
  /**
22
43
  * React hook to use the Cartesia audio API.
23
44
  */
24
- export function useAudio({ apiKey, baseUrl }: UseAudioOptions): UseAudioReturn {
45
+ export function useTTS({
46
+ apiKey,
47
+ baseUrl,
48
+ sampleRate,
49
+ }: UseTTSOptions): UseTTSReturn {
25
50
  if (typeof window === "undefined") {
26
51
  return {
27
- stream: () => {},
52
+ buffer: async () => {},
28
53
  play: async () => {},
29
- download: () => null,
54
+ pause: async () => {},
55
+ resume: async () => {},
56
+ toggle: async () => {},
57
+ playbackStatus: "inactive",
58
+ bufferStatus: "inactive",
59
+ isWaiting: false,
60
+ source: null,
30
61
  isConnected: false,
31
- isPlaying: false,
32
- isStreamed: false,
33
- chunks: [],
34
- messages: [],
62
+ metrics: {
63
+ modelLatency: null,
64
+ },
35
65
  };
36
66
  }
37
67
 
38
- const audio = useMemo(() => {
68
+ const websocket = useMemo(() => {
39
69
  if (!apiKey) {
40
70
  return null;
41
71
  }
42
- const audio = new CartesiaAudio({ apiKey, baseUrl });
43
- return audio;
44
- }, [apiKey, baseUrl]);
45
- const streamReturn = useRef<ReturnType<CartesiaAudio["stream"]> | null>(null);
46
- const [isStreamed, setIsStreamed] = useState(false);
47
- const [isPlaying, setIsPlaying] = useState(false);
72
+ const cartesia = new Cartesia({ apiKey, baseUrl });
73
+ baseUrl = baseUrl ?? cartesia.baseUrl;
74
+ return cartesia.tts.websocket({ sampleRate });
75
+ }, [apiKey, baseUrl, sampleRate]);
76
+ const websocketReturn = useRef<ReturnType<WebSocket["send"]> | null>(null);
77
+ const player = useRef<Player | null>(null);
78
+ const [playbackStatus, setPlaybackStatus] =
79
+ useState<PlaybackStatus>("inactive");
80
+ const [bufferStatus, setBufferStatus] = useState<BufferStatus>("inactive");
81
+ const [isWaiting, setIsWaiting] = useState(false);
48
82
  const [isConnected, setIsConnected] = useState(false);
49
- const [chunks, setChunks] = useState<Chunk[]>([]);
50
- const [messages, setMessages] = useState<StreamEventData["message"][]>([]);
83
+ const [bufferDuration, setBufferDuration] = useState<number | null>(null);
84
+ const [messages, setMessages] = useState<Message[]>([]);
51
85
 
52
- const stream = useCallback(
86
+ const buffer = useCallback(
53
87
  async (options: object) => {
54
- streamReturn.current = audio?.stream(options) ?? null;
55
- if (!streamReturn.current) {
88
+ setMessages([]);
89
+ setBufferStatus("buffering");
90
+ websocketReturn.current = websocket?.send(options) ?? null;
91
+ if (!websocketReturn.current) {
56
92
  return;
57
93
  }
58
- setMessages([]);
59
- streamReturn.current.on(
60
- "chunk",
61
- ({ chunks }: StreamEventData["chunk"]) => {
62
- setChunks(chunks);
63
- },
64
- );
65
- streamReturn.current.on(
66
- "message",
67
- (message: StreamEventData["message"]) => {
68
- setMessages((messages) => [...messages, message]);
69
- },
70
- );
71
- const { chunks } = await streamReturn.current.once("streamed");
72
- setChunks(chunks);
73
- setIsStreamed(true);
94
+ websocketReturn.current.on("message", (message) => {
95
+ setMessages((messages) => [...messages, JSON.parse(message)]);
96
+ });
97
+ await websocketReturn.current.source.once("close");
98
+ setBufferStatus("buffered");
74
99
  },
75
- [audio],
100
+ [websocket],
76
101
  );
77
102
 
78
- const download = useCallback(() => {
79
- if (!isStreamed) {
80
- return null;
103
+ const metrics = useMemo(() => {
104
+ // Model Latency is the first step time
105
+ if (messages.length === 0) {
106
+ return {
107
+ modelLatency: null,
108
+ };
81
109
  }
82
- const audio = bufferToWav(SAMPLE_RATE, [base64ToArray(chunks)]);
83
- return new Blob([audio], { type: "audio/wav" });
84
- }, [isStreamed, chunks]);
110
+ const modelLatency = messages[0].step_time ?? null;
111
+ return {
112
+ modelLatency: Math.trunc(modelLatency),
113
+ };
114
+ }, [messages]);
85
115
 
86
116
  useEffect(() => {
87
117
  let cleanup: (() => void) | undefined = () => {};
88
118
  async function setupConnection() {
89
119
  try {
90
- const connection = await audio?.connect();
120
+ const connection = await websocket?.connect();
91
121
  if (!connection) {
92
122
  return;
93
123
  }
94
124
  setIsConnected(true);
125
+ connection.on("open", () => {
126
+ setIsConnected(true);
127
+ });
95
128
  const unsubscribe = connection.on("close", () => {
96
129
  setIsConnected(false);
97
130
  });
131
+ const intervalId = setInterval(() => {
132
+ if (baseUrl) {
133
+ pingServer(new URL(baseUrl).origin).then((ping) => {
134
+ let bufferDuration: number;
135
+ if (ping < 300) {
136
+ bufferDuration = 0.01; // No buffering for very low latency
137
+ } else if (ping > 1500) {
138
+ bufferDuration = 6; // Max buffering for very high latency (6 seconds)
139
+ } else {
140
+ bufferDuration = (ping / 1000) * 4; // Adjust buffer duration based on ping
141
+ }
142
+ setBufferDuration(bufferDuration);
143
+ });
144
+ }
145
+ }, PING_INTERVAL);
98
146
  return () => {
99
147
  unsubscribe();
100
- audio?.disconnect();
148
+ clearInterval(intervalId);
149
+ websocket?.disconnect();
101
150
  };
102
151
  } catch (e) {
103
152
  console.error(e);
@@ -107,34 +156,77 @@ export function useAudio({ apiKey, baseUrl }: UseAudioOptions): UseAudioReturn {
107
156
  cleanup = cleanupConnection;
108
157
  });
109
158
  return () => cleanup?.();
110
- }, [audio]);
159
+ }, [websocket, baseUrl]);
111
160
 
112
- const play = useCallback(
113
- async (bufferDuration = 0) => {
114
- if (isPlaying || !streamReturn.current) {
115
- return;
161
+ const play = useCallback(async () => {
162
+ if (playbackStatus === "playing" || !websocketReturn.current) {
163
+ return;
164
+ }
165
+ setPlaybackStatus("playing");
166
+
167
+ const unsubscribes = [];
168
+ unsubscribes.push(
169
+ websocketReturn.current.source.on("wait", () => {
170
+ setIsWaiting(true);
171
+ }),
172
+ );
173
+ unsubscribes.push(
174
+ websocketReturn.current.source.on("read", () => {
175
+ setIsWaiting(false);
176
+ }),
177
+ );
178
+
179
+ player.current = new Player({
180
+ bufferDuration: bufferDuration ?? DEFAULT_BUFFER_DURATION,
181
+ });
182
+ // Wait for the playback to finish before setting isPlaying to false.
183
+ await player.current.play(websocketReturn.current.source);
184
+
185
+ for (const unsubscribe of unsubscribes) {
186
+ // Deregister the event listeners (.on()) that we registered above to avoid memory leaks.
187
+ unsubscribe();
188
+ }
189
+
190
+ setPlaybackStatus("finished");
191
+ }, [playbackStatus, bufferDuration]);
192
+
193
+ const pause = useCallback(async () => {
194
+ await player.current?.pause();
195
+ setPlaybackStatus("paused");
196
+ }, []);
197
+
198
+ const resume = useCallback(async () => {
199
+ await player.current?.resume();
200
+ setPlaybackStatus("playing");
201
+ }, []);
202
+
203
+ const toggle = useCallback(async () => {
204
+ await player.current?.toggle();
205
+ setPlaybackStatus((status) => {
206
+ if (status === "playing") {
207
+ return "paused";
116
208
  }
117
- setIsPlaying(true);
118
- await streamReturn.current?.play({ bufferDuration });
119
- setIsPlaying(false);
120
- },
121
- [isPlaying],
122
- );
209
+ if (status === "paused") {
210
+ return "playing";
211
+ }
212
+ return status;
213
+ });
214
+ }, []);
123
215
 
124
216
  // TODO:
125
- // - [] Pause and stop playback.
126
217
  // - [] Access the play and buffer cursors.
127
218
  // - [] Seek to a specific time.
128
- // These are probably best implemented by adding event listener
129
- // functionality to the base library.
130
219
  return {
131
- stream,
220
+ buffer,
132
221
  play,
133
- download,
134
- isPlaying,
222
+ pause,
223
+ source: websocketReturn.current?.source ?? null,
224
+ resume,
225
+ toggle,
226
+ playbackStatus,
227
+ bufferStatus,
228
+ isWaiting,
135
229
  isConnected,
136
- isStreamed,
137
- chunks,
138
- messages,
230
+ metrics,
139
231
  };
140
232
  }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Ping the server to calculate the round-trip time. This is useful for buffering audio in high-latency environments.
3
+ * @param url The URL to ping.
4
+ */
5
+
6
+ export async function pingServer(url: string): Promise<number> {
7
+ const start = new Date().getTime();
8
+ await fetch(url);
9
+ const end = new Date().getTime();
10
+ return end - start;
11
+ }
@@ -0,0 +1,17 @@
1
+ import { Client } from "../lib/client";
2
+ import type { WebSocketOptions } from "../types";
3
+ import WebSocket from "./websocket";
4
+
5
+ export default class TTS extends Client {
6
+ /**
7
+ * Get a WebSocket client for streaming audio from the TTS API.
8
+ *
9
+ * @returns {WebSocket} A Cartesia WebSocket client.
10
+ */
11
+ websocket(options: WebSocketOptions): WebSocket {
12
+ return new WebSocket(options, {
13
+ apiKey: this.apiKey,
14
+ baseUrl: this.baseUrl,
15
+ });
16
+ }
17
+ }
@@ -0,0 +1,109 @@
1
+ import Emittery from "emittery";
2
+ import type Source from "./source";
3
+ import { playAudioBuffer } from "./utils";
4
+
5
+ type PlayEventData = {
6
+ finish: never;
7
+ };
8
+
9
+ export default class Player {
10
+ #context: AudioContext | null = null;
11
+ #startNextPlaybackAt = 0;
12
+ #bufferDuration: number;
13
+ #emitter = new Emittery<PlayEventData>();
14
+
15
+ /**
16
+ * Create a new Player.
17
+ *
18
+ * @param options - Options for the Player.
19
+ * @param options.bufferDuration - The duration of the audio buffer to play.
20
+ */
21
+ constructor({ bufferDuration }: { bufferDuration: number }) {
22
+ this.#bufferDuration = bufferDuration;
23
+ }
24
+
25
+ async #playBuffer(buf: Float32Array, sampleRate: number) {
26
+ if (!this.#context) {
27
+ throw new Error("AudioContext not initialized.");
28
+ }
29
+
30
+ const startAt = this.#startNextPlaybackAt;
31
+ const duration = buf.length / sampleRate;
32
+ this.#startNextPlaybackAt =
33
+ duration + Math.max(this.#context.currentTime, this.#startNextPlaybackAt);
34
+
35
+ await playAudioBuffer(buf, this.#context, startAt, sampleRate);
36
+ }
37
+
38
+ /**
39
+ * Play audio from a source.
40
+ *
41
+ * @param source The source to play audio from.
42
+ * @returns A promise that resolves when the audio has finished playing.
43
+ */
44
+ async play(source: Source) {
45
+ this.#startNextPlaybackAt = 0;
46
+ this.#context = new AudioContext({ sampleRate: source.sampleRate });
47
+ const buffer = new Float32Array(
48
+ source.durationToSampleCount(this.#bufferDuration),
49
+ );
50
+
51
+ const plays: Promise<void>[] = [];
52
+ while (true) {
53
+ const read = await source.read(buffer);
54
+ // If we've reached the end of the source, then read < buffer.length.
55
+ // In that case, we don't want to play the entire buffer, as that
56
+ // will cause repeated audio.
57
+ const playableAudio = buffer.slice(0, read);
58
+ plays.push(this.#playBuffer(playableAudio, source.sampleRate));
59
+
60
+ if (read < buffer.length) {
61
+ // No more audio to read.
62
+ await this.#emitter.emit("finish");
63
+ break;
64
+ }
65
+ }
66
+
67
+ await Promise.all(plays);
68
+ }
69
+
70
+ /**
71
+ * Pause the audio.
72
+ *
73
+ * @returns A promise that resolves when the audio has been paused.
74
+ */
75
+ async pause() {
76
+ if (!this.#context) {
77
+ throw new Error("AudioContext not initialized.");
78
+ }
79
+ await this.#context.suspend();
80
+ }
81
+
82
+ /**
83
+ * Resume the audio.
84
+ *
85
+ * @returns A promise that resolves when the audio has been resumed.
86
+ */
87
+ async resume() {
88
+ if (!this.#context) {
89
+ throw new Error("AudioContext not initialized.");
90
+ }
91
+ await this.#context.resume();
92
+ }
93
+
94
+ /**
95
+ * Toggle the audio.
96
+ *
97
+ * @returns A promise that resolves when the audio has been toggled.
98
+ */
99
+ async toggle() {
100
+ if (!this.#context) {
101
+ throw new Error("AudioContext not initialized.");
102
+ }
103
+ if (this.#context.state === "running") {
104
+ await this.pause();
105
+ } else {
106
+ await this.resume();
107
+ }
108
+ }
109
+ }
@@ -0,0 +1,98 @@
1
+ import Emittery from "emittery";
2
+ import type { SourceEventData } from "../types";
3
+
4
+ export default class Source {
5
+ #emitter = new Emittery<SourceEventData>();
6
+ #buffer = new Float32Array();
7
+ #readIndex = 0;
8
+ #closed = false;
9
+ #sampleRate: number;
10
+
11
+ on = this.#emitter.on.bind(this.#emitter);
12
+ once = this.#emitter.once.bind(this.#emitter);
13
+ events = this.#emitter.events.bind(this.#emitter);
14
+ off = this.#emitter.off.bind(this.#emitter);
15
+
16
+ /**
17
+ * Create a new Source.
18
+ *
19
+ * @param options - Options for the Source.
20
+ * @param options.sampleRate - The sample rate of the audio.
21
+ */
22
+ constructor({ sampleRate }: { sampleRate: number }) {
23
+ this.#sampleRate = sampleRate;
24
+ }
25
+
26
+ get sampleRate() {
27
+ return this.#sampleRate;
28
+ }
29
+
30
+ /**
31
+ * Append audio to the buffer.
32
+ *
33
+ * @param src The audio to append.
34
+ */
35
+ async enqueue(src: Float32Array) {
36
+ // Append the audio to the buffer.
37
+ this.#buffer = new Float32Array([...this.#buffer, ...src]);
38
+ await this.#emitter.emit("enqueue");
39
+ }
40
+
41
+ /**
42
+ * Read audio from the buffer.
43
+ *
44
+ * @param dst The buffer to read the audio into.
45
+ * @returns The number of samples read. If the source is closed, this will be
46
+ * less than the length of the provided buffer.
47
+ */
48
+ async read(dst: Float32Array): Promise<number> {
49
+ // Read the buffer into the provided buffer.
50
+ const targetReadIndex = this.#readIndex + dst.length;
51
+
52
+ while (!this.#closed && targetReadIndex > this.#buffer.length) {
53
+ // Wait for more audio to be enqueued.
54
+ await this.#emitter.emit("wait");
55
+ await Promise.race([
56
+ this.#emitter.once("enqueue"),
57
+ this.#emitter.once("close"),
58
+ ]);
59
+ await this.#emitter.emit("read");
60
+ }
61
+
62
+ const read = Math.min(dst.length, this.#buffer.length - this.#readIndex);
63
+ dst.set(this.#buffer.slice(this.#readIndex, this.#readIndex + read));
64
+ this.#readIndex += read;
65
+ return read;
66
+ }
67
+
68
+ /**
69
+ * Get the number of samples in a given duration.
70
+ *
71
+ * @param durationSecs The duration in seconds.
72
+ * @returns The number of samples.
73
+ */
74
+ durationToSampleCount(durationSecs: number) {
75
+ return Math.trunc(durationSecs * this.#sampleRate);
76
+ }
77
+
78
+ get buffer() {
79
+ return this.#buffer;
80
+ }
81
+
82
+ get readIndex() {
83
+ return this.#readIndex;
84
+ }
85
+
86
+ /**
87
+ * Close the source. This signals that no more audio will be enqueued.
88
+ *
89
+ * This will emit a "close" event.
90
+ *
91
+ * @returns A promise that resolves when the source is closed.
92
+ */
93
+ async close() {
94
+ this.#closed = true;
95
+ await this.#emitter.emit("close");
96
+ this.#emitter.clearListeners();
97
+ }
98
+ }