@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.
|
@@ -808,6 +808,8 @@ class AudioProcessor extends AudioWorkletProcessor {
|
|
|
808
808
|
this.foundAudio = false;
|
|
809
809
|
this.recording = false;
|
|
810
810
|
this.chunks = [];
|
|
811
|
+
this.downsampleRatio = 1;
|
|
812
|
+
this.downsampleOffset = 0;
|
|
811
813
|
}
|
|
812
814
|
|
|
813
815
|
/**
|
|
@@ -914,9 +916,12 @@ class AudioProcessor extends AudioWorkletProcessor {
|
|
|
914
916
|
}
|
|
915
917
|
|
|
916
918
|
receive(e) {
|
|
917
|
-
const { event, id } = e.data;
|
|
919
|
+
const { event, id, data } = e.data;
|
|
918
920
|
let receiptData = {};
|
|
919
921
|
switch (event) {
|
|
922
|
+
case 'configure':
|
|
923
|
+
this.configure(data);
|
|
924
|
+
return;
|
|
920
925
|
case 'start':
|
|
921
926
|
this.recording = true;
|
|
922
927
|
break;
|
|
@@ -939,6 +944,24 @@ class AudioProcessor extends AudioWorkletProcessor {
|
|
|
939
944
|
this.port.postMessage({ event: 'receipt', id, data: receiptData });
|
|
940
945
|
}
|
|
941
946
|
|
|
947
|
+
configure(config = {}) {
|
|
948
|
+
const inputSampleRate = config?.inputSampleRate;
|
|
949
|
+
const targetSampleRate = config?.targetSampleRate;
|
|
950
|
+
if (
|
|
951
|
+
typeof inputSampleRate === 'number' &&
|
|
952
|
+
inputSampleRate > 0 &&
|
|
953
|
+
typeof targetSampleRate === 'number' &&
|
|
954
|
+
targetSampleRate > 0
|
|
955
|
+
) {
|
|
956
|
+
if (inputSampleRate <= targetSampleRate) {
|
|
957
|
+
this.downsampleRatio = 1;
|
|
958
|
+
} else {
|
|
959
|
+
this.downsampleRatio = inputSampleRate / targetSampleRate;
|
|
960
|
+
}
|
|
961
|
+
this.downsampleOffset = 0;
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
|
|
942
965
|
sendChunk(chunk) {
|
|
943
966
|
const channels = this.readChannelData([chunk]);
|
|
944
967
|
const { float32Array, meanValues } = this.formatAudioData(channels);
|
|
@@ -991,14 +1014,40 @@ class AudioProcessor extends AudioWorkletProcessor {
|
|
|
991
1014
|
}
|
|
992
1015
|
}
|
|
993
1016
|
if (inputs && inputs[0] && this.foundAudio && this.recording) {
|
|
994
|
-
// We need to copy the TypedArray, because the
|
|
1017
|
+
// We need to copy the TypedArray, because the \`process\`
|
|
995
1018
|
// internals will reuse the same buffer to hold each input
|
|
996
1019
|
const chunk = inputs.map((input) => input.slice(sliceIndex));
|
|
997
|
-
this.
|
|
998
|
-
|
|
1020
|
+
const processedChunk = this.downsampleChunk(chunk);
|
|
1021
|
+
if (processedChunk[0] && processedChunk[0].length) {
|
|
1022
|
+
this.chunks.push(processedChunk);
|
|
1023
|
+
this.sendChunk(processedChunk);
|
|
1024
|
+
}
|
|
999
1025
|
}
|
|
1000
1026
|
return true;
|
|
1001
1027
|
}
|
|
1028
|
+
|
|
1029
|
+
downsampleChunk(chunk) {
|
|
1030
|
+
if (this.downsampleRatio === 1) {
|
|
1031
|
+
return chunk;
|
|
1032
|
+
}
|
|
1033
|
+
const channelCount = chunk.length;
|
|
1034
|
+
if (!channelCount || !chunk[0]?.length) {
|
|
1035
|
+
return chunk;
|
|
1036
|
+
}
|
|
1037
|
+
const ratio = this.downsampleRatio;
|
|
1038
|
+
const inputLength = chunk[0].length;
|
|
1039
|
+
const outputs = Array.from({ length: channelCount }, () => []);
|
|
1040
|
+
let offset = this.downsampleOffset;
|
|
1041
|
+
while (offset < inputLength) {
|
|
1042
|
+
const sampleIndex = Math.floor(offset);
|
|
1043
|
+
for (let c = 0; c < channelCount; c++) {
|
|
1044
|
+
outputs[c].push(chunk[c][sampleIndex]);
|
|
1045
|
+
}
|
|
1046
|
+
offset += ratio;
|
|
1047
|
+
}
|
|
1048
|
+
this.downsampleOffset = offset - inputLength;
|
|
1049
|
+
return outputs.map((samples) => Float32Array.from(samples));
|
|
1050
|
+
}
|
|
1002
1051
|
}
|
|
1003
1052
|
|
|
1004
1053
|
registerProcessor('audio_processor', AudioProcessor);
|
|
@@ -1044,10 +1093,13 @@ registerProcessor('audio_processor', AudioProcessor);
|
|
|
1044
1093
|
this._devices = [];
|
|
1045
1094
|
// State variables
|
|
1046
1095
|
this.stream = null;
|
|
1096
|
+
this.audioContext = null;
|
|
1047
1097
|
this.processor = null;
|
|
1048
1098
|
this.source = null;
|
|
1049
1099
|
this.node = null;
|
|
1100
|
+
this.analyser = null;
|
|
1050
1101
|
this.recording = false;
|
|
1102
|
+
this.contextSampleRate = sampleRate;
|
|
1051
1103
|
// Event handling with AudioWorklet
|
|
1052
1104
|
this._lastEventId = 0;
|
|
1053
1105
|
this.eventReceipts = {};
|
|
@@ -1256,20 +1308,53 @@ registerProcessor('audio_processor', AudioProcessor);
|
|
|
1256
1308
|
* @returns {Promise<true>}
|
|
1257
1309
|
*/
|
|
1258
1310
|
async requestPermission() {
|
|
1259
|
-
const
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1311
|
+
const ensureUserMediaAccess = async () => {
|
|
1312
|
+
const stream = await navigator.mediaDevices.getUserMedia({
|
|
1313
|
+
audio: true,
|
|
1314
|
+
});
|
|
1315
|
+
const tracks = stream.getTracks();
|
|
1316
|
+
tracks.forEach((track) => track.stop());
|
|
1317
|
+
};
|
|
1318
|
+
|
|
1319
|
+
const permissionsUnsupported =
|
|
1320
|
+
!navigator.permissions ||
|
|
1321
|
+
typeof navigator.permissions.query !== 'function';
|
|
1322
|
+
|
|
1323
|
+
if (permissionsUnsupported) {
|
|
1265
1324
|
try {
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1325
|
+
await ensureUserMediaAccess();
|
|
1326
|
+
} catch (error) {
|
|
1327
|
+
window.alert('You must grant microphone access to use this feature.');
|
|
1328
|
+
throw error;
|
|
1329
|
+
}
|
|
1330
|
+
return true;
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
try {
|
|
1334
|
+
const permissionStatus = await navigator.permissions.query({
|
|
1335
|
+
name: 'microphone',
|
|
1336
|
+
});
|
|
1337
|
+
|
|
1338
|
+
if (permissionStatus.state === 'denied') {
|
|
1339
|
+
window.alert('You must grant microphone access to use this feature.');
|
|
1340
|
+
return true;
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
if (permissionStatus.state === 'prompt') {
|
|
1344
|
+
try {
|
|
1345
|
+
await ensureUserMediaAccess();
|
|
1346
|
+
} catch (error) {
|
|
1347
|
+
window.alert('You must grant microphone access to use this feature.');
|
|
1348
|
+
throw error;
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
} catch (error) {
|
|
1352
|
+
// Firefox rejects permissions.query with NotSupportedError – fall back to getUserMedia directly
|
|
1353
|
+
try {
|
|
1354
|
+
await ensureUserMediaAccess();
|
|
1355
|
+
} catch (fallbackError) {
|
|
1272
1356
|
window.alert('You must grant microphone access to use this feature.');
|
|
1357
|
+
throw fallbackError;
|
|
1273
1358
|
}
|
|
1274
1359
|
}
|
|
1275
1360
|
return true;
|
|
@@ -1305,6 +1390,10 @@ registerProcessor('audio_processor', AudioProcessor);
|
|
|
1305
1390
|
}
|
|
1306
1391
|
defaultDevice.default = true;
|
|
1307
1392
|
deviceList.push(defaultDevice);
|
|
1393
|
+
} else if (audioDevices.length) {
|
|
1394
|
+
const fallbackDefault = audioDevices.shift();
|
|
1395
|
+
fallbackDefault.default = true;
|
|
1396
|
+
deviceList.push(fallbackDefault);
|
|
1308
1397
|
}
|
|
1309
1398
|
return deviceList.concat(audioDevices);
|
|
1310
1399
|
}
|
|
@@ -1346,8 +1435,36 @@ registerProcessor('audio_processor', AudioProcessor);
|
|
|
1346
1435
|
throw new Error('Could not start media stream');
|
|
1347
1436
|
}
|
|
1348
1437
|
|
|
1349
|
-
const
|
|
1350
|
-
|
|
1438
|
+
const createContext = (rate) => {
|
|
1439
|
+
try {
|
|
1440
|
+
return rate ? new AudioContext({ sampleRate: rate }) : new AudioContext();
|
|
1441
|
+
} catch (error) {
|
|
1442
|
+
console.warn('Failed to create AudioContext with sampleRate', rate, error);
|
|
1443
|
+
return null;
|
|
1444
|
+
}
|
|
1445
|
+
};
|
|
1446
|
+
|
|
1447
|
+
let context = createContext(this.sampleRate);
|
|
1448
|
+
if (!context) {
|
|
1449
|
+
context = createContext();
|
|
1450
|
+
}
|
|
1451
|
+
if (!context) {
|
|
1452
|
+
throw new Error('Could not create AudioContext');
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
let source;
|
|
1456
|
+
try {
|
|
1457
|
+
source = context.createMediaStreamSource(this.stream);
|
|
1458
|
+
} catch (error) {
|
|
1459
|
+
await context.close().catch(() => {});
|
|
1460
|
+
context = createContext();
|
|
1461
|
+
if (!context) {
|
|
1462
|
+
throw error;
|
|
1463
|
+
}
|
|
1464
|
+
source = context.createMediaStreamSource(this.stream);
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
this.contextSampleRate = context.sampleRate;
|
|
1351
1468
|
// Load and execute the module script.
|
|
1352
1469
|
try {
|
|
1353
1470
|
await context.audioWorklet.addModule(this.scriptSrc);
|
|
@@ -1383,6 +1500,14 @@ registerProcessor('audio_processor', AudioProcessor);
|
|
|
1383
1500
|
}
|
|
1384
1501
|
};
|
|
1385
1502
|
|
|
1503
|
+
processor.port.postMessage({
|
|
1504
|
+
event: 'configure',
|
|
1505
|
+
data: {
|
|
1506
|
+
inputSampleRate: this.contextSampleRate,
|
|
1507
|
+
targetSampleRate: this.sampleRate,
|
|
1508
|
+
},
|
|
1509
|
+
});
|
|
1510
|
+
|
|
1386
1511
|
const node = source.connect(processor);
|
|
1387
1512
|
const analyser = context.createAnalyser();
|
|
1388
1513
|
analyser.fftSize = 8192;
|
|
@@ -1398,6 +1523,15 @@ registerProcessor('audio_processor', AudioProcessor);
|
|
|
1398
1523
|
analyser.connect(context.destination);
|
|
1399
1524
|
}
|
|
1400
1525
|
|
|
1526
|
+
if (context.state === 'suspended') {
|
|
1527
|
+
try {
|
|
1528
|
+
await context.resume();
|
|
1529
|
+
} catch (resumeError) {
|
|
1530
|
+
console.warn('AudioContext resume failed', resumeError);
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1534
|
+
this.audioContext = context;
|
|
1401
1535
|
this.source = source;
|
|
1402
1536
|
this.node = node;
|
|
1403
1537
|
this.analyser = analyser;
|
|
@@ -1595,6 +1729,17 @@ registerProcessor('audio_processor', AudioProcessor);
|
|
|
1595
1729
|
this.processor = null;
|
|
1596
1730
|
this.source = null;
|
|
1597
1731
|
this.node = null;
|
|
1732
|
+
this.analyser = null;
|
|
1733
|
+
|
|
1734
|
+
if (this.audioContext) {
|
|
1735
|
+
try {
|
|
1736
|
+
await this.audioContext.close();
|
|
1737
|
+
} catch (contextError) {
|
|
1738
|
+
console.warn('Failed to close AudioContext', contextError);
|
|
1739
|
+
}
|
|
1740
|
+
this.audioContext = null;
|
|
1741
|
+
}
|
|
1742
|
+
this.contextSampleRate = null;
|
|
1598
1743
|
|
|
1599
1744
|
const packer = new WavPacker();
|
|
1600
1745
|
const result = packer.pack(this.sampleRate, exportData.audio);
|
|
@@ -3515,7 +3660,7 @@ registerProcessor('audio_processor', AudioProcessor);
|
|
|
3515
3660
|
const NOOP = () => { };
|
|
3516
3661
|
const DEFAULT_WS_URL = 'wss://api.layercode.com/v1/agents/web/websocket';
|
|
3517
3662
|
// SDK version - updated when publishing
|
|
3518
|
-
const SDK_VERSION = '2.1.
|
|
3663
|
+
const SDK_VERSION = '2.1.3';
|
|
3519
3664
|
/**
|
|
3520
3665
|
* @class LayercodeClient
|
|
3521
3666
|
* @classdesc Core client for Layercode audio agent that manages audio recording, WebSocket communication, and speech processing.
|
|
@@ -4016,7 +4161,7 @@ registerProcessor('audio_processor', AudioProcessor);
|
|
|
4016
4161
|
const newStream = this.wavRecorder.getStream();
|
|
4017
4162
|
await this._reinitializeVAD(newStream);
|
|
4018
4163
|
}
|
|
4019
|
-
const reportedDeviceId = (_c = (_b = this.lastReportedDeviceId) !== null && _b !== void 0 ? _b : this.activeDeviceId) !== null && _c !== void 0 ? _c : (this.useSystemDefaultDevice ? 'default' :
|
|
4164
|
+
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');
|
|
4020
4165
|
console.debug(`Successfully switched to input device: ${reportedDeviceId}`);
|
|
4021
4166
|
}
|
|
4022
4167
|
catch (error) {
|
|
@@ -4053,7 +4198,7 @@ registerProcessor('audio_processor', AudioProcessor);
|
|
|
4053
4198
|
this.recorderStarted = true;
|
|
4054
4199
|
this._sendReadyIfNeeded();
|
|
4055
4200
|
}
|
|
4056
|
-
const reportedDeviceId = (_a = this.activeDeviceId) !== null && _a !== void 0 ? _a : (this.useSystemDefaultDevice ? 'default' : (
|
|
4201
|
+
const reportedDeviceId = (_a = this.activeDeviceId) !== null && _a !== void 0 ? _a : (this.useSystemDefaultDevice ? 'default' : (_b = this.deviceId) !== null && _b !== void 0 ? _b : 'default');
|
|
4057
4202
|
if (reportedDeviceId !== previousReportedDeviceId) {
|
|
4058
4203
|
this.lastReportedDeviceId = reportedDeviceId;
|
|
4059
4204
|
if (this.options.onDeviceSwitched) {
|