ishumdz-bail 1.0.4 → 1.0.5
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/lib/Caller/audio-feeder.d.mts +24 -0
- package/lib/Caller/audio-feeder.mjs +255 -0
- package/lib/Caller/audio-utils.js +73 -0
- package/lib/Caller/auto-answer.js +115 -0
- package/lib/Caller/banner.js +41 -0
- package/lib/Caller/index.d.mts +69 -0
- package/lib/Caller/index.mjs +873 -0
- package/lib/Caller/relay-transport.d.mts +43 -0
- package/lib/Caller/relay-transport.mjs +605 -0
- package/lib/Caller/signaling.d.mts +41 -0
- package/lib/Caller/signaling.mjs +767 -0
- package/lib/Caller/types.d.mts +79 -0
- package/lib/Caller/types.mjs +17 -0
- package/lib/Caller/video-feeder.mjs +253 -0
- package/lib/Caller/wasm-engine.d.mts +105 -0
- package/lib/Caller/wasm-engine.mjs +1347 -0
- package/lib/Caller/worker-bootstrap.mjs +1042 -0
- package/lib/index.js +3 -1
- package/package.json +1 -1
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export declare class AudioFeeder {
|
|
2
|
+
#private;
|
|
3
|
+
private readonly sampleRate;
|
|
4
|
+
private readonly channels;
|
|
5
|
+
private readonly framesPerChunk;
|
|
6
|
+
private readonly onChunk;
|
|
7
|
+
private readonly source;
|
|
8
|
+
private readonly onFinished?;
|
|
9
|
+
private readonly loop;
|
|
10
|
+
private readonly warmupSilenceMs;
|
|
11
|
+
private readonly trailingSilenceMs;
|
|
12
|
+
droppedChunks: number;
|
|
13
|
+
underflowChunks: number;
|
|
14
|
+
bytesProduced: number;
|
|
15
|
+
constructor(sampleRate: number, channels: number, framesPerChunk: number, onChunk: (chunk: Float32Array) => void, source?: string, onFinished?: (() => void) | undefined, loop?: boolean, warmupSilenceMs?: number, // 500ms warm-up for WebRTC/RTP stabilization
|
|
16
|
+
trailingSilenceMs?: number);
|
|
17
|
+
get chunksEmitted(): number;
|
|
18
|
+
/**
|
|
19
|
+
* Pre-decode an audio file into memory ahead of time so playback can start with 0ms latency.
|
|
20
|
+
*/
|
|
21
|
+
static preload: (source: string, sampleRate?: number, channels?: number) => Promise<Float32Array | null>;
|
|
22
|
+
start: () => void;
|
|
23
|
+
stop: () => void;
|
|
24
|
+
}
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Audio feeder.
|
|
3
|
+
*
|
|
4
|
+
* Decodes `source` into an in-memory Float32Array PCM buffer at the requested rate,
|
|
5
|
+
* then meters frames out with microsecond-accurate cadence to the WASM VoIP uplink.
|
|
6
|
+
* Uses an elastic accumulator loop with a 60ms cushion matching PJMEDIA's 60ms encoder
|
|
7
|
+
* frame length, eliminating Windows timer jitter and buffer underruns.
|
|
8
|
+
*
|
|
9
|
+
* @author ShellTear & Chama
|
|
10
|
+
*/
|
|
11
|
+
import { spawn } from "node:child_process";
|
|
12
|
+
// Cache decoded audio buffers in memory across calls for instant 0ms startup
|
|
13
|
+
const audioBufferCache = new Map();
|
|
14
|
+
export class AudioFeeder {
|
|
15
|
+
sampleRate;
|
|
16
|
+
channels;
|
|
17
|
+
framesPerChunk;
|
|
18
|
+
onChunk;
|
|
19
|
+
source;
|
|
20
|
+
onFinished;
|
|
21
|
+
loop;
|
|
22
|
+
warmupSilenceMs;
|
|
23
|
+
trailingSilenceMs;
|
|
24
|
+
loopGapMs;
|
|
25
|
+
#proc = null;
|
|
26
|
+
#timer = null;
|
|
27
|
+
#active = false;
|
|
28
|
+
#samples = null;
|
|
29
|
+
#playhead = 0;
|
|
30
|
+
#startTime = 0;
|
|
31
|
+
#chunksEmitted = 0;
|
|
32
|
+
#finishedEmitted = false;
|
|
33
|
+
#warmupRemaining = 0;
|
|
34
|
+
#trailingRemaining = 0;
|
|
35
|
+
#loopGapRemaining = 0;
|
|
36
|
+
#cushionChunks = 3; // 60ms cushion (3 * 20ms) matching PJMEDIA's 60ms encoder cycle
|
|
37
|
+
#chunkSamples = 0;
|
|
38
|
+
#chunkIntervalMs = 0;
|
|
39
|
+
droppedChunks = 0;
|
|
40
|
+
underflowChunks = 0;
|
|
41
|
+
bytesProduced = 0;
|
|
42
|
+
constructor(sampleRate, channels, framesPerChunk, onChunk, source = "silence", onFinished, loop = true, warmupSilenceMs = 1500, // 1500ms warm-up for WebRTC/RTP stabilization
|
|
43
|
+
trailingSilenceMs = 2000, loopGapMs = 1500) {
|
|
44
|
+
this.sampleRate = sampleRate;
|
|
45
|
+
this.channels = channels;
|
|
46
|
+
this.framesPerChunk = framesPerChunk;
|
|
47
|
+
this.onChunk = onChunk;
|
|
48
|
+
this.source = source;
|
|
49
|
+
this.onFinished = onFinished;
|
|
50
|
+
this.loop = loop;
|
|
51
|
+
this.warmupSilenceMs = warmupSilenceMs;
|
|
52
|
+
this.trailingSilenceMs = trailingSilenceMs;
|
|
53
|
+
this.loopGapMs = loopGapMs;
|
|
54
|
+
this.#chunkSamples = this.framesPerChunk * this.channels;
|
|
55
|
+
this.#chunkIntervalMs = (this.framesPerChunk / this.sampleRate) * 1000;
|
|
56
|
+
this.#warmupRemaining = Math.max(0, Math.round(this.warmupSilenceMs / this.#chunkIntervalMs));
|
|
57
|
+
this.#trailingRemaining = Math.max(0, Math.round(this.trailingSilenceMs / this.#chunkIntervalMs));
|
|
58
|
+
this.#loopGapRemaining = Math.max(0, Math.round(this.loopGapMs / this.#chunkIntervalMs));
|
|
59
|
+
}
|
|
60
|
+
get chunksEmitted() {
|
|
61
|
+
return this.#chunksEmitted;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Clear cached in-memory decoded audio buffers.
|
|
65
|
+
*/
|
|
66
|
+
static clearCache = () => {
|
|
67
|
+
audioBufferCache.clear();
|
|
68
|
+
};
|
|
69
|
+
/**
|
|
70
|
+
* Pre-decode an audio file into memory ahead of time so playback can start with 0ms latency.
|
|
71
|
+
*/
|
|
72
|
+
static preload = async (source, sampleRate = 16000, channels = 1) => {
|
|
73
|
+
if (!source || source === "silence")
|
|
74
|
+
return null;
|
|
75
|
+
const cacheKey = `${source}:${sampleRate}:${channels}`;
|
|
76
|
+
if (audioBufferCache.has(cacheKey)) {
|
|
77
|
+
return audioBufferCache.get(cacheKey);
|
|
78
|
+
}
|
|
79
|
+
return AudioFeeder.#decodeSource(source, sampleRate, channels);
|
|
80
|
+
};
|
|
81
|
+
static #decodeSource = (source, sampleRate, channels) => {
|
|
82
|
+
const cacheKey = `${source}:${sampleRate}:${channels}`;
|
|
83
|
+
return new Promise((resolve) => {
|
|
84
|
+
console.log(`[AudioFeeder] Decoding audio source to PCM: ${source}`);
|
|
85
|
+
const inputArgs = source.startsWith("lavfi:")
|
|
86
|
+
? ["-f", "lavfi", "-i", source.slice("lavfi:".length)]
|
|
87
|
+
: ["-i", source];
|
|
88
|
+
const proc = spawn("ffmpeg", [
|
|
89
|
+
"-hide_banner",
|
|
90
|
+
"-loglevel", "error",
|
|
91
|
+
...inputArgs,
|
|
92
|
+
"-af", "volume=2.2",
|
|
93
|
+
"-f", "f32le",
|
|
94
|
+
"-ac", String(channels),
|
|
95
|
+
"-ar", String(sampleRate),
|
|
96
|
+
"pipe:1",
|
|
97
|
+
]);
|
|
98
|
+
const chunks = [];
|
|
99
|
+
proc.stdout.on("data", (chunk) => {
|
|
100
|
+
chunks.push(chunk);
|
|
101
|
+
});
|
|
102
|
+
proc.stderr.on("data", (chunk) => {
|
|
103
|
+
process.stderr.write(`[AudioFeeder] ${chunk.toString().trim()}\n`);
|
|
104
|
+
});
|
|
105
|
+
proc.on("close", () => {
|
|
106
|
+
const fullBuf = Buffer.concat(chunks);
|
|
107
|
+
if (fullBuf.length >= 4) {
|
|
108
|
+
const numFloats = Math.floor(fullBuf.length / 4);
|
|
109
|
+
const fullFloats = new Float32Array(numFloats);
|
|
110
|
+
const srcView = new Uint8Array(fullBuf.buffer, fullBuf.byteOffset, numFloats * 4);
|
|
111
|
+
const dstView = new Uint8Array(fullFloats.buffer, fullFloats.byteOffset, numFloats * 4);
|
|
112
|
+
dstView.set(srcView);
|
|
113
|
+
audioBufferCache.set(cacheKey, fullFloats);
|
|
114
|
+
console.log(`✅ [AudioFeeder] Decoded and cached audio (${numFloats} samples, ${(numFloats / sampleRate).toFixed(1)}s) from ${source}`);
|
|
115
|
+
resolve(fullFloats);
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
console.warn(`[AudioFeeder] Decode produced no audio bytes for ${source}`);
|
|
119
|
+
resolve(null);
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
proc.on("error", (err) => {
|
|
123
|
+
console.error(`[AudioFeeder] ffmpeg spawn error decoding ${source}:`, err);
|
|
124
|
+
resolve(null);
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
};
|
|
128
|
+
start = () => {
|
|
129
|
+
if (this.#active)
|
|
130
|
+
return;
|
|
131
|
+
this.#active = true;
|
|
132
|
+
this.#startTime = performance.now();
|
|
133
|
+
this.#chunksEmitted = 0;
|
|
134
|
+
this.#finishedEmitted = false;
|
|
135
|
+
this.#playhead = 0;
|
|
136
|
+
const cacheKey = `${this.source}:${this.sampleRate}:${this.channels}`;
|
|
137
|
+
if (!this.source || this.source === "silence") {
|
|
138
|
+
this.#samples = new Float32Array(this.#chunkSamples);
|
|
139
|
+
this.#startPacer();
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
const cached = audioBufferCache.get(cacheKey);
|
|
143
|
+
if (cached && cached.length > 0) {
|
|
144
|
+
console.log(`[AudioFeeder] Using cached PCM buffer (${cached.length} samples, ${(cached.length / this.sampleRate).toFixed(1)}s) for ${this.source}`);
|
|
145
|
+
this.#samples = cached;
|
|
146
|
+
this.#startPacer();
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
// If not cached, start pacer immediately with silence while decoding in background
|
|
150
|
+
console.log(`[AudioFeeder] Audio buffer not cached yet; starting pacer with silence while decoding ${this.source}...`);
|
|
151
|
+
this.#startPacer();
|
|
152
|
+
AudioFeeder.#decodeSource(this.source, this.sampleRate, this.channels).then((samples) => {
|
|
153
|
+
if (this.#active && samples) {
|
|
154
|
+
this.#samples = samples;
|
|
155
|
+
console.log(`[AudioFeeder] Background decode completed (${samples.length} samples); voice streaming active!`);
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
};
|
|
159
|
+
#startPacer = () => {
|
|
160
|
+
if (this.#timer || !this.#active)
|
|
161
|
+
return;
|
|
162
|
+
// Immediately flush initial cushion frames so PJMEDIA's sound port buffer is never empty
|
|
163
|
+
this.#tick();
|
|
164
|
+
// High-resolution polling every 5ms to keep jitter buffer perfectly filled
|
|
165
|
+
this.#timer = setInterval(this.#tick, 5);
|
|
166
|
+
};
|
|
167
|
+
#tick = () => {
|
|
168
|
+
if (!this.#active)
|
|
169
|
+
return;
|
|
170
|
+
const elapsedMs = performance.now() - this.#startTime;
|
|
171
|
+
// Calculate total frames that should have been emitted by now + cushion
|
|
172
|
+
const targetChunks = Math.floor(elapsedMs / this.#chunkIntervalMs) + this.#cushionChunks;
|
|
173
|
+
// Flush all pending chunks up to target in this tick
|
|
174
|
+
while (this.#chunksEmitted < targetChunks && this.#active && !this.#finishedEmitted) {
|
|
175
|
+
this.#flushOne();
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
#flushOne = () => {
|
|
179
|
+
if (!this.#active)
|
|
180
|
+
return;
|
|
181
|
+
const frame = new Float32Array(this.#chunkSamples);
|
|
182
|
+
// 1. Warm-up silence phase (allows WebRTC/RTP connection to fully establish)
|
|
183
|
+
if (this.#warmupRemaining > 0) {
|
|
184
|
+
this.#warmupRemaining--;
|
|
185
|
+
// frame remains all 0.0f
|
|
186
|
+
}
|
|
187
|
+
// 2. Speech PCM samples phase
|
|
188
|
+
else if (this.#samples && this.#playhead < this.#samples.length) {
|
|
189
|
+
const audio = this.#samples;
|
|
190
|
+
for (let i = 0; i < this.#chunkSamples; i++) {
|
|
191
|
+
if (this.#playhead < audio.length) {
|
|
192
|
+
frame[i] = audio[this.#playhead++];
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
frame[i] = 0;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
// If audio finished playing and loop is enabled, prime the loop gap pause
|
|
199
|
+
if (this.#playhead >= audio.length && this.loop) {
|
|
200
|
+
this.#loopGapRemaining = Math.max(0, Math.round(this.loopGapMs / this.#chunkIntervalMs));
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
// 3. Loop gap silence phase (natural pause before repeating greeting)
|
|
204
|
+
else if (this.loop && this.#loopGapRemaining > 0) {
|
|
205
|
+
this.#loopGapRemaining--;
|
|
206
|
+
// frame remains all 0.0f
|
|
207
|
+
if (this.#loopGapRemaining <= 0) {
|
|
208
|
+
// Rewind playhead to replay greeting cleanly!
|
|
209
|
+
this.#playhead = 0;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
// 4. Trailing silence padding phase (for non-looping audio)
|
|
213
|
+
else {
|
|
214
|
+
if (!this.loop) {
|
|
215
|
+
if (this.#trailingRemaining > 0) {
|
|
216
|
+
this.#trailingRemaining--;
|
|
217
|
+
// frame remains all 0.0f
|
|
218
|
+
}
|
|
219
|
+
else {
|
|
220
|
+
if (!this.#finishedEmitted) {
|
|
221
|
+
this.#finishedEmitted = true;
|
|
222
|
+
this.#chunksEmitted++;
|
|
223
|
+
this.bytesProduced += frame.byteLength;
|
|
224
|
+
this.onChunk(frame);
|
|
225
|
+
console.log(`[AudioFeeder] Audio playback completed (${this.#chunksEmitted} chunks emitted). Triggering onFinished to cut call...`);
|
|
226
|
+
this.stop();
|
|
227
|
+
this.onFinished?.();
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
this.#chunksEmitted++;
|
|
234
|
+
this.bytesProduced += frame.byteLength;
|
|
235
|
+
this.onChunk(frame);
|
|
236
|
+
};
|
|
237
|
+
stop = () => {
|
|
238
|
+
if (!this.#active)
|
|
239
|
+
return;
|
|
240
|
+
this.#active = false;
|
|
241
|
+
console.log(`[AudioFeeder] Stopped. Total emitted chunks: ${this.#chunksEmitted}, bytes: ${this.bytesProduced}`);
|
|
242
|
+
if (this.#timer) {
|
|
243
|
+
clearInterval(this.#timer);
|
|
244
|
+
this.#timer = null;
|
|
245
|
+
}
|
|
246
|
+
if (this.#proc) {
|
|
247
|
+
try {
|
|
248
|
+
this.#proc.kill("SIGTERM");
|
|
249
|
+
}
|
|
250
|
+
catch { }
|
|
251
|
+
this.#proc = null;
|
|
252
|
+
}
|
|
253
|
+
this.#playhead = 0;
|
|
254
|
+
};
|
|
255
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Audio Utilities for Chama Baileys VoIP Calling
|
|
3
|
+
*
|
|
4
|
+
* Provides PCM Float32 conversion to valid RIFF WAV buffers and helpers.
|
|
5
|
+
*
|
|
6
|
+
* @author Chama (@chamanemax02) & ShellTear
|
|
7
|
+
*/
|
|
8
|
+
import fs from "node:fs";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Convert Float32Array audio chunks into a valid RIFF WAV buffer (16-bit PCM).
|
|
13
|
+
*
|
|
14
|
+
* @param {Float32Array[]} chunks Array of Float32Array PCM chunks
|
|
15
|
+
* @param {number} [sampleRate=16000] Sample rate in Hz
|
|
16
|
+
* @param {number} [numChannels=1] Number of channels
|
|
17
|
+
* @returns {Buffer} Valid WAV audio Buffer
|
|
18
|
+
*/
|
|
19
|
+
export function float32ToWav(chunks, sampleRate = 16000, numChannels = 1) {
|
|
20
|
+
let totalSamples = 0;
|
|
21
|
+
for (const chunk of chunks) {
|
|
22
|
+
if (chunk && chunk.length) totalSamples += chunk.length;
|
|
23
|
+
}
|
|
24
|
+
const buffer = Buffer.alloc(44 + totalSamples * 2);
|
|
25
|
+
|
|
26
|
+
// RIFF chunk descriptor
|
|
27
|
+
buffer.write("RIFF", 0);
|
|
28
|
+
buffer.writeUInt32LE(36 + totalSamples * 2, 4);
|
|
29
|
+
buffer.write("WAVE", 8);
|
|
30
|
+
|
|
31
|
+
// "fmt " sub-chunk
|
|
32
|
+
buffer.write("fmt ", 12);
|
|
33
|
+
buffer.writeUInt32LE(16, 16); // Subchunk1Size (16 for PCM)
|
|
34
|
+
buffer.writeUInt16LE(1, 20); // AudioFormat (1 for PCM)
|
|
35
|
+
buffer.writeUInt16LE(numChannels, 22); // NumChannels
|
|
36
|
+
buffer.writeUInt32LE(sampleRate, 24); // SampleRate
|
|
37
|
+
buffer.writeUInt32LE(sampleRate * numChannels * 2, 28); // ByteRate
|
|
38
|
+
buffer.writeUInt16LE(numChannels * 2, 32); // BlockAlign
|
|
39
|
+
buffer.writeUInt16LE(16, 34); // BitsPerSample (16-bit)
|
|
40
|
+
|
|
41
|
+
// "data" sub-chunk
|
|
42
|
+
buffer.write("data", 36);
|
|
43
|
+
buffer.writeUInt32LE(totalSamples * 2, 40);
|
|
44
|
+
|
|
45
|
+
let offset = 44;
|
|
46
|
+
for (const chunk of chunks) {
|
|
47
|
+
if (!chunk) continue;
|
|
48
|
+
for (let i = 0; i < chunk.length; i++) {
|
|
49
|
+
const s = Math.max(-1, Math.min(1, chunk[i]));
|
|
50
|
+
buffer.writeInt16LE(s < 0 ? Math.floor(s * 0x8000) : Math.floor(s * 0x7FFF), offset);
|
|
51
|
+
offset += 2;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return buffer;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Save Float32 PCM chunks to a WAV file on disk.
|
|
59
|
+
*
|
|
60
|
+
* @param {Float32Array[]} chunks
|
|
61
|
+
* @param {string} filePath
|
|
62
|
+
* @param {number} [sampleRate=16000]
|
|
63
|
+
* @returns {Buffer}
|
|
64
|
+
*/
|
|
65
|
+
export function saveFloat32ToWavFile(chunks, filePath, sampleRate = 16000) {
|
|
66
|
+
const dir = path.dirname(filePath);
|
|
67
|
+
if (!fs.existsSync(dir)) {
|
|
68
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
69
|
+
}
|
|
70
|
+
const wavBuffer = float32ToWav(chunks, sampleRate, 1);
|
|
71
|
+
fs.writeFileSync(filePath, wavBuffer);
|
|
72
|
+
return wavBuffer;
|
|
73
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chama Baileys Caller — Voice Call Auto-Answer Plugin
|
|
3
|
+
*
|
|
4
|
+
* Gives ANY Baileys bot instant WhatsApp VoIP voice call answering & audio playback!
|
|
5
|
+
*
|
|
6
|
+
* @author Chama (@chamanemax02)
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import { printChamaBanner } from "./banner.js";
|
|
11
|
+
import { VoipClient, CallState } from "./index.mjs";
|
|
12
|
+
|
|
13
|
+
let _activeVoipClient = null;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Enable WhatsApp Voice Call Auto-Answer with audio playback on any Baileys socket.
|
|
17
|
+
*
|
|
18
|
+
* @param {any} sock The Baileys socket created by makeWASocket()
|
|
19
|
+
* @param {object} options Call configuration options
|
|
20
|
+
* @param {string} [options.audio="welcome.wav"] Path to MP3 / WAV audio to play
|
|
21
|
+
* @param {boolean} [options.autoAnswer=true] Automatically answer incoming calls
|
|
22
|
+
* @param {number} [options.answerDelayMs=200] Delay before answering in milliseconds
|
|
23
|
+
* @param {number} [options.durationMs=60000] Maximum call duration in milliseconds
|
|
24
|
+
* @param {function} [options.onCall] Callback when an incoming call arrives
|
|
25
|
+
* @param {function} [options.onAnswer] Callback when call is answered
|
|
26
|
+
* @param {function} [options.onEnd] Callback when call ends
|
|
27
|
+
* @returns {Promise<VoipClient>}
|
|
28
|
+
*/
|
|
29
|
+
export async function enableCallAutoAnswer(sock, options = {}) {
|
|
30
|
+
printChamaBanner();
|
|
31
|
+
|
|
32
|
+
if (_activeVoipClient) {
|
|
33
|
+
try {
|
|
34
|
+
_activeVoipClient.destroy?.();
|
|
35
|
+
} catch {}
|
|
36
|
+
_activeVoipClient = null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const autoAnswer = options.autoAnswer !== false;
|
|
40
|
+
const audioSource = options.audio || options.audioSource || options.audioFile || "welcome.wav";
|
|
41
|
+
const videoSource = options.video || options.videoSource || options.videoFile || "sample.mp4";
|
|
42
|
+
const durationMs = options.durationMs || 60000;
|
|
43
|
+
const answerDelay = options.answerDelayMs || 200;
|
|
44
|
+
const loop = options.loop !== false;
|
|
45
|
+
const warmupSilenceMs = options.warmupSilenceMs || 1500;
|
|
46
|
+
const loopGapMs = options.loopGapMs || 1500;
|
|
47
|
+
|
|
48
|
+
console.log(`\x1b[38;5;82m📞 [Chama Caller] Voice/Video Engine Active — Auto-Answer: ${autoAnswer ? "ON" : "OFF"} | Audio: ${audioSource} | Video: ${videoSource} | Loop: ${loop ? "YES" : "NO"}\x1b[0m`);
|
|
49
|
+
|
|
50
|
+
const client = new VoipClient({
|
|
51
|
+
autoAnswer: false,
|
|
52
|
+
defaultAudioSource: audioSource,
|
|
53
|
+
defaultVideoSource: videoSource,
|
|
54
|
+
defaultDurationMs: durationMs,
|
|
55
|
+
loop,
|
|
56
|
+
warmupSilenceMs,
|
|
57
|
+
loopGapMs,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
await client.attach(sock);
|
|
61
|
+
_activeVoipClient = client;
|
|
62
|
+
|
|
63
|
+
client.on("call", (call) => {
|
|
64
|
+
const callerNumber = call.peerJid ? call.peerJid.split("@")[0].split(":")[0] : "Unknown";
|
|
65
|
+
console.log(`\n\x1b[38;5;51m┌─────────────────────────────────────────────────────────────┐\x1b[0m`);
|
|
66
|
+
console.log(`\x1b[38;5;51m│\x1b[0m \x1b[1;37m📲 [CHAMA CALL]\x1b[0m \x1b[38;5;213mIncoming WhatsApp ${call.isVideo ? "VIDEO" : "VOICE"} Call\x1b[0m`);
|
|
67
|
+
console.log(`\x1b[38;5;51m│\x1b[0m 👤 \x1b[38;5;222mCaller :\x1b[0m +${callerNumber}`);
|
|
68
|
+
console.log(`\x1b[38;5;51m│\x1b[0m 🆔 \x1b[38;5;222mCall ID:\x1b[0m ${call.callId}`);
|
|
69
|
+
console.log(`\x1b[38;5;51m└─────────────────────────────────────────────────────────────┘\x1b[0m`);
|
|
70
|
+
|
|
71
|
+
call.recordAudio = options.recordCallerAudio !== false;
|
|
72
|
+
const recordingsDir = options.recordingsDir || "./recordings";
|
|
73
|
+
call.saveRecordingPath = path.resolve(recordingsDir, `incoming_${call.isVideo ? "video" : "voice"}_${callerNumber}_${Date.now()}.wav`);
|
|
74
|
+
|
|
75
|
+
if (options.onCall) {
|
|
76
|
+
try { options.onCall(call); } catch {}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (autoAnswer) {
|
|
80
|
+
setTimeout(() => {
|
|
81
|
+
try {
|
|
82
|
+
if (call.isVideo) {
|
|
83
|
+
call.accept(audioSource, videoSource, true);
|
|
84
|
+
} else {
|
|
85
|
+
call.accept(audioSource);
|
|
86
|
+
}
|
|
87
|
+
if (options.onAnswer) {
|
|
88
|
+
try { options.onAnswer(call); } catch {}
|
|
89
|
+
}
|
|
90
|
+
} catch (err) {
|
|
91
|
+
console.error("❌ [Chama Caller] Error answering call:", err.message);
|
|
92
|
+
}
|
|
93
|
+
}, answerDelay);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
call.on("connected", () => {
|
|
97
|
+
console.log(`\x1b[38;5;46m✨ [CHAMA CALL] Connected to +${callerNumber}! Streaming ${call.isVideo ? "720p Video & Audio" : "Audio"}...\x1b[0m`);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
call.on("ended", (reason) => {
|
|
101
|
+
console.log(`\x1b[38;5;208m📴 [CHAMA CALL] Call Ended with +${callerNumber} (${reason})\x1b[0m\n`);
|
|
102
|
+
if (options.onEnd) {
|
|
103
|
+
try { options.onEnd(call, reason); } catch {}
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
return client;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function getActiveVoipClient() {
|
|
112
|
+
return _activeVoipClient;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export { CallState };
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chama Baileys Caller — Custom Colored Welcome Banner
|
|
3
|
+
* @author Chama (@chamanemax02)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export function printChamaBanner(botInfo = {}) {
|
|
7
|
+
const c = {
|
|
8
|
+
cyan: "\x1b[1;38;5;51m",
|
|
9
|
+
sky: "\x1b[38;5;45m",
|
|
10
|
+
blue: "\x1b[38;5;39m",
|
|
11
|
+
gold: "\x1b[1;38;5;220m",
|
|
12
|
+
pink: "\x1b[1;38;5;213m",
|
|
13
|
+
magenta: "\x1b[1;38;5;198m",
|
|
14
|
+
green: "\x1b[1;38;5;82m",
|
|
15
|
+
purple: "\x1b[38;5;141m",
|
|
16
|
+
white: "\x1b[1;37m",
|
|
17
|
+
dim: "\x1b[2m",
|
|
18
|
+
reset: "\x1b[0m",
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const rawInfo = typeof botInfo === "string" ? botInfo : (botInfo?.id || botInfo?.name || "");
|
|
22
|
+
const cleanNumber = rawInfo ? rawInfo.split("@")[0].split(":")[0] : "Ready";
|
|
23
|
+
|
|
24
|
+
console.log(`
|
|
25
|
+
${c.cyan} ██████╗██╗ ██╗ █████╗ ███╗ ███╗ █████╗
|
|
26
|
+
${c.sky} ██╔════╝██║ ██║██╔══██╗████╗ ████║██╔══██╗
|
|
27
|
+
${c.blue} ██║ ███████║███████║██╔████╔██║███████║
|
|
28
|
+
${c.purple} ██║ ██╔══██║██╔══██║██║╚██╔╝██║██╔══██║
|
|
29
|
+
${c.magenta} ╚██████╗██║ ██║██║ ██║██║ ╚═╝ ██║██║ ██║
|
|
30
|
+
${c.pink} ╚═════╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝${c.reset}
|
|
31
|
+
|
|
32
|
+
${c.cyan}╔══════════════════════════════════════════════════════════════════════════╗
|
|
33
|
+
║${c.gold} ⚡ CHAMA BAILEYS CALL ENGINE v2.0 ⚡ ${c.cyan}║
|
|
34
|
+
║${c.dim} ──────────────────────────────────────────────────────────────────── ${c.cyan}║
|
|
35
|
+
║ ${c.pink}👑 Author : ${c.white}CHAMA (@chamanemax02) ${c.cyan}║
|
|
36
|
+
║ ${c.purple}🚀 Engine : ${c.white}WhatsApp VoIP Native WebRTC Audio Engine ${c.cyan}║
|
|
37
|
+
║ ${c.green}🟢 Connected : ${c.white}+${cleanNumber.padEnd(54)} ${c.cyan}║
|
|
38
|
+
║ ${c.gold}📞 Auto-Call : ${c.white}READY — Listening for incoming WhatsApp voice calls! ${c.cyan}║
|
|
39
|
+
╚══════════════════════════════════════════════════════════════════════════╝${c.reset}
|
|
40
|
+
`);
|
|
41
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* baileys-caller — WhatsApp voice calling for Node.js.
|
|
3
|
+
*
|
|
4
|
+
* Wraps WhatsApp Web's official VoIP WASM stack and routes signaling through
|
|
5
|
+
* Baileys. Public surface:
|
|
6
|
+
*
|
|
7
|
+
* const client = new VoipClient({ authDir })
|
|
8
|
+
* await client.connect()
|
|
9
|
+
* const call = await client.call("12345678901", { audioSource: "./hi.mp3" })
|
|
10
|
+
*
|
|
11
|
+
* @author ShellTear
|
|
12
|
+
*/
|
|
13
|
+
import { EventEmitter } from "node:events";
|
|
14
|
+
import { WasmEngine } from "./wasm-engine.mjs";
|
|
15
|
+
import { CallState, type VoipSdkConfig } from "./types.mjs";
|
|
16
|
+
export type { VoipSdkConfig, CallOptions, CallEvents, AudioConfig } from "./types.mjs";
|
|
17
|
+
export { CallState } from "./types.mjs";
|
|
18
|
+
export { AudioFeeder } from "./audio-feeder.mjs";
|
|
19
|
+
/** A live or recently-ended call. */
|
|
20
|
+
export declare class ActiveCall extends EventEmitter {
|
|
21
|
+
#private;
|
|
22
|
+
readonly callId: string;
|
|
23
|
+
private readonly engine;
|
|
24
|
+
readonly durationMs: number;
|
|
25
|
+
/** @internal mirrors the source path for the audio feeder */
|
|
26
|
+
_audioSource: string;
|
|
27
|
+
peerJid: string;
|
|
28
|
+
isIncoming: boolean;
|
|
29
|
+
/** @internal */
|
|
30
|
+
_shouldAutoAccept: boolean;
|
|
31
|
+
constructor(callId: string, engine: WasmEngine, durationMs: number);
|
|
32
|
+
get state(): CallState;
|
|
33
|
+
accept: (audioSource?: string) => void;
|
|
34
|
+
reject: () => void;
|
|
35
|
+
end: () => void;
|
|
36
|
+
mute: (muted: boolean) => void;
|
|
37
|
+
waitForEnd: () => Promise<string>;
|
|
38
|
+
/** @internal — called by VoipClient on WASM call-state change */
|
|
39
|
+
_updateState: (state: number) => void;
|
|
40
|
+
/** @internal */
|
|
41
|
+
_emitAudio: (pcm: Float32Array) => void;
|
|
42
|
+
/** @internal */
|
|
43
|
+
_forceEnd: (reason: string) => void;
|
|
44
|
+
}
|
|
45
|
+
/** Top-level client. Connects to WhatsApp and lets you place or answer calls. */
|
|
46
|
+
export declare class VoipClient extends EventEmitter {
|
|
47
|
+
#private;
|
|
48
|
+
static preloadAudio: (source: string, sampleRate?: number, channels?: number) => Promise<Float32Array | null>;
|
|
49
|
+
constructor(config?: VoipSdkConfig);
|
|
50
|
+
get sock(): any;
|
|
51
|
+
get activeCall(): ActiveCall | null;
|
|
52
|
+
get engine(): WasmEngine | null;
|
|
53
|
+
/** Connect to WhatsApp and bring up the WASM VoIP stack. */
|
|
54
|
+
connect: () => Promise<void>;
|
|
55
|
+
/** Attach to an existing Baileys socket instead of creating a new one. */
|
|
56
|
+
attach: (sock: any) => Promise<void>;
|
|
57
|
+
/** Place an outbound voice call. */
|
|
58
|
+
call: (phoneNumber: string, opts?: {
|
|
59
|
+
audioSource?: string;
|
|
60
|
+
durationMs?: number;
|
|
61
|
+
}) => Promise<ActiveCall>;
|
|
62
|
+
/** Accept an incoming call */
|
|
63
|
+
acceptCall: (audioSource?: string) => void;
|
|
64
|
+
/** Reject an incoming call */
|
|
65
|
+
rejectCall: () => void;
|
|
66
|
+
/** Tear down the WhatsApp socket and release resources. */
|
|
67
|
+
disconnect: () => void;
|
|
68
|
+
destroy: () => void;
|
|
69
|
+
}
|