@vapi-ai/web 1.0.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/dist/api.js ADDED
@@ -0,0 +1,309 @@
1
+ "use strict";
2
+ /* eslint-disable */
3
+ /* tslint:disable */
4
+ /*
5
+ * ---------------------------------------------------------------
6
+ * ## THIS FILE WAS GENERATED VIA SWAGGER-TYPESCRIPT-API ##
7
+ * ## ##
8
+ * ## AUTHOR: acacode ##
9
+ * ## SOURCE: https://github.com/acacode/swagger-typescript-api ##
10
+ * ---------------------------------------------------------------
11
+ */
12
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
13
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
14
+ return new (P || (P = Promise))(function (resolve, reject) {
15
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
16
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
17
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
18
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
19
+ });
20
+ };
21
+ var __rest = (this && this.__rest) || function (s, e) {
22
+ var t = {};
23
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
24
+ t[p] = s[p];
25
+ if (s != null && typeof Object.getOwnPropertySymbols === "function")
26
+ for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
27
+ if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
28
+ t[p[i]] = s[p[i]];
29
+ }
30
+ return t;
31
+ };
32
+ Object.defineProperty(exports, "__esModule", { value: true });
33
+ exports.Api = exports.HttpClient = exports.ContentType = void 0;
34
+ var ContentType;
35
+ (function (ContentType) {
36
+ ContentType["Json"] = "application/json";
37
+ ContentType["FormData"] = "multipart/form-data";
38
+ ContentType["UrlEncoded"] = "application/x-www-form-urlencoded";
39
+ ContentType["Text"] = "text/plain";
40
+ })(ContentType || (exports.ContentType = ContentType = {}));
41
+ class HttpClient {
42
+ constructor(apiConfig = {}) {
43
+ this.baseUrl = "";
44
+ this.securityData = null;
45
+ this.abortControllers = new Map();
46
+ this.customFetch = (...fetchParams) => fetch(...fetchParams);
47
+ this.baseApiParams = {
48
+ credentials: "same-origin",
49
+ headers: {},
50
+ redirect: "follow",
51
+ referrerPolicy: "no-referrer",
52
+ };
53
+ this.setSecurityData = (data) => {
54
+ this.securityData = data;
55
+ };
56
+ this.contentFormatters = {
57
+ [ContentType.Json]: (input) => input !== null && (typeof input === "object" || typeof input === "string") ? JSON.stringify(input) : input,
58
+ [ContentType.Text]: (input) => (input !== null && typeof input !== "string" ? JSON.stringify(input) : input),
59
+ [ContentType.FormData]: (input) => Object.keys(input || {}).reduce((formData, key) => {
60
+ const property = input[key];
61
+ formData.append(key, property instanceof Blob
62
+ ? property
63
+ : typeof property === "object" && property !== null
64
+ ? JSON.stringify(property)
65
+ : `${property}`);
66
+ return formData;
67
+ }, new FormData()),
68
+ [ContentType.UrlEncoded]: (input) => this.toQueryString(input),
69
+ };
70
+ this.createAbortSignal = (cancelToken) => {
71
+ if (this.abortControllers.has(cancelToken)) {
72
+ const abortController = this.abortControllers.get(cancelToken);
73
+ if (abortController) {
74
+ return abortController.signal;
75
+ }
76
+ return void 0;
77
+ }
78
+ const abortController = new AbortController();
79
+ this.abortControllers.set(cancelToken, abortController);
80
+ return abortController.signal;
81
+ };
82
+ this.abortRequest = (cancelToken) => {
83
+ const abortController = this.abortControllers.get(cancelToken);
84
+ if (abortController) {
85
+ abortController.abort();
86
+ this.abortControllers.delete(cancelToken);
87
+ }
88
+ };
89
+ this.request = (_a) => __awaiter(this, void 0, void 0, function* () {
90
+ var { body, secure, path, type, query, format, baseUrl, cancelToken } = _a, params = __rest(_a, ["body", "secure", "path", "type", "query", "format", "baseUrl", "cancelToken"]);
91
+ const secureParams = ((typeof secure === "boolean" ? secure : this.baseApiParams.secure) &&
92
+ this.securityWorker &&
93
+ (yield this.securityWorker(this.securityData))) ||
94
+ {};
95
+ const requestParams = this.mergeRequestParams(params, secureParams);
96
+ const queryString = query && this.toQueryString(query);
97
+ const payloadFormatter = this.contentFormatters[type || ContentType.Json];
98
+ const responseFormat = format || requestParams.format;
99
+ return this.customFetch(`${baseUrl || this.baseUrl || ""}${path}${queryString ? `?${queryString}` : ""}`, Object.assign(Object.assign({}, requestParams), { headers: Object.assign(Object.assign({}, (requestParams.headers || {})), (type && type !== ContentType.FormData ? { "Content-Type": type } : {})), signal: (cancelToken ? this.createAbortSignal(cancelToken) : requestParams.signal) || null, body: typeof body === "undefined" || body === null ? null : payloadFormatter(body) })).then((response) => __awaiter(this, void 0, void 0, function* () {
100
+ const r = response;
101
+ r.data = null;
102
+ r.error = null;
103
+ const data = !responseFormat
104
+ ? r
105
+ : yield response[responseFormat]()
106
+ .then((data) => {
107
+ if (r.ok) {
108
+ r.data = data;
109
+ }
110
+ else {
111
+ r.error = data;
112
+ }
113
+ return r;
114
+ })
115
+ .catch((e) => {
116
+ r.error = e;
117
+ return r;
118
+ });
119
+ if (cancelToken) {
120
+ this.abortControllers.delete(cancelToken);
121
+ }
122
+ if (!response.ok)
123
+ throw data;
124
+ return data;
125
+ }));
126
+ });
127
+ Object.assign(this, apiConfig);
128
+ }
129
+ encodeQueryParam(key, value) {
130
+ const encodedKey = encodeURIComponent(key);
131
+ return `${encodedKey}=${encodeURIComponent(typeof value === "number" ? value : `${value}`)}`;
132
+ }
133
+ addQueryParam(query, key) {
134
+ return this.encodeQueryParam(key, query[key]);
135
+ }
136
+ addArrayQueryParam(query, key) {
137
+ const value = query[key];
138
+ return value.map((v) => this.encodeQueryParam(key, v)).join("&");
139
+ }
140
+ toQueryString(rawQuery) {
141
+ const query = rawQuery || {};
142
+ const keys = Object.keys(query).filter((key) => "undefined" !== typeof query[key]);
143
+ return keys
144
+ .map((key) => (Array.isArray(query[key]) ? this.addArrayQueryParam(query, key) : this.addQueryParam(query, key)))
145
+ .join("&");
146
+ }
147
+ addQueryParams(rawQuery) {
148
+ const queryString = this.toQueryString(rawQuery);
149
+ return queryString ? `?${queryString}` : "";
150
+ }
151
+ mergeRequestParams(params1, params2) {
152
+ return Object.assign(Object.assign(Object.assign(Object.assign({}, this.baseApiParams), params1), (params2 || {})), { headers: Object.assign(Object.assign(Object.assign({}, (this.baseApiParams.headers || {})), (params1.headers || {})), ((params2 && params2.headers) || {})) });
153
+ }
154
+ }
155
+ exports.HttpClient = HttpClient;
156
+ /**
157
+ * @title Vapi API
158
+ * @version 1.0
159
+ * @contact
160
+ *
161
+ * API for talking AIs
162
+ */
163
+ class Api extends HttpClient {
164
+ constructor() {
165
+ super(...arguments);
166
+ this.call = {
167
+ /**
168
+ * No description
169
+ *
170
+ * @tags Calls
171
+ * @name CallControllerFindAll
172
+ * @summary List Calls
173
+ * @request GET:/call
174
+ */
175
+ callControllerFindAll: (params = {}) => this.request(Object.assign({ path: `/call`, method: "GET", format: "json" }, params)),
176
+ /**
177
+ * No description
178
+ *
179
+ * @tags Calls
180
+ * @name CallControllerFindOne
181
+ * @summary Get Call
182
+ * @request GET:/call/{id}
183
+ */
184
+ callControllerFindOne: (id, params = {}) => this.request(Object.assign({ path: `/call/${id}`, method: "GET", format: "json" }, params)),
185
+ /**
186
+ * No description
187
+ *
188
+ * @tags Calls
189
+ * @name CallControllerCreatePhoneCall
190
+ * @summary Create Phone Call
191
+ * @request POST:/call/phone
192
+ */
193
+ callControllerCreatePhoneCall: (data, params = {}) => this.request(Object.assign({ path: `/call/phone`, method: "POST", body: data, type: ContentType.Json, format: "json" }, params)),
194
+ /**
195
+ * No description
196
+ *
197
+ * @tags Calls
198
+ * @name CallControllerCreateWebCall
199
+ * @summary Create Web Call
200
+ * @request POST:/call/web
201
+ */
202
+ callControllerCreateWebCall: (data, params = {}) => this.request(Object.assign({ path: `/call/web`, method: "POST", body: data, type: ContentType.Json, format: "json" }, params)),
203
+ };
204
+ this.agent = {
205
+ /**
206
+ * No description
207
+ *
208
+ * @tags Agents
209
+ * @name AgentControllerCreate
210
+ * @summary Create Agent
211
+ * @request POST:/agent
212
+ */
213
+ agentControllerCreate: (data, params = {}) => this.request(Object.assign({ path: `/agent`, method: "POST", body: data, type: ContentType.Json, format: "json" }, params)),
214
+ /**
215
+ * No description
216
+ *
217
+ * @tags Agents
218
+ * @name AgentControllerFindAll
219
+ * @summary List Agents
220
+ * @request GET:/agent
221
+ */
222
+ agentControllerFindAll: (params = {}) => this.request(Object.assign({ path: `/agent`, method: "GET", format: "json" }, params)),
223
+ /**
224
+ * No description
225
+ *
226
+ * @tags Agents
227
+ * @name AgentControllerFindOne
228
+ * @summary Get Agent
229
+ * @request GET:/agent/{id}
230
+ */
231
+ agentControllerFindOne: (id, params = {}) => this.request(Object.assign({ path: `/agent/${id}`, method: "GET", format: "json" }, params)),
232
+ /**
233
+ * No description
234
+ *
235
+ * @tags Agents
236
+ * @name AgentControllerUpdate
237
+ * @summary Update Agent
238
+ * @request PATCH:/agent/{id}
239
+ */
240
+ agentControllerUpdate: (id, data, params = {}) => this.request(Object.assign({ path: `/agent/${id}`, method: "PATCH", body: data, type: ContentType.Json, format: "json" }, params)),
241
+ /**
242
+ * No description
243
+ *
244
+ * @tags Agents
245
+ * @name AgentControllerRemove
246
+ * @summary Delete Agent
247
+ * @request DELETE:/agent/{id}
248
+ */
249
+ agentControllerRemove: (id, params = {}) => this.request(Object.assign({ path: `/agent/${id}`, method: "DELETE" }, params)),
250
+ };
251
+ this.phoneNumber = {
252
+ /**
253
+ * No description
254
+ *
255
+ * @tags Phone Numbers
256
+ * @name PhoneNumberControllerBuy
257
+ * @summary Buy Phone Number
258
+ * @request POST:/phone-number/buy
259
+ */
260
+ phoneNumberControllerBuy: (data, params = {}) => this.request(Object.assign({ path: `/phone-number/buy`, method: "POST", body: data, type: ContentType.Json, format: "json" }, params)),
261
+ /**
262
+ * No description
263
+ *
264
+ * @tags Phone Numbers
265
+ * @name PhoneNumberControllerImport
266
+ * @summary Import Twilio Number
267
+ * @request POST:/phone-number/import
268
+ */
269
+ phoneNumberControllerImport: (data, params = {}) => this.request(Object.assign({ path: `/phone-number/import`, method: "POST", body: data, type: ContentType.Json, format: "json" }, params)),
270
+ /**
271
+ * No description
272
+ *
273
+ * @tags Phone Numbers
274
+ * @name PhoneNumberControllerFindAll
275
+ * @summary List Phone Numbers
276
+ * @request GET:/phone-number
277
+ */
278
+ phoneNumberControllerFindAll: (params = {}) => this.request(Object.assign({ path: `/phone-number`, method: "GET", format: "json" }, params)),
279
+ /**
280
+ * No description
281
+ *
282
+ * @tags Phone Numbers
283
+ * @name PhoneNumberControllerFindOne
284
+ * @summary Get Phone Number
285
+ * @request GET:/phone-number/{id}
286
+ */
287
+ phoneNumberControllerFindOne: (id, params = {}) => this.request(Object.assign({ path: `/phone-number/${id}`, method: "GET", format: "json" }, params)),
288
+ /**
289
+ * No description
290
+ *
291
+ * @tags Phone Numbers
292
+ * @name PhoneNumberControllerUpdate
293
+ * @summary Update Phone Number
294
+ * @request PATCH:/phone-number/{id}
295
+ */
296
+ phoneNumberControllerUpdate: (id, data, params = {}) => this.request(Object.assign({ path: `/phone-number/${id}`, method: "PATCH", body: data, type: ContentType.Json, format: "json" }, params)),
297
+ /**
298
+ * No description
299
+ *
300
+ * @tags Phone Numbers
301
+ * @name PhoneNumberControllerRemove
302
+ * @summary Delete Phone Number
303
+ * @request DELETE:/phone-number/{id}
304
+ */
305
+ phoneNumberControllerRemove: (id, params = {}) => this.request(Object.assign({ path: `/phone-number/${id}`, method: "DELETE" }, params)),
306
+ };
307
+ }
308
+ }
309
+ exports.Api = Api;
@@ -0,0 +1,2 @@
1
+ import { Api } from "./api";
2
+ export declare const client: Api<unknown>;
package/dist/client.js ADDED
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.client = void 0;
13
+ const api_1 = require("./api");
14
+ const api = new api_1.Api({
15
+ baseUrl: "https://api.vapi.ai",
16
+ baseApiParams: {
17
+ secure: true,
18
+ },
19
+ securityWorker: (securityData) => __awaiter(void 0, void 0, void 0, function* () {
20
+ if (securityData) {
21
+ return {
22
+ headers: {
23
+ Authorization: `Bearer ${securityData}`,
24
+ },
25
+ };
26
+ }
27
+ }),
28
+ });
29
+ exports.client = api;
@@ -0,0 +1,10 @@
1
+ export declare class ContinuousPlayer {
2
+ private audioCtx;
3
+ private nextStartTime;
4
+ private audioQueue;
5
+ private crossFadeDuration;
6
+ constructor(crossFadeDuration?: number);
7
+ private createQueue;
8
+ playChunk(audioData: ArrayBuffer): void;
9
+ clear(): Promise<void>;
10
+ }
package/dist/player.js ADDED
@@ -0,0 +1,56 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.ContinuousPlayer = void 0;
13
+ const async_1 = require("async"); // You need to install the async package
14
+ class ContinuousPlayer {
15
+ constructor(crossFadeDuration = 0.5) {
16
+ this.audioCtx = new AudioContext();
17
+ this.nextStartTime = 0;
18
+ this.audioQueue = this.createQueue();
19
+ this.crossFadeDuration = crossFadeDuration;
20
+ }
21
+ createQueue() {
22
+ return (0, async_1.queue)((audioPromise, callback) => __awaiter(this, void 0, void 0, function* () {
23
+ const audioBuffer = yield audioPromise;
24
+ const source = this.audioCtx.createBufferSource();
25
+ source.buffer = audioBuffer;
26
+ source.connect(this.audioCtx.destination);
27
+ // Create a gain node for crossfading
28
+ const gainNode = this.audioCtx.createGain();
29
+ source.connect(gainNode);
30
+ gainNode.connect(this.audioCtx.destination);
31
+ // Start the source at the next start time
32
+ source.start(this.nextStartTime);
33
+ // Schedule the crossfade
34
+ gainNode.gain.setValueAtTime(0, this.nextStartTime);
35
+ gainNode.gain.linearRampToValueAtTime(1, this.nextStartTime + this.crossFadeDuration);
36
+ gainNode.gain.setValueAtTime(1, this.nextStartTime + audioBuffer.duration - this.crossFadeDuration);
37
+ gainNode.gain.linearRampToValueAtTime(0, this.nextStartTime + audioBuffer.duration);
38
+ source.onended = () => {
39
+ this.nextStartTime += audioBuffer.duration;
40
+ callback();
41
+ };
42
+ }), 1);
43
+ }
44
+ playChunk(audioData) {
45
+ const decodePromise = this.audioCtx.decodeAudioData(audioData);
46
+ this.audioQueue.push(decodePromise);
47
+ }
48
+ clear() {
49
+ return __awaiter(this, void 0, void 0, function* () {
50
+ this.audioQueue.kill();
51
+ this.audioQueue = this.createQueue();
52
+ this.nextStartTime = 0;
53
+ });
54
+ }
55
+ }
56
+ exports.ContinuousPlayer = ContinuousPlayer;
package/dist/vapi.d.ts ADDED
@@ -0,0 +1,13 @@
1
+ import { CreateAgentDTO } from "./api";
2
+ export default class Vapi {
3
+ private started;
4
+ private ws;
5
+ private mediaRecorder;
6
+ private player;
7
+ constructor(apiToken: string);
8
+ start(agent: CreateAgentDTO): void;
9
+ private startRecording;
10
+ private onMessage;
11
+ clear(): void;
12
+ stop(): void;
13
+ }
package/dist/vapi.js ADDED
@@ -0,0 +1,82 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const player_1 = require("./player");
4
+ const client_1 = require("./client");
5
+ const base64_arraybuffer_1 = require("base64-arraybuffer");
6
+ class Vapi {
7
+ constructor(apiToken) {
8
+ this.started = false;
9
+ this.ws = null;
10
+ this.mediaRecorder = null;
11
+ this.player = new player_1.ContinuousPlayer();
12
+ client_1.client.setSecurityData(apiToken);
13
+ }
14
+ start(agent) {
15
+ if (this.started) {
16
+ return;
17
+ }
18
+ this.started = true;
19
+ client_1.client.call
20
+ .callControllerCreateWebCall({
21
+ agent,
22
+ })
23
+ .then(({ data }) => {
24
+ const { callId, url } = data;
25
+ const socket = new WebSocket(url);
26
+ this.ws = socket;
27
+ socket.onopen = () => {
28
+ socket.send(JSON.stringify({ event: "start", callId }));
29
+ this.startRecording();
30
+ };
31
+ socket.onmessage = (event) => {
32
+ if (!socket)
33
+ return;
34
+ this.onMessage(socket, event);
35
+ };
36
+ socket.onclose = () => {
37
+ this.stop();
38
+ };
39
+ })
40
+ .catch((error) => {
41
+ console.error(error);
42
+ });
43
+ }
44
+ startRecording() {
45
+ navigator.mediaDevices.getUserMedia({ audio: true }).then((stream) => {
46
+ this.mediaRecorder = new MediaRecorder(stream, {
47
+ mimeType: "audio/webm;codecs=opus",
48
+ });
49
+ this.mediaRecorder.start(100);
50
+ this.mediaRecorder.ondataavailable = (event) => {
51
+ if (this.ws && this.ws.readyState === WebSocket.OPEN) {
52
+ this.ws.send(event.data);
53
+ }
54
+ };
55
+ });
56
+ }
57
+ onMessage(ws, event) {
58
+ const data = JSON.parse(event.data);
59
+ if (data.event === "media") {
60
+ const audioData = (0, base64_arraybuffer_1.decode)(data.media.payload);
61
+ this.player.playChunk(audioData);
62
+ }
63
+ if (data.event === "clear") {
64
+ this.clear();
65
+ }
66
+ }
67
+ clear() {
68
+ this.player.clear();
69
+ }
70
+ stop() {
71
+ this.started = false;
72
+ if (this.ws) {
73
+ this.ws.close();
74
+ }
75
+ this.ws = null;
76
+ if (this.mediaRecorder) {
77
+ this.mediaRecorder.stop();
78
+ }
79
+ this.mediaRecorder = null;
80
+ }
81
+ }
82
+ exports.default = Vapi;
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@vapi-ai/web",
3
+ "version": "1.0.0",
4
+ "description": "",
5
+ "main": "dist/vapi.js",
6
+ "types": "dist/vapi.d.ts",
7
+ "scripts": {
8
+ "test": "echo \"Error: no test specified\" && exit 1",
9
+ "build": "tsc && tsc --declaration",
10
+ "start": "npm run generate-api && npm run build && node dist/vapi.js",
11
+ "generate-api": "npx swagger-typescript-api -p https://api-tv2l.onrender.com/api-json -o . -n api.ts"
12
+ },
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+https://github.com/VapiAI/snippet.git"
16
+ },
17
+ "author": "vapi",
18
+ "license": "ISC",
19
+ "bugs": {
20
+ "url": "https://github.com/VapiAI/snippet/issues"
21
+ },
22
+ "homepage": "https://github.com/VapiAI/snippet#readme",
23
+ "dependencies": {
24
+ "async": "^3.2.4",
25
+ "base64-arraybuffer": "^1.0.2",
26
+ "extendable-media-recorder": "^9.1.0",
27
+ "extendable-media-recorder-wav-encoder": "^7.0.96",
28
+ "msr": "^1.3.4",
29
+ "openai": "^4.0.1",
30
+ "standardized-audio-context": "^25.3.56",
31
+ "swagger-typescript-api": "^13.0.3"
32
+ },
33
+ "devDependencies": {
34
+ "@types/async": "^3.2.21",
35
+ "@types/node": "^20.6.0",
36
+ "@types/ws": "^8.5.5"
37
+ }
38
+ }
package/player.ts ADDED
@@ -0,0 +1,66 @@
1
+ import { QueueObject, queue } from "async"; // You need to install the async package
2
+
3
+ export class ContinuousPlayer {
4
+ private audioCtx: AudioContext;
5
+ private nextStartTime: number;
6
+ private audioQueue: QueueObject<Promise<AudioBuffer>>;
7
+ private crossFadeDuration: number; // Duration of the crossfade
8
+
9
+ constructor(crossFadeDuration = 0.5) {
10
+ this.audioCtx = new AudioContext();
11
+ this.nextStartTime = 0;
12
+ this.audioQueue = this.createQueue();
13
+ this.crossFadeDuration = crossFadeDuration;
14
+ }
15
+
16
+ private createQueue() {
17
+ return queue(
18
+ async (audioPromise: Promise<AudioBuffer>, callback: Function) => {
19
+ const audioBuffer = await audioPromise;
20
+ const source = this.audioCtx.createBufferSource();
21
+ source.buffer = audioBuffer;
22
+ source.connect(this.audioCtx.destination);
23
+
24
+ // Create a gain node for crossfading
25
+ const gainNode = this.audioCtx.createGain();
26
+ source.connect(gainNode);
27
+ gainNode.connect(this.audioCtx.destination);
28
+
29
+ // Start the source at the next start time
30
+ source.start(this.nextStartTime);
31
+
32
+ // Schedule the crossfade
33
+ gainNode.gain.setValueAtTime(0, this.nextStartTime);
34
+ gainNode.gain.linearRampToValueAtTime(
35
+ 1,
36
+ this.nextStartTime + this.crossFadeDuration
37
+ );
38
+ gainNode.gain.setValueAtTime(
39
+ 1,
40
+ this.nextStartTime + audioBuffer.duration - this.crossFadeDuration
41
+ );
42
+ gainNode.gain.linearRampToValueAtTime(
43
+ 0,
44
+ this.nextStartTime + audioBuffer.duration
45
+ );
46
+
47
+ source.onended = () => {
48
+ this.nextStartTime += audioBuffer.duration;
49
+ callback();
50
+ };
51
+ },
52
+ 1
53
+ );
54
+ }
55
+
56
+ playChunk(audioData: ArrayBuffer) {
57
+ const decodePromise = this.audioCtx.decodeAudioData(audioData);
58
+ this.audioQueue.push(decodePromise);
59
+ }
60
+
61
+ async clear() {
62
+ this.audioQueue.kill();
63
+ this.audioQueue = this.createQueue();
64
+ this.nextStartTime = 0;
65
+ }
66
+ }