feat(voice): implement full voice transmit (Browser → Discord)

Frontend (useAudioTransmit.ts):
- Capture microphone via getUserMedia
- Convert Float32 to Int16 PCM at 24kHz mono
- Base64 encode and send as JSON via WebSocket
- Send voice:transmit:start/stop commands on start/stop

Backend (ws/server.ts):
- Handle voice_transmit messages from browser
- Forward PCM data to Redis channel backend:voice:transmit
- Handle voice_command messages and forward to backend:command
- discord-gateway VoiceTransmitter receives and plays to voice channel

Flow:
   Browser Mic → getUserMedia → ScriptProcessor → Int16 PCM → base64 →
   WebSocket JSON → Backend → Redis → VoiceTransmitter → Discord Voice Channel

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
MythEclipse
2026-06-08 21:29:41 +07:00
co-authored by Claude Opus 4.8
parent 2e9e9c1a06
commit 314d2baec4
2 changed files with 65 additions and 10 deletions
+34 -7
View File
@@ -65,13 +65,40 @@ export function createWebSocketServer(server: Server): WebSocketServer {
);
ws.on("message", (data: Buffer) => {
// Binary PCM data received from browser.
// Since backend has no Discord client to relay to, drop it.
if (Buffer.isBuffer(data) && data.length > 0) {
logger.debug(
{ bytes: data.length },
"Dropping binary PCM (no Discord client)",
);
// Handle JSON messages from browser
if (typeof data === 'string' || (Buffer.isBuffer(data) && data.length > 0 && data[0] === 0x7B)) {
try {
const message = JSON.parse(data.toString());
if (message.type === 'voice_transmit' && message.buffer) {
// Forward PCM data to Redis for discord-gateway
import('../shared/redis/index.js').then(({ getCommandPublisher }) => {
const publisher = getCommandPublisher();
publisher.publish('backend:voice:transmit', JSON.stringify({
type: 'pcm',
buffer: message.buffer
})).catch((err: Error) => {
logger.error({ err }, 'Failed to publish voice transmit to Redis');
});
});
} else if (message.type === 'voice_command' && message.command) {
// Forward voice commands to discord-gateway
import('../shared/redis/index.js').then(({ getCommandPublisher }) => {
const publisher = getCommandPublisher();
const commandId = `cmd-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
publisher.publish('backend:command', JSON.stringify({
id: commandId,
type: message.command,
payload: {},
replyChannel: `reply:${commandId}`
})).catch((err: Error) => {
logger.error({ err }, 'Failed to publish voice command to Redis');
});
});
}
} catch (err) {
logger.debug({ err }, 'Failed to parse WebSocket message as JSON');
}
}
});