ishumdz-bail 1.0.4 → 1.0.6

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.
@@ -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
+ }