@teamlearners/clawops 0.15.1 → 0.16.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.
- package/README.md +19 -0
- package/dist/agent/index.cjs +86 -46
- package/dist/agent/index.cjs.map +1 -1
- package/dist/agent/index.d.cts +21 -7
- package/dist/agent/index.d.ts +21 -7
- package/dist/agent/index.js +79 -39
- package/dist/agent/index.js.map +1 -1
- package/dist/{chunk-RMN74YKI.cjs → chunk-4QQXMZ76.cjs} +3 -3
- package/dist/{chunk-RMN74YKI.cjs.map → chunk-4QQXMZ76.cjs.map} +1 -1
- package/dist/{chunk-Z4W4G7RD.js → chunk-5IYJOP2D.js} +3 -3
- package/dist/{chunk-Z4W4G7RD.js.map → chunk-5IYJOP2D.js.map} +1 -1
- package/dist/index.cjs +100 -50
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +43 -1
- package/dist/index.d.ts +43 -1
- package/dist/index.js +69 -20
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -201,6 +201,25 @@ if (summary.status === 'completed') {
|
|
|
201
201
|
}
|
|
202
202
|
```
|
|
203
203
|
|
|
204
|
+
### 통화 녹음 (Recordings)
|
|
205
|
+
|
|
206
|
+
콘솔에서 들리는 것과 동일한 서버측 MixMonitor 원본(WAV PCM 16bit mono 8kHz)을 다운로드합니다. SDK 측 mix.wav가 아닌 서버에서 합성된 파일이라 싱크/볼륨이 정상입니다.
|
|
207
|
+
|
|
208
|
+
```typescript
|
|
209
|
+
import { writeFile } from 'node:fs/promises';
|
|
210
|
+
|
|
211
|
+
// callList 응답의 recordingUrl 필드로 녹음 유무 확인 가능
|
|
212
|
+
const list = await client.calls.list({ pageSize: 10 });
|
|
213
|
+
for (const call of list.data) {
|
|
214
|
+
if (!call.recordingUrl) continue; // failed/no-answer 등은 null
|
|
215
|
+
const rec = await client.recordings.download(call.callId);
|
|
216
|
+
await writeFile(rec.filename ?? `${call.callId}.wav`, Buffer.from(rec.data));
|
|
217
|
+
console.log(rec.contentType, rec.data.byteLength, 'bytes');
|
|
218
|
+
}
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
녹음이 없는 통화(`recordingUrl: null`)에 호출하면 `NotFoundError(404)` 가 발생합니다.
|
|
222
|
+
|
|
204
223
|
### 전화번호 (Numbers)
|
|
205
224
|
|
|
206
225
|
```typescript
|
package/dist/agent/index.cjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var
|
|
3
|
+
var chunk4QQXMZ76_cjs = require('../chunk-4QQXMZ76.cjs');
|
|
4
4
|
var pino = require('pino');
|
|
5
5
|
var fs = require('fs');
|
|
6
6
|
var path = require('path');
|
|
@@ -331,6 +331,27 @@ function ulawToPcm16(ulaw) {
|
|
|
331
331
|
}
|
|
332
332
|
return out;
|
|
333
333
|
}
|
|
334
|
+
function applyPcm16Gain(pcm, gain) {
|
|
335
|
+
if (!pcm.length) return Buffer.alloc(0);
|
|
336
|
+
if (gain === 1) return pcm;
|
|
337
|
+
if (!Number.isFinite(gain) || gain < 0) {
|
|
338
|
+
throw new Error("gain must be a finite number >= 0");
|
|
339
|
+
}
|
|
340
|
+
const nSamples = pcm.length >> 1;
|
|
341
|
+
const out = Buffer.alloc(nSamples * 2);
|
|
342
|
+
for (let i = 0; i < nSamples; i++) {
|
|
343
|
+
let value = Math.round(pcm.readInt16LE(i * 2) * gain);
|
|
344
|
+
if (value > 32767) value = 32767;
|
|
345
|
+
else if (value < -32768) value = -32768;
|
|
346
|
+
out.writeInt16LE(value, i * 2);
|
|
347
|
+
}
|
|
348
|
+
return out;
|
|
349
|
+
}
|
|
350
|
+
function applyUlawGain(ulaw, gain) {
|
|
351
|
+
if (!ulaw.length) return Buffer.alloc(0);
|
|
352
|
+
if (gain === 1) return ulaw;
|
|
353
|
+
return pcm16ToUlaw(applyPcm16Gain(ulawToPcm16(ulaw), gain));
|
|
354
|
+
}
|
|
334
355
|
function resamplePcm16(pcm, fromRate, toRate) {
|
|
335
356
|
if (fromRate === toRate || !pcm.length) return pcm;
|
|
336
357
|
const nSamples = pcm.length >> 1;
|
|
@@ -919,8 +940,9 @@ var AudioRecorder = class {
|
|
|
919
940
|
_inWritten = 0;
|
|
920
941
|
_outWritten = 0;
|
|
921
942
|
_mixWritten = 0;
|
|
922
|
-
_startTime = 0;
|
|
923
943
|
_started = false;
|
|
944
|
+
_baseTs = null;
|
|
945
|
+
_outCursor = 0;
|
|
924
946
|
_log = NOOP_LOGGER;
|
|
925
947
|
setLogger(logger) {
|
|
926
948
|
this._log = logger;
|
|
@@ -937,17 +959,18 @@ var AudioRecorder = class {
|
|
|
937
959
|
fs__namespace.writeSync(this._fdIn, header);
|
|
938
960
|
fs__namespace.writeSync(this._fdOut, header);
|
|
939
961
|
fs__namespace.writeSync(this._fdMix, header);
|
|
940
|
-
this._startTime = performance.now();
|
|
941
962
|
this._started = true;
|
|
942
963
|
this._log.info("Recording started: %s", this._dir);
|
|
943
964
|
}
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
965
|
+
_timestampToBytes(mediaTsMs) {
|
|
966
|
+
if (this._baseTs === null) {
|
|
967
|
+
this._baseTs = mediaTsMs;
|
|
968
|
+
}
|
|
969
|
+
const target = Math.floor((mediaTsMs - this._baseTs) * BYTES_PER_SECOND / 1e3);
|
|
970
|
+
return Math.max(0, target - target % 2);
|
|
947
971
|
}
|
|
948
|
-
_padSilence(fd, written) {
|
|
949
|
-
|
|
950
|
-
let gap = expected - written;
|
|
972
|
+
_padSilence(fd, written, target) {
|
|
973
|
+
let gap = target - written;
|
|
951
974
|
if (gap <= 0) return 0;
|
|
952
975
|
gap = gap - gap % 2;
|
|
953
976
|
if (gap > 0) {
|
|
@@ -957,9 +980,12 @@ var AudioRecorder = class {
|
|
|
957
980
|
}
|
|
958
981
|
_writeToMix(data, trackPos) {
|
|
959
982
|
if (this._fdMix === null) return;
|
|
983
|
+
data = data.subarray(0, data.length - data.length % 2);
|
|
984
|
+
if (data.length === 0) return;
|
|
960
985
|
const filePos = 44 + trackPos;
|
|
961
986
|
if (trackPos < this._mixWritten) {
|
|
962
|
-
|
|
987
|
+
let overlap = Math.min(data.length, this._mixWritten - trackPos);
|
|
988
|
+
overlap = overlap - overlap % 2;
|
|
963
989
|
const existing = Buffer.alloc(overlap);
|
|
964
990
|
fs__namespace.readSync(this._fdMix, existing, 0, overlap, filePos);
|
|
965
991
|
const mixed = mixSamples(existing, data.subarray(0, overlap));
|
|
@@ -982,10 +1008,13 @@ var AudioRecorder = class {
|
|
|
982
1008
|
this._mixWritten += data.length;
|
|
983
1009
|
}
|
|
984
1010
|
}
|
|
985
|
-
writeInbound(pcm16_8k) {
|
|
1011
|
+
writeInbound(pcm16_8k, mediaTsMs = 0) {
|
|
986
1012
|
if (!this._started || this._fdIn === null) return;
|
|
987
1013
|
try {
|
|
988
|
-
|
|
1014
|
+
pcm16_8k = pcm16_8k.subarray(0, pcm16_8k.length - pcm16_8k.length % 2);
|
|
1015
|
+
if (pcm16_8k.length === 0) return;
|
|
1016
|
+
const target = this._timestampToBytes(mediaTsMs);
|
|
1017
|
+
const gap = this._padSilence(this._fdIn, this._inWritten, target);
|
|
989
1018
|
this._inWritten += gap;
|
|
990
1019
|
const posBefore = this._inWritten;
|
|
991
1020
|
fs__namespace.writeSync(this._fdIn, pcm16_8k);
|
|
@@ -995,14 +1024,21 @@ var AudioRecorder = class {
|
|
|
995
1024
|
this._log.error({ err }, "Recording write error (inbound)");
|
|
996
1025
|
}
|
|
997
1026
|
}
|
|
998
|
-
writeOutbound(pcm16_8k) {
|
|
1027
|
+
writeOutbound(pcm16_8k, mediaTsMs) {
|
|
999
1028
|
if (!this._started || this._fdOut === null) return;
|
|
1000
1029
|
try {
|
|
1001
|
-
|
|
1030
|
+
pcm16_8k = pcm16_8k.subarray(0, pcm16_8k.length - pcm16_8k.length % 2);
|
|
1031
|
+
if (pcm16_8k.length === 0) return;
|
|
1032
|
+
let target = this._outCursor;
|
|
1033
|
+
if (mediaTsMs !== void 0) {
|
|
1034
|
+
target = Math.max(target, this._timestampToBytes(mediaTsMs));
|
|
1035
|
+
}
|
|
1036
|
+
const gap = this._padSilence(this._fdOut, this._outWritten, target);
|
|
1002
1037
|
this._outWritten += gap;
|
|
1003
1038
|
const posBefore = this._outWritten;
|
|
1004
1039
|
fs__namespace.writeSync(this._fdOut, pcm16_8k);
|
|
1005
1040
|
this._outWritten += pcm16_8k.length;
|
|
1041
|
+
this._outCursor = this._outWritten;
|
|
1006
1042
|
this._writeToMix(pcm16_8k, posBefore);
|
|
1007
1043
|
} catch (err) {
|
|
1008
1044
|
this._log.error({ err }, "Recording write error (outbound)");
|
|
@@ -1043,7 +1079,7 @@ var MAX_ERROR_MESSAGE_LENGTH = 200;
|
|
|
1043
1079
|
function getSdkInfo() {
|
|
1044
1080
|
return {
|
|
1045
1081
|
name: "clawops-node",
|
|
1046
|
-
version:
|
|
1082
|
+
version: chunk4QQXMZ76_cjs.VERSION,
|
|
1047
1083
|
runtime: `node/${process.versions.node}`,
|
|
1048
1084
|
os: `${process.platform}/${os__default.default.arch()}`
|
|
1049
1085
|
};
|
|
@@ -1698,7 +1734,7 @@ var ATTR_CALL_DIRECTION = "clawops.call.direction";
|
|
|
1698
1734
|
var ATTR_AGENT_ID = "clawops.agent.id";
|
|
1699
1735
|
|
|
1700
1736
|
// src/agent/agent.ts
|
|
1701
|
-
var ClawOpsAgent = class {
|
|
1737
|
+
var ClawOpsAgent = class _ClawOpsAgent {
|
|
1702
1738
|
_apiKey;
|
|
1703
1739
|
_accountId;
|
|
1704
1740
|
_baseUrl;
|
|
@@ -1721,10 +1757,12 @@ var ClawOpsAgent = class {
|
|
|
1721
1757
|
_pipelineLog;
|
|
1722
1758
|
_isPipelineSession = false;
|
|
1723
1759
|
_holdAudioChunks = null;
|
|
1760
|
+
_rxGain;
|
|
1761
|
+
_txGain;
|
|
1724
1762
|
constructor(options) {
|
|
1725
1763
|
this._apiKey = options.apiKey ?? process.env["CLAWOPS_API_KEY"] ?? "";
|
|
1726
1764
|
this._accountId = options.accountId ?? process.env["CLAWOPS_ACCOUNT_ID"] ?? "";
|
|
1727
|
-
this._baseUrl = options.baseUrl ??
|
|
1765
|
+
this._baseUrl = options.baseUrl ?? chunk4QQXMZ76_cjs.DEFAULT_BASE_URL;
|
|
1728
1766
|
this._fromNumber = options.from;
|
|
1729
1767
|
this._session = options.session;
|
|
1730
1768
|
this._recording = options.recording ?? false;
|
|
@@ -1732,6 +1770,8 @@ var ClawOpsAgent = class {
|
|
|
1732
1770
|
this._mcpServers = options.mcpServers ?? [];
|
|
1733
1771
|
this._builtinTools = resolveBuiltinTools(options.builtinTools ?? "all" /* ALL */);
|
|
1734
1772
|
this._passiveDtmfDebounceMs = options.passiveDtmfDebounceMs ?? 500;
|
|
1773
|
+
this._rxGain = _ClawOpsAgent._validateGain("rxGain", options.rxGain ?? 1);
|
|
1774
|
+
this._txGain = _ClawOpsAgent._validateGain("txGain", options.txGain ?? 1);
|
|
1735
1775
|
if (options.tracing) {
|
|
1736
1776
|
setTracingConfig(options.tracing);
|
|
1737
1777
|
}
|
|
@@ -1742,6 +1782,12 @@ var ClawOpsAgent = class {
|
|
|
1742
1782
|
this._holdAudioChunks = loadHoldAudio(options.toolConfig.holdAudio);
|
|
1743
1783
|
}
|
|
1744
1784
|
}
|
|
1785
|
+
static _validateGain(name, gain) {
|
|
1786
|
+
if (typeof gain !== "number" || !Number.isFinite(gain) || gain < 0) {
|
|
1787
|
+
throw new chunk4QQXMZ76_cjs.AgentError(`${name}=${gain} must be a finite number >= 0`);
|
|
1788
|
+
}
|
|
1789
|
+
return gain;
|
|
1790
|
+
}
|
|
1745
1791
|
/**
|
|
1746
1792
|
* Register a function tool.
|
|
1747
1793
|
*
|
|
@@ -1752,7 +1798,7 @@ var ClawOpsAgent = class {
|
|
|
1752
1798
|
tool(nameOrTool, description, parameters, handler) {
|
|
1753
1799
|
if (typeof nameOrTool === "string") {
|
|
1754
1800
|
if (!description || !parameters || !handler) {
|
|
1755
|
-
throw new
|
|
1801
|
+
throw new chunk4QQXMZ76_cjs.AgentError(
|
|
1756
1802
|
"tool(name, description, parameters, handler) requires all arguments."
|
|
1757
1803
|
);
|
|
1758
1804
|
}
|
|
@@ -1788,10 +1834,10 @@ var ClawOpsAgent = class {
|
|
|
1788
1834
|
async connect() {
|
|
1789
1835
|
if (this._controlWs) return;
|
|
1790
1836
|
if (!this._apiKey) {
|
|
1791
|
-
throw new
|
|
1837
|
+
throw new chunk4QQXMZ76_cjs.AgentError("API key is required. Set CLAWOPS_API_KEY or pass apiKey option.");
|
|
1792
1838
|
}
|
|
1793
1839
|
if (!this._accountId) {
|
|
1794
|
-
throw new
|
|
1840
|
+
throw new chunk4QQXMZ76_cjs.AgentError(
|
|
1795
1841
|
"Account ID is required. Set CLAWOPS_ACCOUNT_ID or pass accountId option."
|
|
1796
1842
|
);
|
|
1797
1843
|
}
|
|
@@ -1815,7 +1861,7 @@ var ClawOpsAgent = class {
|
|
|
1815
1861
|
} catch {
|
|
1816
1862
|
}
|
|
1817
1863
|
} catch (err) {
|
|
1818
|
-
throw new
|
|
1864
|
+
throw new chunk4QQXMZ76_cjs.AgentConnectionError(
|
|
1819
1865
|
`Failed to connect to ClawOps: ${err instanceof Error ? err.message : String(err)}`
|
|
1820
1866
|
);
|
|
1821
1867
|
}
|
|
@@ -1866,7 +1912,7 @@ var ClawOpsAgent = class {
|
|
|
1866
1912
|
});
|
|
1867
1913
|
if (resp.status !== 201) {
|
|
1868
1914
|
const error = await resp.json();
|
|
1869
|
-
throw new
|
|
1915
|
+
throw new chunk4QQXMZ76_cjs.AgentError(`\uBC1C\uC2E0 \uC2E4\uD328 (${resp.status}): ${error["error"] ?? ""}`);
|
|
1870
1916
|
}
|
|
1871
1917
|
const data = await resp.json();
|
|
1872
1918
|
const callSession = new CallSession({
|
|
@@ -2024,9 +2070,14 @@ var ClawOpsAgent = class {
|
|
|
2024
2070
|
}
|
|
2025
2071
|
const mediaWs = new MediaWebSocket();
|
|
2026
2072
|
mediaWs.setLogger(this._log);
|
|
2073
|
+
let latestMediaTs = 0;
|
|
2027
2074
|
session._bindTransport(
|
|
2028
2075
|
(audio) => {
|
|
2029
|
-
|
|
2076
|
+
const gained = applyUlawGain(audio, this._txGain);
|
|
2077
|
+
if (recorder) {
|
|
2078
|
+
recorder.writeOutbound(ulawToPcm16(gained), latestMediaTs);
|
|
2079
|
+
}
|
|
2080
|
+
mediaWs.sendAudio(gained.toString("base64"));
|
|
2030
2081
|
session.recordFirstResponse();
|
|
2031
2082
|
},
|
|
2032
2083
|
() => {
|
|
@@ -2063,12 +2114,14 @@ var ClawOpsAgent = class {
|
|
|
2063
2114
|
sessionHandler.setHoldAudio(this._holdAudioChunks);
|
|
2064
2115
|
}
|
|
2065
2116
|
this._callSessions.set(session.callId, sessionHandler);
|
|
2066
|
-
mediaWs.onAudio((ulawAudio,
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
}
|
|
2117
|
+
mediaWs.onAudio((ulawAudio, timestamp) => {
|
|
2118
|
+
latestMediaTs = timestamp;
|
|
2119
|
+
const gained = applyUlawGain(ulawAudio, this._rxGain);
|
|
2070
2120
|
if (recorder) {
|
|
2071
|
-
recorder.writeInbound(ulawToPcm16(
|
|
2121
|
+
recorder.writeInbound(ulawToPcm16(gained), timestamp);
|
|
2122
|
+
}
|
|
2123
|
+
if (sessionHandler) {
|
|
2124
|
+
sessionHandler.feedAudio(gained, timestamp);
|
|
2072
2125
|
}
|
|
2073
2126
|
});
|
|
2074
2127
|
mediaWs.onDtmf((digit) => {
|
|
@@ -2539,10 +2592,6 @@ var PipelineSession = class {
|
|
|
2539
2592
|
sampleRate: this._sampleRate
|
|
2540
2593
|
})) {
|
|
2541
2594
|
if (!this._running || !this._speaking) break;
|
|
2542
|
-
if (this._recorder) {
|
|
2543
|
-
const pcm8k2 = this._sampleRate !== 8e3 ? resamplePcm16(audioChunk, this._sampleRate, 8e3) : audioChunk;
|
|
2544
|
-
this._recorder.writeOutbound(pcm8k2);
|
|
2545
|
-
}
|
|
2546
2595
|
const pcm8k = resamplePcm16(audioChunk, this._sampleRate, 8e3);
|
|
2547
2596
|
const ulaw = pcm16ToUlaw(pcm8k);
|
|
2548
2597
|
for (let off = 0; off < ulaw.length; off += 160) {
|
|
@@ -2612,7 +2661,7 @@ var OpenAIRealtime = class {
|
|
|
2612
2661
|
constructor(options = {}) {
|
|
2613
2662
|
this._apiKey = options.apiKey ?? process.env["OPENAI_API_KEY"] ?? "";
|
|
2614
2663
|
this._systemPrompt = options.systemPrompt ?? "";
|
|
2615
|
-
this._model = options.model ?? "gpt-realtime-
|
|
2664
|
+
this._model = options.model ?? "gpt-realtime-2";
|
|
2616
2665
|
this._voice = options.voice ?? "marin";
|
|
2617
2666
|
this._language = options.language ?? "ko";
|
|
2618
2667
|
this._turnDetection = options.turnDetection !== void 0 ? options.turnDetection : { type: "semantic_vad", eagerness: "medium", interrupt_response: true };
|
|
@@ -2687,8 +2736,8 @@ var OpenAIRealtime = class {
|
|
|
2687
2736
|
});
|
|
2688
2737
|
this._send({ type: "response.create" });
|
|
2689
2738
|
}
|
|
2690
|
-
feedAudio(audio) {
|
|
2691
|
-
this._latestMediaTs =
|
|
2739
|
+
feedAudio(audio, timestamp) {
|
|
2740
|
+
this._latestMediaTs = timestamp ?? this._latestMediaTs;
|
|
2692
2741
|
if (this._ws && this._ws.readyState === 1 && !this._closed) {
|
|
2693
2742
|
this._send({
|
|
2694
2743
|
type: "input_audio_buffer.append",
|
|
@@ -2799,7 +2848,7 @@ var OpenAIRealtime = class {
|
|
|
2799
2848
|
if (this._playback === null) {
|
|
2800
2849
|
this._playback = {
|
|
2801
2850
|
itemId: msg["item_id"] || "",
|
|
2802
|
-
startTs: this._latestMediaTs
|
|
2851
|
+
startTs: this._latestMediaTs,
|
|
2803
2852
|
sentChunks: 0,
|
|
2804
2853
|
generating: true,
|
|
2805
2854
|
audioRemainder: Buffer.alloc(0)
|
|
@@ -2809,9 +2858,6 @@ var OpenAIRealtime = class {
|
|
|
2809
2858
|
}
|
|
2810
2859
|
const pb = this._playback;
|
|
2811
2860
|
const ulaw = Buffer.from(msg["delta"], "base64");
|
|
2812
|
-
if (this._recorder) {
|
|
2813
|
-
this._recorder.writeOutbound(ulawToPcm16(ulaw));
|
|
2814
|
-
}
|
|
2815
2861
|
const combined = Buffer.concat([pb.audioRemainder, ulaw]);
|
|
2816
2862
|
const chunkSize = 160;
|
|
2817
2863
|
const fullEnd = Math.floor(combined.length / chunkSize) * chunkSize;
|
|
@@ -2835,7 +2881,7 @@ var OpenAIRealtime = class {
|
|
|
2835
2881
|
if (pb === null) {
|
|
2836
2882
|
return;
|
|
2837
2883
|
}
|
|
2838
|
-
const playedMs = Math.max(0,
|
|
2884
|
+
const playedMs = Math.max(0, this._latestMediaTs - pb.startTs);
|
|
2839
2885
|
this._log.info(
|
|
2840
2886
|
"[Interrupt] item=%s played=%dms total=%dms",
|
|
2841
2887
|
pb.itemId,
|
|
@@ -3127,9 +3173,6 @@ var GeminiRealtime = class _GeminiRealtime {
|
|
|
3127
3173
|
feedAudio(audio) {
|
|
3128
3174
|
if (this._session && !this._closed) {
|
|
3129
3175
|
const pcm8k = ulawToPcm16(audio);
|
|
3130
|
-
if (this._recorder) {
|
|
3131
|
-
this._recorder.writeInbound(pcm8k);
|
|
3132
|
-
}
|
|
3133
3176
|
const pcm16k = resamplePcm16(pcm8k, 8e3, 16e3);
|
|
3134
3177
|
this._session.sendRealtimeInput({
|
|
3135
3178
|
audio: {
|
|
@@ -3227,9 +3270,6 @@ var GeminiRealtime = class _GeminiRealtime {
|
|
|
3227
3270
|
_handleAudioData(b64Data) {
|
|
3228
3271
|
if (!this._call) return;
|
|
3229
3272
|
const pcm24k = Buffer.from(b64Data, "base64");
|
|
3230
|
-
if (this._recorder) {
|
|
3231
|
-
this._recorder.writeOutbound(resamplePcm16(pcm24k, 24e3, 8e3));
|
|
3232
|
-
}
|
|
3233
3273
|
const pcm8k = resamplePcm16(pcm24k, 24e3, 8e3);
|
|
3234
3274
|
const ulaw = pcm16ToUlaw(pcm8k);
|
|
3235
3275
|
const combined = Buffer.concat([this._audioRemainder, ulaw]);
|