feat(voice): add voice transmit functionality for real-time audio streaming

- Add VoiceTransmitter class to handle PCM audio from backend/browser to Discord
- Implement voice:transmit:start and voice:transmit:stop command handlers
- Upsample 24kHz mono PCM to 48kHz stereo for Discord compatibility
- Encode PCM to Opus and stream via browser-bridge player owner
- Subscribe to Redis channel backend:voice:transmit for real-time PCM data

Features:
- Backend can send PCM audio (24kHz mono s16le base64) via Redis
- Automatic upsampling and encoding to Discord-compatible format
- Clean start/stop lifecycle with resource cleanup

This completes the bidirectional voice streaming:
- Listen: Discord → Backend (already working via voicePcmData broadcast)
- Transmit: Backend → Discord (now implemented)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
MythEclipse
2026-06-08 20:48:16 +07:00
co-authored by Claude Opus 4.8
parent 60edb4576f
commit f74252f594
3 changed files with 233 additions and 0 deletions
@@ -3,6 +3,7 @@ import type { Client } from "discord.js-selfbot-v13";
import Redis from "ioredis";
import { config } from "../../shared/config/config.js";
import { discordPlayer } from "../voice-recording/player.js";
import { voiceTransmitter } from "../voice-recording/transmitter.js";
import type { VoiceController } from "../voice-recording/voiceController.js";
const logger = createChildLogger("command-handler");
@@ -130,6 +131,12 @@ export class CommandHandler {
case "voice:channels":
reply = await this.handleVoiceChannels(cmd);
break;
case "voice:transmit:start":
reply = await this.handleVoiceTransmitStart(cmd);
break;
case "voice:transmit:stop":
reply = await this.handleVoiceTransmitStop(cmd);
break;
case "guilds:list":
reply = await this.handleListGuilds(cmd);
break;
@@ -370,6 +377,63 @@ export class CommandHandler {
};
}
private async handleVoiceTransmitStart(cmd: BackendCommand): Promise<CommandReply> {
if (!discordPlayer.isConnected()) {
return {
id: cmd.id,
success: false,
data: null,
error: "Not connected to voice channel",
};
}
try {
// Create a new Redis connection for the transmitter
const transmitRedis = new Redis(config.REDIS_URL);
await voiceTransmitter.start(transmitRedis);
const status = voiceTransmitter.getStatus();
logger.info({ status }, "Voice transmit started");
return {
id: cmd.id,
success: true,
data: status,
};
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
logger.error({ error: message }, "Failed to start voice transmit");
return {
id: cmd.id,
success: false,
data: null,
error: message,
};
}
}
private async handleVoiceTransmitStop(cmd: BackendCommand): Promise<CommandReply> {
try {
await voiceTransmitter.stop();
logger.info("Voice transmit stopped");
return {
id: cmd.id,
success: true,
data: { status: "stopped" },
};
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
logger.error({ error: message }, "Failed to stop voice transmit");
return {
id: cmd.id,
success: false,
data: null,
error: message,
};
}
}
// ---- Status publishing ----
private publishVoiceStatus(): void {