feat: redesign UI and overhaul audio processing for Discord Gateway v4

This commit is contained in:
baharsah
2026-05-13 02:30:09 +07:00
parent 44ac346c21
commit ad7dcde47c
7 changed files with 396 additions and 297 deletions

View File

@@ -63,20 +63,22 @@ export async function startRecording(client: Client, channel: VoiceChannel): Pro
// Dengarkan siapapun yang mulai bicara
receiver.speaking.on("start", async (userId) => {
if (config.verbose) {
// console.log(`[recorder-debug] Speaking 'start' event triggered for userId: ${userId}. Subscriptions has? ${receiver.subscriptions.has(userId)}`);
// Coba ambil data user dari cache atau fetch dari API
const user = client.users.cache.get(userId) || await client.users.fetch(userId).catch(() => null);
const username = user?.username ?? "Unknown User";
const avatar = user?.displayAvatarURL({ format: 'png', size: 64 }) ?? "https://cdn.discordapp.com/embed/avatars/0.png";
// Tampilkan format "nama user [voice activity]"
console.log(`${username} [voice activity]`);
// Notify webserver
if ((global as any).updateActiveUser) {
(global as any).updateActiveUser(userId, { username, avatar, speaking: true });
}
// Jangan record kalau sudah ada stream aktif untuk user ini
if (receiver.subscriptions.has(userId)) return;
// Coba ambil data user dari cache atau fetch dari API
const user = client.users.cache.get(userId) || await client.users.fetch(userId).catch(() => null);
const username = user?.username ?? "Unknown User";
// Tampilkan format "nama user [voice activity]"
console.log(`${username} [voice activity]`);
const timestamp = Date.now();
const userDir = path.join(recordingsDir, userId);
if (!fs.existsSync(userDir)) {
@@ -88,38 +90,60 @@ export async function startRecording(client: Client, channel: VoiceChannel): Pro
const audioStream = receiver.subscribe(userId, {
end: {
behavior: EndBehaviorType.AfterSilence,
duration: 1000, // Stop 1 detik setelah user diam
duration: 3000, // 3 seconds — avoids FFmpeg restart overhead between utterances
},
});
try {
const packetFilter = new PacketFilter(10);
// --- OGG file recording (unchanged) ---
const packetFilterForOgg = new PacketFilter(8);
const oggStream = new prism.opus.OggLogicalBitstream({
opusHead: new prism.opus.OpusHead({
channelCount: 2,
sampleRate: 48000,
}),
pageSizeControl: {
maxPackets: 10,
},
crc: true, // Use our mock node-crc
opusHead: new prism.opus.OpusHead({ channelCount: 2, sampleRate: 48000 }),
pageSizeControl: { maxPackets: 10 },
crc: true,
});
const out = fs.createWriteStream(filename);
audioStream.pipe(packetFilterForOgg).pipe(oggStream).pipe(out);
// Pipe: audioStream -> packetFilter -> oggStream -> out
audioStream.pipe(packetFilter).pipe(oggStream).pipe(out);
// --- Web broadcast: pure JS Opus → PCM, no FFmpeg ---
// Create a fresh decoder for each user session
const opusDecoder = new prism.opus.Decoder({ frameSize: 960, channels: 2, rate: 48000 });
// Forward raw Opus packets to the web shared Ogg stream
packetFilter.on('data', (chunk) => {
if ((global as any).broadcastOpusToWeb) {
(global as any).broadcastOpusToWeb(chunk);
// CRITICAL: Swallow decode errors (DAVE/bad packets) without crashing
opusDecoder.on('error', () => {});
// Downsample 48kHz stereo → 24kHz mono (take left channel, every 2nd sample)
opusDecoder.on('data', (pcm: Buffer) => {
if (!(global as any).broadcastPcmToWeb) return;
// Input: 48kHz stereo s16le → 4 bytes per sample-pair
// Output: 24kHz mono s16le → 2 bytes per sample
const outBuf = Buffer.alloc(pcm.length / 4);
for (let i = 0; i < outBuf.length / 2; i++) {
outBuf.writeInt16LE(pcm.readInt16LE(i * 8), i * 2);
}
(global as any).broadcastPcmToWeb(outBuf, userId);
});
// Feed Opus packets one-by-one; catch per-packet decode errors
let packetCount = 0;
audioStream.on('data', (chunk: Buffer) => {
packetCount++;
if (packetCount <= 5) {
console.log(`[recorder] Pkt #${packetCount} from ${userId}: ${chunk.length}b | 0x${chunk.slice(0,4).toString('hex')}`);
}
if (chunk.length < 8) return; // skip tiny control packets
try {
opusDecoder.write(chunk);
} catch (_) {} // per-packet isolation — don't let one bad packet stop the stream
});
audioStream.on('end', () => {
opusDecoder.end();
if ((global as any).updateActiveUser) {
(global as any).updateActiveUser(userId, { username, avatar, speaking: false });
}
});
if (config.verbose) {
console.log(`[recorder] Recording user ${userId}${filename}`);
}
out.on('finish', async () => {
if (config.verbose) {
@@ -145,17 +169,9 @@ export async function startRecording(client: Client, channel: VoiceChannel): Pro
audioStream.on('error', (err) => {
console.error(`[recorder] Audio Stream error ${userId}:`, err.message);
});
audioStream.on('data', (chunk) => {
if (config.verbose) {
console.log(`[recorder-debug] Received audio packet from ${userId}, size: ${chunk.length} bytes`);
}
packetFilterForOgg.on('error', (err) => {
console.error(`[recorder] PacketFilter(ogg) error ${userId}:`, err.message);
});
packetFilter.on('error', (err) => {
console.error(`[recorder] Packet Filter error ${userId}:`, err.message);
});
out.on('error', (err) => {
console.error(`[recorder] File write error ${userId}:`, err.message);
});