@layercode/js-sdk 2.1.2 → 2.1.3
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.
|
@@ -802,6 +802,8 @@ class AudioProcessor extends AudioWorkletProcessor {
|
|
|
802
802
|
this.foundAudio = false;
|
|
803
803
|
this.recording = false;
|
|
804
804
|
this.chunks = [];
|
|
805
|
+
this.downsampleRatio = 1;
|
|
806
|
+
this.downsampleOffset = 0;
|
|
805
807
|
}
|
|
806
808
|
|
|
807
809
|
/**
|
|
@@ -908,9 +910,12 @@ class AudioProcessor extends AudioWorkletProcessor {
|
|
|
908
910
|
}
|
|
909
911
|
|
|
910
912
|
receive(e) {
|
|
911
|
-
const { event, id } = e.data;
|
|
913
|
+
const { event, id, data } = e.data;
|
|
912
914
|
let receiptData = {};
|
|
913
915
|
switch (event) {
|
|
916
|
+
case 'configure':
|
|
917
|
+
this.configure(data);
|
|
918
|
+
return;
|
|
914
919
|
case 'start':
|
|
915
920
|
this.recording = true;
|
|
916
921
|
break;
|
|
@@ -933,6 +938,24 @@ class AudioProcessor extends AudioWorkletProcessor {
|
|
|
933
938
|
this.port.postMessage({ event: 'receipt', id, data: receiptData });
|
|
934
939
|
}
|
|
935
940
|
|
|
941
|
+
configure(config = {}) {
|
|
942
|
+
const inputSampleRate = config?.inputSampleRate;
|
|
943
|
+
const targetSampleRate = config?.targetSampleRate;
|
|
944
|
+
if (
|
|
945
|
+
typeof inputSampleRate === 'number' &&
|
|
946
|
+
inputSampleRate > 0 &&
|
|
947
|
+
typeof targetSampleRate === 'number' &&
|
|
948
|
+
targetSampleRate > 0
|
|
949
|
+
) {
|
|
950
|
+
if (inputSampleRate <= targetSampleRate) {
|
|
951
|
+
this.downsampleRatio = 1;
|
|
952
|
+
} else {
|
|
953
|
+
this.downsampleRatio = inputSampleRate / targetSampleRate;
|
|
954
|
+
}
|
|
955
|
+
this.downsampleOffset = 0;
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
|
|
936
959
|
sendChunk(chunk) {
|
|
937
960
|
const channels = this.readChannelData([chunk]);
|
|
938
961
|
const { float32Array, meanValues } = this.formatAudioData(channels);
|
|
@@ -985,14 +1008,40 @@ class AudioProcessor extends AudioWorkletProcessor {
|
|
|
985
1008
|
}
|
|
986
1009
|
}
|
|
987
1010
|
if (inputs && inputs[0] && this.foundAudio && this.recording) {
|
|
988
|
-
// We need to copy the TypedArray, because the
|
|
1011
|
+
// We need to copy the TypedArray, because the \`process\`
|
|
989
1012
|
// internals will reuse the same buffer to hold each input
|
|
990
1013
|
const chunk = inputs.map((input) => input.slice(sliceIndex));
|
|
991
|
-
this.
|
|
992
|
-
|
|
1014
|
+
const processedChunk = this.downsampleChunk(chunk);
|
|
1015
|
+
if (processedChunk[0] && processedChunk[0].length) {
|
|
1016
|
+
this.chunks.push(processedChunk);
|
|
1017
|
+
this.sendChunk(processedChunk);
|
|
1018
|
+
}
|
|
993
1019
|
}
|
|
994
1020
|
return true;
|
|
995
1021
|
}
|
|
1022
|
+
|
|
1023
|
+
downsampleChunk(chunk) {
|
|
1024
|
+
if (this.downsampleRatio === 1) {
|
|
1025
|
+
return chunk;
|
|
1026
|
+
}
|
|
1027
|
+
const channelCount = chunk.length;
|
|
1028
|
+
if (!channelCount || !chunk[0]?.length) {
|
|
1029
|
+
return chunk;
|
|
1030
|
+
}
|
|
1031
|
+
const ratio = this.downsampleRatio;
|
|
1032
|
+
const inputLength = chunk[0].length;
|
|
1033
|
+
const outputs = Array.from({ length: channelCount }, () => []);
|
|
1034
|
+
let offset = this.downsampleOffset;
|
|
1035
|
+
while (offset < inputLength) {
|
|
1036
|
+
const sampleIndex = Math.floor(offset);
|
|
1037
|
+
for (let c = 0; c < channelCount; c++) {
|
|
1038
|
+
outputs[c].push(chunk[c][sampleIndex]);
|
|
1039
|
+
}
|
|
1040
|
+
offset += ratio;
|
|
1041
|
+
}
|
|
1042
|
+
this.downsampleOffset = offset - inputLength;
|
|
1043
|
+
return outputs.map((samples) => Float32Array.from(samples));
|
|
1044
|
+
}
|
|
996
1045
|
}
|
|
997
1046
|
|
|
998
1047
|
registerProcessor('audio_processor', AudioProcessor);
|
|
@@ -1038,10 +1087,13 @@ class WavRecorder {
|
|
|
1038
1087
|
this._devices = [];
|
|
1039
1088
|
// State variables
|
|
1040
1089
|
this.stream = null;
|
|
1090
|
+
this.audioContext = null;
|
|
1041
1091
|
this.processor = null;
|
|
1042
1092
|
this.source = null;
|
|
1043
1093
|
this.node = null;
|
|
1094
|
+
this.analyser = null;
|
|
1044
1095
|
this.recording = false;
|
|
1096
|
+
this.contextSampleRate = sampleRate;
|
|
1045
1097
|
// Event handling with AudioWorklet
|
|
1046
1098
|
this._lastEventId = 0;
|
|
1047
1099
|
this.eventReceipts = {};
|
|
@@ -1250,20 +1302,53 @@ class WavRecorder {
|
|
|
1250
1302
|
* @returns {Promise<true>}
|
|
1251
1303
|
*/
|
|
1252
1304
|
async requestPermission() {
|
|
1253
|
-
const
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1305
|
+
const ensureUserMediaAccess = async () => {
|
|
1306
|
+
const stream = await navigator.mediaDevices.getUserMedia({
|
|
1307
|
+
audio: true,
|
|
1308
|
+
});
|
|
1309
|
+
const tracks = stream.getTracks();
|
|
1310
|
+
tracks.forEach((track) => track.stop());
|
|
1311
|
+
};
|
|
1312
|
+
|
|
1313
|
+
const permissionsUnsupported =
|
|
1314
|
+
!navigator.permissions ||
|
|
1315
|
+
typeof navigator.permissions.query !== 'function';
|
|
1316
|
+
|
|
1317
|
+
if (permissionsUnsupported) {
|
|
1259
1318
|
try {
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1319
|
+
await ensureUserMediaAccess();
|
|
1320
|
+
} catch (error) {
|
|
1321
|
+
window.alert('You must grant microphone access to use this feature.');
|
|
1322
|
+
throw error;
|
|
1323
|
+
}
|
|
1324
|
+
return true;
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
try {
|
|
1328
|
+
const permissionStatus = await navigator.permissions.query({
|
|
1329
|
+
name: 'microphone',
|
|
1330
|
+
});
|
|
1331
|
+
|
|
1332
|
+
if (permissionStatus.state === 'denied') {
|
|
1333
|
+
window.alert('You must grant microphone access to use this feature.');
|
|
1334
|
+
return true;
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
if (permissionStatus.state === 'prompt') {
|
|
1338
|
+
try {
|
|
1339
|
+
await ensureUserMediaAccess();
|
|
1340
|
+
} catch (error) {
|
|
1341
|
+
window.alert('You must grant microphone access to use this feature.');
|
|
1342
|
+
throw error;
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
} catch (error) {
|
|
1346
|
+
// Firefox rejects permissions.query with NotSupportedError – fall back to getUserMedia directly
|
|
1347
|
+
try {
|
|
1348
|
+
await ensureUserMediaAccess();
|
|
1349
|
+
} catch (fallbackError) {
|
|
1266
1350
|
window.alert('You must grant microphone access to use this feature.');
|
|
1351
|
+
throw fallbackError;
|
|
1267
1352
|
}
|
|
1268
1353
|
}
|
|
1269
1354
|
return true;
|
|
@@ -1299,6 +1384,10 @@ class WavRecorder {
|
|
|
1299
1384
|
}
|
|
1300
1385
|
defaultDevice.default = true;
|
|
1301
1386
|
deviceList.push(defaultDevice);
|
|
1387
|
+
} else if (audioDevices.length) {
|
|
1388
|
+
const fallbackDefault = audioDevices.shift();
|
|
1389
|
+
fallbackDefault.default = true;
|
|
1390
|
+
deviceList.push(fallbackDefault);
|
|
1302
1391
|
}
|
|
1303
1392
|
return deviceList.concat(audioDevices);
|
|
1304
1393
|
}
|
|
@@ -1340,8 +1429,36 @@ class WavRecorder {
|
|
|
1340
1429
|
throw new Error('Could not start media stream');
|
|
1341
1430
|
}
|
|
1342
1431
|
|
|
1343
|
-
const
|
|
1344
|
-
|
|
1432
|
+
const createContext = (rate) => {
|
|
1433
|
+
try {
|
|
1434
|
+
return rate ? new AudioContext({ sampleRate: rate }) : new AudioContext();
|
|
1435
|
+
} catch (error) {
|
|
1436
|
+
console.warn('Failed to create AudioContext with sampleRate', rate, error);
|
|
1437
|
+
return null;
|
|
1438
|
+
}
|
|
1439
|
+
};
|
|
1440
|
+
|
|
1441
|
+
let context = createContext(this.sampleRate);
|
|
1442
|
+
if (!context) {
|
|
1443
|
+
context = createContext();
|
|
1444
|
+
}
|
|
1445
|
+
if (!context) {
|
|
1446
|
+
throw new Error('Could not create AudioContext');
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
let source;
|
|
1450
|
+
try {
|
|
1451
|
+
source = context.createMediaStreamSource(this.stream);
|
|
1452
|
+
} catch (error) {
|
|
1453
|
+
await context.close().catch(() => {});
|
|
1454
|
+
context = createContext();
|
|
1455
|
+
if (!context) {
|
|
1456
|
+
throw error;
|
|
1457
|
+
}
|
|
1458
|
+
source = context.createMediaStreamSource(this.stream);
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
this.contextSampleRate = context.sampleRate;
|
|
1345
1462
|
// Load and execute the module script.
|
|
1346
1463
|
try {
|
|
1347
1464
|
await context.audioWorklet.addModule(this.scriptSrc);
|
|
@@ -1377,6 +1494,14 @@ class WavRecorder {
|
|
|
1377
1494
|
}
|
|
1378
1495
|
};
|
|
1379
1496
|
|
|
1497
|
+
processor.port.postMessage({
|
|
1498
|
+
event: 'configure',
|
|
1499
|
+
data: {
|
|
1500
|
+
inputSampleRate: this.contextSampleRate,
|
|
1501
|
+
targetSampleRate: this.sampleRate,
|
|
1502
|
+
},
|
|
1503
|
+
});
|
|
1504
|
+
|
|
1380
1505
|
const node = source.connect(processor);
|
|
1381
1506
|
const analyser = context.createAnalyser();
|
|
1382
1507
|
analyser.fftSize = 8192;
|
|
@@ -1392,6 +1517,15 @@ class WavRecorder {
|
|
|
1392
1517
|
analyser.connect(context.destination);
|
|
1393
1518
|
}
|
|
1394
1519
|
|
|
1520
|
+
if (context.state === 'suspended') {
|
|
1521
|
+
try {
|
|
1522
|
+
await context.resume();
|
|
1523
|
+
} catch (resumeError) {
|
|
1524
|
+
console.warn('AudioContext resume failed', resumeError);
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
this.audioContext = context;
|
|
1395
1529
|
this.source = source;
|
|
1396
1530
|
this.node = node;
|
|
1397
1531
|
this.analyser = analyser;
|
|
@@ -1589,6 +1723,17 @@ class WavRecorder {
|
|
|
1589
1723
|
this.processor = null;
|
|
1590
1724
|
this.source = null;
|
|
1591
1725
|
this.node = null;
|
|
1726
|
+
this.analyser = null;
|
|
1727
|
+
|
|
1728
|
+
if (this.audioContext) {
|
|
1729
|
+
try {
|
|
1730
|
+
await this.audioContext.close();
|
|
1731
|
+
} catch (contextError) {
|
|
1732
|
+
console.warn('Failed to close AudioContext', contextError);
|
|
1733
|
+
}
|
|
1734
|
+
this.audioContext = null;
|
|
1735
|
+
}
|
|
1736
|
+
this.contextSampleRate = null;
|
|
1592
1737
|
|
|
1593
1738
|
const packer = new WavPacker();
|
|
1594
1739
|
const result = packer.pack(this.sampleRate, exportData.audio);
|
|
@@ -3509,7 +3654,7 @@ function arrayBufferToBase64(arrayBuffer) {
|
|
|
3509
3654
|
const NOOP = () => { };
|
|
3510
3655
|
const DEFAULT_WS_URL = 'wss://api.layercode.com/v1/agents/web/websocket';
|
|
3511
3656
|
// SDK version - updated when publishing
|
|
3512
|
-
const SDK_VERSION = '2.1.
|
|
3657
|
+
const SDK_VERSION = '2.1.3';
|
|
3513
3658
|
/**
|
|
3514
3659
|
* @class LayercodeClient
|
|
3515
3660
|
* @classdesc Core client for Layercode audio agent that manages audio recording, WebSocket communication, and speech processing.
|
|
@@ -4010,7 +4155,7 @@ class LayercodeClient {
|
|
|
4010
4155
|
const newStream = this.wavRecorder.getStream();
|
|
4011
4156
|
await this._reinitializeVAD(newStream);
|
|
4012
4157
|
}
|
|
4013
|
-
const reportedDeviceId = (_c = (_b = this.lastReportedDeviceId) !== null && _b !== void 0 ? _b : this.activeDeviceId) !== null && _c !== void 0 ? _c : (this.useSystemDefaultDevice ? 'default' :
|
|
4158
|
+
const reportedDeviceId = (_c = (_b = this.lastReportedDeviceId) !== null && _b !== void 0 ? _b : this.activeDeviceId) !== null && _c !== void 0 ? _c : (this.useSystemDefaultDevice ? 'default' : normalizedDeviceId !== null && normalizedDeviceId !== void 0 ? normalizedDeviceId : 'default');
|
|
4014
4159
|
console.debug(`Successfully switched to input device: ${reportedDeviceId}`);
|
|
4015
4160
|
}
|
|
4016
4161
|
catch (error) {
|
|
@@ -4047,7 +4192,7 @@ class LayercodeClient {
|
|
|
4047
4192
|
this.recorderStarted = true;
|
|
4048
4193
|
this._sendReadyIfNeeded();
|
|
4049
4194
|
}
|
|
4050
|
-
const reportedDeviceId = (_a = this.activeDeviceId) !== null && _a !== void 0 ? _a : (this.useSystemDefaultDevice ? 'default' : (
|
|
4195
|
+
const reportedDeviceId = (_a = this.activeDeviceId) !== null && _a !== void 0 ? _a : (this.useSystemDefaultDevice ? 'default' : (_b = this.deviceId) !== null && _b !== void 0 ? _b : 'default');
|
|
4051
4196
|
if (reportedDeviceId !== previousReportedDeviceId) {
|
|
4052
4197
|
this.lastReportedDeviceId = reportedDeviceId;
|
|
4053
4198
|
if (this.options.onDeviceSwitched) {
|