feat(fe): real mic capture for voice transmit (kirim suara)

Sebelumnya tombol Live/Muted cuma kirim voice:transmit:start/stop ke
gateway — TIDAK ADA audio yang dikirim (0 getUserMedia/AudioContext di
frontend). MicControl cuma toggle state kosong.

- lib/audio/mic-transmit.ts (BARU): getUserMedia → AudioContext 48kHz →
  AudioWorklet (downsample 24kHz mono s16le + volume + chunk 20ms) →
  frame 'PCM\0' + Int16LE → ws.sendBinary. Worklet inline via Blob URL
  (aman untuk static export).
- useMicTransmit(ws): aktif = start capture + voice:transmit:start;
  nonaktif = stop capture + voice:transmit:stop; setVolume untuk slider.
- voice page: volume slider sekarang beneran ngatur gain mic; disconnect
  ikut matiin mic.

Verifikasi: tsc PASS, biome 0 error, next build PASS. Test mic butuh
real device (headless browser tidak punya mic) — protokol: connect voice
→ Live → ngomong → orang di channel denger.
This commit is contained in:
asepharyana
2026-08-01 11:26:18 +07:00
parent 9d60f00934
commit 189ab1c1f6
3 changed files with 203 additions and 15 deletions
+25 -7
View File
@@ -1,7 +1,8 @@
import { useCallback, useState } from "react";
import { useCallback, useRef, useState } from "react";
import useSWR, { useSWRConfig } from "swr";
import { useAction } from "@/hooks/use-action";
import { voiceApi } from "@/lib/api";
import { MicTransmitter } from "@/lib/audio/mic-transmit";
import type { ActiveSpeaker, Channel, VoiceStatus } from "@/lib/types";
import type { WsHook } from "@/lib/ws-hook";
@@ -65,10 +66,27 @@ export function useVoiceDisconnect() {
return useAction(() => voiceApi.disconnect(), { onSuccess: invalidate });
}
export function useMicTransmit() {
return useAction((active: boolean) =>
voiceApi.sendCommand(
active ? "voice:transmit:start" : "voice:transmit:stop",
),
);
export function useMicTransmit(ws: {
sendBinary: (data: ArrayBufferLike) => void;
}) {
const transmitterRef = useRef<MicTransmitter | null>(null);
const action = useAction(async (active: boolean) => {
if (active) {
const transmitter = new MicTransmitter((frame) => ws.sendBinary(frame));
transmitterRef.current = transmitter;
await transmitter.start();
await voiceApi.sendCommand("voice:transmit:start");
} else {
transmitterRef.current?.stop();
transmitterRef.current = null;
await voiceApi.sendCommand("voice:transmit:stop");
}
});
const setVolume = useCallback((volume: number) => {
transmitterRef.current?.setVolume(volume / 100);
}, []);
return { ...action, setVolume };
}