From 1af261058f6c18cb5584fb549c1f4eaf72fbae98 Mon Sep 17 00:00:00 2001 From: Leo dev Date: Mon, 5 Jan 2026 22:34:25 +0100 Subject: [PATCH] Voice testing --- public/audio-worklet.js | 29 ++++++++++-- src/components/channel/VoiceBox.tsx | 8 +++- src/lib/audio.ts | 69 +++++++++++++++++++++++++++-- src/lib/auth.ts | 10 ++++- 4 files changed, 106 insertions(+), 10 deletions(-) diff --git a/public/audio-worklet.js b/public/audio-worklet.js index ebd7adf..b3fed3a 100644 --- a/public/audio-worklet.js +++ b/public/audio-worklet.js @@ -1,10 +1,33 @@ class MicProcessor extends AudioWorkletProcessor { + constructor() { + super(); + this.buffer = new Float32Array(960); + this.offset = 0; + } + process(inputs) { const input = inputs[0]; - if (input.length > 0) { - const channelData = input[0]; // Float32Array - this.port.postMessage(channelData); + if (!input || !input[0]) return true; + + const channel = input[0]; + + let i = 0; + while (i < channel.length) { + const remaining = this.buffer.length - this.offset; + const toCopy = Math.min(remaining, channel.length - i); + + this.buffer.set(channel.subarray(i, i + toCopy), this.offset); + + this.offset += toCopy; + i += toCopy; + + if (this.offset === this.buffer.length) { + // COPY before sending + this.port.postMessage(this.buffer.slice()); + this.offset = 0; + } } + return true; } } diff --git a/src/components/channel/VoiceBox.tsx b/src/components/channel/VoiceBox.tsx index 782dd0c..263437d 100644 --- a/src/components/channel/VoiceBox.tsx +++ b/src/components/channel/VoiceBox.tsx @@ -3,7 +3,7 @@ import App from "@/types/app"; import { ClientMessage, IndicatorContext } from "@/types/protocol"; import { Message } from "@/types/types"; -import { useEffect } from "react"; +import { useEffect, useState } from "react"; import { startMic } from "@/lib/audio"; export default function VoiceBox({ @@ -18,7 +18,11 @@ export default function VoiceBox({ sendVoice: (data: Int16Array) => void; }) { useEffect(() => { - startMic(sendVoice); + const stop = startMic(sendVoice); + + return () => { + stop.then((s) => s()); + }; }, []); return ( diff --git a/src/lib/audio.ts b/src/lib/audio.ts index 760433e..d71e39e 100644 --- a/src/lib/audio.ts +++ b/src/lib/audio.ts @@ -1,3 +1,5 @@ +"use client"; + async function getMicStream() { return await navigator.mediaDevices.getUserMedia({ audio: { @@ -10,20 +12,44 @@ async function getMicStream() { export async function startMic(onAudio: (data: Int16Array) => void) { const stream = await getMicStream(); - - const audioCtx = new AudioContext({ sampleRate: 48000 }); + const sampleRate = 48000; + const audioCtx = new AudioContext({ sampleRate }); await audioCtx.audioWorklet.addModule("/audio-worklet.js"); const source = audioCtx.createMediaStreamSource(stream); const worklet = new AudioWorkletNode(audioCtx, "mic-processor"); source.connect(worklet); - worklet.connect(audioCtx.destination); // optional + worklet.connect(audioCtx.destination); + + const buffer: number[] = []; + const chunkSize = sampleRate; worklet.port.onmessage = (e) => { const float32 = e.data as Float32Array; const pcm16 = float32ToPCM16(float32); - onAudio(pcm16); + buffer.push(...pcm16); + }; + + const interval = setInterval(() => { + if (buffer.length === 0) return; + let chunk: Int16Array; + if (buffer.length >= chunkSize) { + chunk = new Int16Array(buffer.splice(0, chunkSize)); + } else { + chunk = new Int16Array(chunkSize); + chunk.set(buffer.splice(0, buffer.length)); + } + onAudio(chunk); + }, 1000); + + // Return cleanup function + return () => { + clearInterval(interval); + worklet.disconnect(); + source.disconnect(); + audioCtx.close(); + stream.getTracks().forEach((t) => t.stop()); }; } @@ -45,3 +71,38 @@ export function sendVoice(ws: WebSocket, voiceId: number, pcm: Int16Array) { ws.send(buffer); } + +let ctx: AudioContext | null = null; + +function getAudioContext(sampleRate: number) { + if (!ctx) { + ctx = new AudioContext({ sampleRate }); + } + return ctx; +} + +export function playPCM16(u8: Uint8Array) { + const audioCtx = getAudioContext(48000); + + // Remove the first two bytes + const trimmed = u8.slice(2); + + const pcm16 = new Int16Array( + trimmed.buffer, + trimmed.byteOffset, + trimmed.byteLength / 2 + ); + + const float32 = new Float32Array(pcm16.length); + for (let i = 0; i < pcm16.length; i++) { + float32[i] = pcm16[i] / 0x8000; + } + + const buffer = audioCtx.createBuffer(1, float32.length, 48000); + buffer.getChannelData(0).set(float32); + + const src = audioCtx.createBufferSource(); + src.buffer = buffer; + src.connect(audioCtx.destination); + src.start(); +} diff --git a/src/lib/auth.ts b/src/lib/auth.ts index ed9f5fc..9f4464b 100644 --- a/src/lib/auth.ts +++ b/src/lib/auth.ts @@ -5,6 +5,7 @@ import { MessageStore } from "@/hooks/use-messages"; import { IndicatorContext, ServerMessage } from "@/types/protocol"; import { authRelay } from "@/actions/auth"; import { getServerById } from "@/actions/get-server"; +import { playPCM16 } from "./audio"; type AuthParams = { id: string; @@ -43,7 +44,14 @@ export default async function auth({ console.log("Connected to WebSocket:", ip); }; - ws.onmessage = (m) => { + ws.onmessage = async (m) => { + if (m.data instanceof Blob) { + const buffer = await m.data.arrayBuffer(); + const bytes = new Uint8Array(buffer); + playPCM16(bytes) + return; + } + var data = JSON.parse(m.data); console.log("Message received:", data);