@cartesia/cartesia-js 0.0.4-alpha.0 → 1.0.0-alpha.2
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/.turbo/turbo-build.log +63 -45
- package/CHANGELOG.md +12 -0
- package/README.md +123 -16
- package/dist/chunk-36JBKJUN.js +119 -0
- package/dist/chunk-3F5E46FT.js +212 -0
- package/dist/{chunk-XPIMIAAE.js → chunk-3FL2SNIR.js} +1 -1
- package/dist/chunk-JGP5BIUV.js +34 -0
- package/dist/chunk-KWBSQZTY.js +25 -0
- package/dist/chunk-PQ6CIPFW.js +120 -0
- package/dist/chunk-RO7TY474.js +81 -0
- package/dist/chunk-T3RG6WV4.js +22 -0
- package/dist/{chunk-R4P7LWVZ.js → chunk-WIFMLPT5.js} +31 -6
- package/dist/chunk-WVTITUXX.js +58 -0
- package/dist/chunk-XHTDPLFR.js +19 -0
- package/dist/index.cjs +425 -166
- package/dist/index.d.cts +7 -3
- package/dist/index.d.ts +7 -3
- package/dist/index.js +13 -6
- package/dist/lib/client.cjs +49 -1
- package/dist/lib/client.d.cts +2 -0
- package/dist/lib/client.d.ts +2 -0
- package/dist/lib/client.js +3 -3
- package/dist/lib/constants.cjs +15 -8
- package/dist/lib/constants.d.cts +4 -4
- package/dist/lib/constants.d.ts +4 -4
- package/dist/lib/constants.js +6 -6
- package/dist/lib/index.cjs +310 -171
- package/dist/lib/index.d.cts +6 -2
- package/dist/lib/index.d.ts +6 -2
- package/dist/lib/index.js +9 -6
- package/dist/react/index.cjs +573 -290
- package/dist/react/index.d.cts +20 -14
- package/dist/react/index.d.ts +20 -14
- package/dist/react/index.js +157 -105
- package/dist/react/utils.js +2 -2
- package/dist/tts/index.cjs +496 -0
- package/dist/tts/index.d.cts +17 -0
- package/dist/tts/index.d.ts +17 -0
- package/dist/tts/index.js +12 -0
- package/dist/tts/player.cjs +198 -0
- package/dist/tts/player.d.cts +43 -0
- package/dist/tts/player.d.ts +43 -0
- package/dist/tts/player.js +8 -0
- package/dist/tts/source.cjs +181 -0
- package/dist/tts/source.d.cts +53 -0
- package/dist/tts/source.d.ts +53 -0
- package/dist/tts/source.js +7 -0
- package/dist/{audio → tts}/utils.cjs +25 -60
- package/dist/tts/utils.d.cts +67 -0
- package/dist/tts/utils.d.ts +67 -0
- package/dist/{audio → tts}/utils.js +2 -7
- package/dist/tts/websocket.cjs +479 -0
- package/dist/tts/websocket.d.cts +53 -0
- package/dist/tts/websocket.d.ts +53 -0
- package/dist/tts/websocket.js +11 -0
- package/dist/types/index.d.cts +50 -1
- package/dist/types/index.d.ts +50 -1
- package/dist/voices/index.cjs +157 -0
- package/dist/voices/index.d.cts +12 -0
- package/dist/voices/index.d.ts +12 -0
- package/dist/voices/index.js +9 -0
- package/package.json +2 -1
- package/src/index.ts +1 -0
- package/src/lib/client.ts +15 -1
- package/src/lib/constants.ts +15 -4
- package/src/lib/index.ts +6 -3
- package/src/react/index.ts +176 -110
- package/src/tts/index.ts +17 -0
- package/src/tts/player.ts +110 -0
- package/src/tts/source.ts +115 -0
- package/src/tts/utils.ts +150 -0
- package/src/tts/websocket.ts +214 -0
- package/src/types/index.ts +63 -0
- package/src/voices/index.ts +47 -0
- package/dist/audio/index.cjs +0 -404
- package/dist/audio/index.d.cts +0 -5
- package/dist/audio/index.d.ts +0 -5
- package/dist/audio/index.js +0 -10
- package/dist/audio/utils.d.cts +0 -5
- package/dist/audio/utils.d.ts +0 -5
- package/dist/chunk-4MHF74A7.js +0 -272
- package/dist/chunk-5TSWLYOW.js +0 -113
- package/dist/chunk-MJIFZWHS.js +0 -18
- package/dist/chunk-OVI3W3GG.js +0 -12
- package/dist/chunk-S6A27RQL.js +0 -18
- package/dist/index-C2_3XFxn.d.cts +0 -163
- package/dist/index-DgwnZezj.d.ts +0 -163
- package/src/audio/index.ts +0 -297
- package/src/audio/utils.ts +0 -220
package/src/react/index.ts
CHANGED
|
@@ -1,113 +1,164 @@
|
|
|
1
|
+
import type { UnsubscribeFunction } from "emittery";
|
|
1
2
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import
|
|
3
|
+
import { Cartesia } from "../lib";
|
|
4
|
+
import Player from "../tts/player";
|
|
5
|
+
import type Source from "../tts/source";
|
|
6
|
+
import type WebSocket from "../tts/websocket";
|
|
5
7
|
import { pingServer } from "./utils";
|
|
6
8
|
|
|
7
|
-
export type
|
|
9
|
+
export type UseTTSOptions = {
|
|
8
10
|
apiKey: string | null;
|
|
9
11
|
baseUrl?: string;
|
|
12
|
+
sampleRate: number;
|
|
10
13
|
};
|
|
11
14
|
|
|
12
|
-
|
|
13
|
-
|
|
15
|
+
export type PlaybackStatus = "inactive" | "playing" | "paused" | "finished";
|
|
16
|
+
export type BufferStatus = "inactive" | "buffering" | "buffered";
|
|
17
|
+
|
|
18
|
+
export type Metrics = {
|
|
19
|
+
modelLatency: number | null;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export interface UseTTSReturn {
|
|
23
|
+
buffer: (options: object) => Promise<void>;
|
|
14
24
|
play: (bufferDuration?: number) => Promise<void>;
|
|
15
|
-
|
|
16
|
-
|
|
25
|
+
pause: () => Promise<void>;
|
|
26
|
+
resume: () => Promise<void>;
|
|
27
|
+
toggle: () => Promise<void>;
|
|
28
|
+
source: Source | null;
|
|
29
|
+
playbackStatus: PlaybackStatus;
|
|
30
|
+
bufferStatus: BufferStatus;
|
|
31
|
+
isWaiting: boolean;
|
|
17
32
|
isConnected: boolean;
|
|
18
|
-
|
|
19
|
-
isBuffering: boolean;
|
|
20
|
-
chunks: Chunk[];
|
|
21
|
-
messages: StreamEventData["message"][];
|
|
33
|
+
metrics: Metrics;
|
|
22
34
|
}
|
|
35
|
+
|
|
36
|
+
const PING_INTERVAL = 5000;
|
|
37
|
+
const DEFAULT_BUFFER_DURATION = 0.01;
|
|
38
|
+
|
|
39
|
+
type Message = {
|
|
40
|
+
step_time: number;
|
|
41
|
+
};
|
|
42
|
+
|
|
23
43
|
/**
|
|
24
44
|
* React hook to use the Cartesia audio API.
|
|
25
45
|
*/
|
|
26
|
-
export function
|
|
46
|
+
export function useTTS({
|
|
47
|
+
apiKey,
|
|
48
|
+
baseUrl,
|
|
49
|
+
sampleRate,
|
|
50
|
+
}: UseTTSOptions): UseTTSReturn {
|
|
27
51
|
if (typeof window === "undefined") {
|
|
28
52
|
return {
|
|
29
|
-
|
|
53
|
+
buffer: async () => {},
|
|
30
54
|
play: async () => {},
|
|
31
|
-
|
|
55
|
+
pause: async () => {},
|
|
56
|
+
resume: async () => {},
|
|
57
|
+
toggle: async () => {},
|
|
58
|
+
playbackStatus: "inactive",
|
|
59
|
+
bufferStatus: "inactive",
|
|
60
|
+
isWaiting: false,
|
|
61
|
+
source: null,
|
|
32
62
|
isConnected: false,
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
chunks: [],
|
|
37
|
-
messages: [],
|
|
63
|
+
metrics: {
|
|
64
|
+
modelLatency: null,
|
|
65
|
+
},
|
|
38
66
|
};
|
|
39
67
|
}
|
|
40
68
|
|
|
41
|
-
const
|
|
69
|
+
const websocket = useMemo(() => {
|
|
42
70
|
if (!apiKey) {
|
|
43
71
|
return null;
|
|
44
72
|
}
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
const [
|
|
50
|
-
const
|
|
51
|
-
const [
|
|
73
|
+
const cartesia = new Cartesia({ apiKey, baseUrl });
|
|
74
|
+
baseUrl = baseUrl ?? cartesia.baseUrl;
|
|
75
|
+
return cartesia.tts.websocket({ sampleRate });
|
|
76
|
+
}, [apiKey, baseUrl, sampleRate]);
|
|
77
|
+
const websocketReturn = useRef<ReturnType<WebSocket["send"]> | null>(null);
|
|
78
|
+
const player = useRef<Player | null>(null);
|
|
79
|
+
const [playbackStatus, setPlaybackStatus] =
|
|
80
|
+
useState<PlaybackStatus>("inactive");
|
|
81
|
+
const [bufferStatus, setBufferStatus] = useState<BufferStatus>("inactive");
|
|
82
|
+
const [isWaiting, setIsWaiting] = useState(false);
|
|
52
83
|
const [isConnected, setIsConnected] = useState(false);
|
|
53
|
-
const [
|
|
54
|
-
const [messages, setMessages] = useState<
|
|
84
|
+
const [bufferDuration, setBufferDuration] = useState<number | null>(null);
|
|
85
|
+
const [messages, setMessages] = useState<Message[]>([]);
|
|
55
86
|
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
const stream = useCallback(
|
|
87
|
+
const buffer = useCallback(
|
|
59
88
|
async (options: object) => {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
89
|
+
setMessages([]);
|
|
90
|
+
setBufferStatus("buffering");
|
|
91
|
+
websocketReturn.current = websocket?.send(options) ?? null;
|
|
92
|
+
if (!websocketReturn.current) {
|
|
63
93
|
return;
|
|
64
94
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
);
|
|
72
|
-
streamReturn.current.on(
|
|
73
|
-
"message",
|
|
74
|
-
(message: StreamEventData["message"]) => {
|
|
75
|
-
setMessages((messages) => [...messages, message]);
|
|
76
|
-
},
|
|
77
|
-
);
|
|
78
|
-
const { chunks } = await streamReturn.current.once("streamed");
|
|
79
|
-
setChunks(chunks);
|
|
80
|
-
setIsStreamed(true);
|
|
95
|
+
const unsubscribe = websocketReturn.current.on("message", (message) => {
|
|
96
|
+
setMessages((messages) => [...messages, JSON.parse(message)]);
|
|
97
|
+
});
|
|
98
|
+
await websocketReturn.current.source.once("close");
|
|
99
|
+
setBufferStatus("buffered");
|
|
100
|
+
unsubscribe();
|
|
81
101
|
},
|
|
82
|
-
[
|
|
102
|
+
[websocket],
|
|
83
103
|
);
|
|
84
104
|
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
105
|
+
const metrics = useMemo(() => {
|
|
106
|
+
// Model Latency is the first step time
|
|
107
|
+
if (messages.length === 0) {
|
|
108
|
+
return {
|
|
109
|
+
modelLatency: null,
|
|
110
|
+
};
|
|
88
111
|
}
|
|
89
|
-
const
|
|
90
|
-
return
|
|
91
|
-
|
|
112
|
+
const modelLatency = messages[0].step_time ?? null;
|
|
113
|
+
return {
|
|
114
|
+
modelLatency: Math.trunc(modelLatency),
|
|
115
|
+
};
|
|
116
|
+
}, [messages]);
|
|
92
117
|
|
|
93
118
|
useEffect(() => {
|
|
94
119
|
let cleanup: (() => void) | undefined = () => {};
|
|
95
120
|
async function setupConnection() {
|
|
96
121
|
try {
|
|
97
|
-
const connection = await
|
|
122
|
+
const connection = await websocket?.connect();
|
|
98
123
|
if (!connection) {
|
|
99
124
|
return;
|
|
100
125
|
}
|
|
126
|
+
const unsubscribes = <UnsubscribeFunction[]>[];
|
|
127
|
+
// The await ensures that the connection is open, so we already know that we are connected.
|
|
101
128
|
setIsConnected(true);
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
129
|
+
// If the WebSocket is the kind that automatically reconnects, we need an additional
|
|
130
|
+
// listener for the open event to update the connection status.
|
|
131
|
+
unsubscribes.push(
|
|
132
|
+
connection.on("open", () => {
|
|
133
|
+
setIsConnected(true);
|
|
134
|
+
}),
|
|
135
|
+
);
|
|
136
|
+
unsubscribes.push(
|
|
137
|
+
connection.on("close", () => {
|
|
138
|
+
setIsConnected(false);
|
|
139
|
+
}),
|
|
140
|
+
);
|
|
141
|
+
const intervalId = setInterval(() => {
|
|
142
|
+
if (baseUrl) {
|
|
143
|
+
pingServer(new URL(baseUrl).origin).then((ping) => {
|
|
144
|
+
let bufferDuration: number;
|
|
145
|
+
if (ping < 300) {
|
|
146
|
+
bufferDuration = 0.01; // No buffering for very low latency
|
|
147
|
+
} else if (ping > 1500) {
|
|
148
|
+
bufferDuration = 6; // Max buffering for very high latency (6 seconds)
|
|
149
|
+
} else {
|
|
150
|
+
bufferDuration = (ping / 1000) * 4; // Adjust buffer duration based on ping
|
|
151
|
+
}
|
|
152
|
+
setBufferDuration(bufferDuration);
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
}, PING_INTERVAL);
|
|
108
156
|
return () => {
|
|
109
|
-
unsubscribe
|
|
110
|
-
|
|
157
|
+
for (const unsubscribe of unsubscribes) {
|
|
158
|
+
unsubscribe();
|
|
159
|
+
}
|
|
160
|
+
clearInterval(intervalId);
|
|
161
|
+
websocket?.disconnect();
|
|
111
162
|
};
|
|
112
163
|
} catch (e) {
|
|
113
164
|
console.error(e);
|
|
@@ -117,62 +168,77 @@ export function useAudio({ apiKey, baseUrl }: UseAudioOptions): UseAudioReturn {
|
|
|
117
168
|
cleanup = cleanupConnection;
|
|
118
169
|
});
|
|
119
170
|
return () => cleanup?.();
|
|
120
|
-
}, [
|
|
171
|
+
}, [websocket, baseUrl]);
|
|
121
172
|
|
|
122
173
|
const play = useCallback(async () => {
|
|
123
|
-
if (
|
|
174
|
+
if (playbackStatus === "playing" || !websocketReturn.current) {
|
|
124
175
|
return;
|
|
125
176
|
}
|
|
126
|
-
|
|
127
|
-
const ping = await pingServer(latencyEndpoint);
|
|
128
|
-
let bufferingTimeout: ReturnType<typeof setTimeout> | null;
|
|
129
|
-
|
|
130
|
-
let bufferDuration: number;
|
|
131
|
-
if (ping < 300) {
|
|
132
|
-
bufferDuration = 0; // No buffering for very low latency
|
|
133
|
-
} else if (ping > 1500) {
|
|
134
|
-
bufferDuration = 6; // Max buffering for very high latency (6 seconds)
|
|
135
|
-
} else {
|
|
136
|
-
bufferDuration = (ping / 1000) * 4; // Adjust buffer duration based on ping
|
|
137
|
-
}
|
|
177
|
+
setPlaybackStatus("playing");
|
|
138
178
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
179
|
+
const unsubscribes = [];
|
|
180
|
+
unsubscribes.push(
|
|
181
|
+
websocketReturn.current.source.on("wait", () => {
|
|
182
|
+
setIsWaiting(true);
|
|
183
|
+
}),
|
|
184
|
+
);
|
|
185
|
+
unsubscribes.push(
|
|
186
|
+
websocketReturn.current.source.on("read", () => {
|
|
187
|
+
setIsWaiting(false);
|
|
188
|
+
}),
|
|
189
|
+
);
|
|
150
190
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
setTimeout(() => {
|
|
154
|
-
setIsPlaying(false);
|
|
155
|
-
}, data.playbackEndsIn);
|
|
191
|
+
player.current = new Player({
|
|
192
|
+
bufferDuration: bufferDuration ?? DEFAULT_BUFFER_DURATION,
|
|
156
193
|
});
|
|
194
|
+
// Wait for the playback to finish before setting isPlaying to false.
|
|
195
|
+
await player.current.play(websocketReturn.current.source);
|
|
196
|
+
|
|
197
|
+
for (const unsubscribe of unsubscribes) {
|
|
198
|
+
// Deregister the event listeners (.on()) that we registered above to avoid memory leaks.
|
|
199
|
+
unsubscribe();
|
|
200
|
+
}
|
|
157
201
|
|
|
158
|
-
|
|
159
|
-
}, [
|
|
202
|
+
setPlaybackStatus("finished");
|
|
203
|
+
}, [playbackStatus, bufferDuration]);
|
|
204
|
+
|
|
205
|
+
const pause = useCallback(async () => {
|
|
206
|
+
await player.current?.pause();
|
|
207
|
+
setPlaybackStatus("paused");
|
|
208
|
+
}, []);
|
|
209
|
+
|
|
210
|
+
const resume = useCallback(async () => {
|
|
211
|
+
await player.current?.resume();
|
|
212
|
+
setPlaybackStatus("playing");
|
|
213
|
+
}, []);
|
|
214
|
+
|
|
215
|
+
const toggle = useCallback(async () => {
|
|
216
|
+
await player.current?.toggle();
|
|
217
|
+
setPlaybackStatus((status) => {
|
|
218
|
+
if (status === "playing") {
|
|
219
|
+
return "paused";
|
|
220
|
+
}
|
|
221
|
+
if (status === "paused") {
|
|
222
|
+
return "playing";
|
|
223
|
+
}
|
|
224
|
+
return status;
|
|
225
|
+
});
|
|
226
|
+
}, []);
|
|
160
227
|
|
|
161
228
|
// TODO:
|
|
162
|
-
// - [] Pause and stop playback.
|
|
163
229
|
// - [] Access the play and buffer cursors.
|
|
164
230
|
// - [] Seek to a specific time.
|
|
165
|
-
// These are probably best implemented by adding event listener
|
|
166
|
-
// functionality to the base library.
|
|
167
231
|
return {
|
|
168
|
-
|
|
232
|
+
buffer,
|
|
169
233
|
play,
|
|
170
|
-
|
|
171
|
-
|
|
234
|
+
pause,
|
|
235
|
+
source: websocketReturn.current?.source ?? null,
|
|
236
|
+
resume,
|
|
237
|
+
toggle,
|
|
238
|
+
playbackStatus,
|
|
239
|
+
bufferStatus,
|
|
240
|
+
isWaiting,
|
|
172
241
|
isConnected,
|
|
173
|
-
|
|
174
|
-
isBuffering,
|
|
175
|
-
chunks,
|
|
176
|
-
messages,
|
|
242
|
+
metrics,
|
|
177
243
|
};
|
|
178
244
|
}
|
package/src/tts/index.ts
ADDED
|
@@ -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,110 @@
|
|
|
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
|
+
// So we set the buffer to the correct length.
|
|
58
|
+
const playableAudio = buffer.subarray(0, read);
|
|
59
|
+
plays.push(this.#playBuffer(playableAudio, source.sampleRate));
|
|
60
|
+
|
|
61
|
+
if (read < buffer.length) {
|
|
62
|
+
// No more audio to read.
|
|
63
|
+
await this.#emitter.emit("finish");
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
await Promise.all(plays);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Pause the audio.
|
|
73
|
+
*
|
|
74
|
+
* @returns A promise that resolves when the audio has been paused.
|
|
75
|
+
*/
|
|
76
|
+
async pause() {
|
|
77
|
+
if (!this.#context) {
|
|
78
|
+
throw new Error("AudioContext not initialized.");
|
|
79
|
+
}
|
|
80
|
+
await this.#context.suspend();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Resume the audio.
|
|
85
|
+
*
|
|
86
|
+
* @returns A promise that resolves when the audio has been resumed.
|
|
87
|
+
*/
|
|
88
|
+
async resume() {
|
|
89
|
+
if (!this.#context) {
|
|
90
|
+
throw new Error("AudioContext not initialized.");
|
|
91
|
+
}
|
|
92
|
+
await this.#context.resume();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Toggle the audio.
|
|
97
|
+
*
|
|
98
|
+
* @returns A promise that resolves when the audio has been toggled.
|
|
99
|
+
*/
|
|
100
|
+
async toggle() {
|
|
101
|
+
if (!this.#context) {
|
|
102
|
+
throw new Error("AudioContext not initialized.");
|
|
103
|
+
}
|
|
104
|
+
if (this.#context.state === "running") {
|
|
105
|
+
await this.pause();
|
|
106
|
+
} else {
|
|
107
|
+
await this.resume();
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import Emittery from "emittery";
|
|
2
|
+
import type { SourceEventData } from "../types";
|
|
3
|
+
|
|
4
|
+
export default class Source {
|
|
5
|
+
#emitter = new Emittery<SourceEventData>();
|
|
6
|
+
#buffer: Float32Array;
|
|
7
|
+
#readIndex = 0;
|
|
8
|
+
#writeIndex = 0;
|
|
9
|
+
#closed = false;
|
|
10
|
+
#sampleRate: number;
|
|
11
|
+
|
|
12
|
+
on = this.#emitter.on.bind(this.#emitter);
|
|
13
|
+
once = this.#emitter.once.bind(this.#emitter);
|
|
14
|
+
events = this.#emitter.events.bind(this.#emitter);
|
|
15
|
+
off = this.#emitter.off.bind(this.#emitter);
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Create a new Source.
|
|
19
|
+
*
|
|
20
|
+
* @param options - Options for the Source.
|
|
21
|
+
* @param options.sampleRate - The sample rate of the audio.
|
|
22
|
+
*/
|
|
23
|
+
constructor({ sampleRate }: { sampleRate: number }) {
|
|
24
|
+
this.#sampleRate = sampleRate;
|
|
25
|
+
this.#buffer = new Float32Array(1024); // Initial size, can be adjusted
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
get sampleRate() {
|
|
29
|
+
return this.#sampleRate;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Append audio to the buffer.
|
|
34
|
+
*
|
|
35
|
+
* @param src The audio to append.
|
|
36
|
+
*/
|
|
37
|
+
async enqueue(src: Float32Array) {
|
|
38
|
+
const requiredCapacity = this.#writeIndex + src.length;
|
|
39
|
+
|
|
40
|
+
// Resize buffer if necessary
|
|
41
|
+
if (requiredCapacity > this.#buffer.length) {
|
|
42
|
+
let newCapacity = this.#buffer.length;
|
|
43
|
+
while (newCapacity < requiredCapacity) {
|
|
44
|
+
newCapacity *= 2; // Double the buffer size
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const newBuffer = new Float32Array(newCapacity);
|
|
48
|
+
newBuffer.set(this.#buffer);
|
|
49
|
+
this.#buffer = newBuffer;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Append the audio to the buffer.
|
|
53
|
+
this.#buffer.set(src, this.#writeIndex);
|
|
54
|
+
this.#writeIndex += src.length;
|
|
55
|
+
await this.#emitter.emit("enqueue");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Read audio from the buffer.
|
|
60
|
+
*
|
|
61
|
+
* @param dst The buffer to read the audio into.
|
|
62
|
+
* @returns The number of samples read. If the source is closed, this will be
|
|
63
|
+
* less than the length of the provided buffer.
|
|
64
|
+
*/
|
|
65
|
+
async read(dst: Float32Array): Promise<number> {
|
|
66
|
+
// Read the buffer into the provided buffer.
|
|
67
|
+
const targetReadIndex = this.#readIndex + dst.length;
|
|
68
|
+
|
|
69
|
+
while (!this.#closed && targetReadIndex > this.#writeIndex) {
|
|
70
|
+
// Wait for more audio to be enqueued.
|
|
71
|
+
await this.#emitter.emit("wait");
|
|
72
|
+
await Promise.race([
|
|
73
|
+
this.#emitter.once("enqueue"),
|
|
74
|
+
this.#emitter.once("close"),
|
|
75
|
+
]);
|
|
76
|
+
await this.#emitter.emit("read");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const read = Math.min(dst.length, this.#writeIndex - this.#readIndex);
|
|
80
|
+
dst.set(this.#buffer.subarray(this.#readIndex, this.#readIndex + read));
|
|
81
|
+
this.#readIndex += read;
|
|
82
|
+
return read;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Get the number of samples in a given duration.
|
|
87
|
+
*
|
|
88
|
+
* @param durationSecs The duration in seconds.
|
|
89
|
+
* @returns The number of samples.
|
|
90
|
+
*/
|
|
91
|
+
durationToSampleCount(durationSecs: number) {
|
|
92
|
+
return Math.trunc(durationSecs * this.#sampleRate);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
get buffer() {
|
|
96
|
+
return this.#buffer;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
get readIndex() {
|
|
100
|
+
return this.#readIndex;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Close the source. This signals that no more audio will be enqueued.
|
|
105
|
+
*
|
|
106
|
+
* This will emit a "close" event.
|
|
107
|
+
*
|
|
108
|
+
* @returns A promise that resolves when the source is closed.
|
|
109
|
+
*/
|
|
110
|
+
async close() {
|
|
111
|
+
this.#closed = true;
|
|
112
|
+
await this.#emitter.emit("close");
|
|
113
|
+
this.#emitter.clearListeners();
|
|
114
|
+
}
|
|
115
|
+
}
|