Speaking indicator
This commit is contained in:
@@ -41,6 +41,11 @@ export default function Server() {
|
|||||||
[channelId]: remainingUsers,
|
[channelId]: remainingUsers,
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
|
onSpeaking: (voiceId) =>
|
||||||
|
app.setSpeaking((speaking) => ({
|
||||||
|
...speaking,
|
||||||
|
[voiceId]: Date.now(),
|
||||||
|
})),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[ip, messageStore.messages],
|
[ip, messageStore.messages],
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import App from "@/types/app";
|
|||||||
import { ClientMessage, IndicatorContext } from "@/types/protocol";
|
import { ClientMessage, IndicatorContext } from "@/types/protocol";
|
||||||
import { Message } from "@/types/types";
|
import { Message } from "@/types/types";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { startMic } from "@/lib/audio";
|
import { rms, startMic } from "@/lib/audio";
|
||||||
import { Button } from "../../ui/button";
|
import { Button } from "../../ui/button";
|
||||||
import { ChevronLeftIcon, Phone, Volume2Icon } from "lucide-react";
|
import { ChevronLeftIcon, Phone, Volume2Icon } from "lucide-react";
|
||||||
import VoiceGrid from "./VoiceGrid";
|
import VoiceGrid from "./VoiceGrid";
|
||||||
@@ -28,13 +28,43 @@ export default function VoiceBox({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!voiceConn) return;
|
if (!voiceConn) return;
|
||||||
|
if (!app.profile) return;
|
||||||
|
|
||||||
const stop = startMic(sendVoice);
|
const stop = startMic((data) => {
|
||||||
|
if (rms(data) > 0.02) {
|
||||||
|
sendVoice(data);
|
||||||
|
|
||||||
|
if (!app.profile) return;
|
||||||
|
|
||||||
|
const voiceId = app.voiceConns[channelId]?.[app.profile.id];
|
||||||
|
if (!voiceId) return;
|
||||||
|
|
||||||
|
app.setSpeaking((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[voiceId]: Date.now(),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
stop.then((s) => s());
|
stop.then((s) => s());
|
||||||
};
|
};
|
||||||
}, [voiceConn]);
|
}, [voiceConn, channelId, app.profile?.id, app.voiceConns]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const inv = setInterval(() => {
|
||||||
|
const now = Date.now();
|
||||||
|
const TIMEOUT = 500;
|
||||||
|
|
||||||
|
app.setSpeaking((prev) =>
|
||||||
|
Object.fromEntries(
|
||||||
|
Object.entries(prev).filter(([_, ts]) => now - ts < TIMEOUT)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}, 250);
|
||||||
|
|
||||||
|
return () => clearInterval(inv);
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-svh w-full max-h-svh flex flex-col pb-5 pl-5 pr-5 gap-5">
|
<div className="h-svh w-full max-h-svh flex flex-col pb-5 pl-5 pr-5 gap-5">
|
||||||
@@ -53,9 +83,9 @@ export default function VoiceBox({
|
|||||||
{app.voiceConns[channelId] && (
|
{app.voiceConns[channelId] && (
|
||||||
<VoiceGrid
|
<VoiceGrid
|
||||||
users={Object.entries(app.voiceConns[channelId]).map(
|
users={Object.entries(app.voiceConns[channelId]).map(
|
||||||
([userId, _]) => ({
|
([userId, voiceId]) => ({
|
||||||
id: userId,
|
id: userId,
|
||||||
speaking: false,
|
speaking: Boolean(app.speaking[voiceId]),
|
||||||
})
|
})
|
||||||
)}
|
)}
|
||||||
app={app}
|
app={app}
|
||||||
|
|||||||
@@ -21,10 +21,12 @@ export default function useApp(): App {
|
|||||||
const [currentChannel, setCurrentChannel] = useState<Channel | null>(null);
|
const [currentChannel, setCurrentChannel] = useState<Channel | null>(null);
|
||||||
const [server, setServer] = useState<Server>();
|
const [server, setServer] = useState<Server>();
|
||||||
const privateMessages = useMessages();
|
const privateMessages = useMessages();
|
||||||
const [voiceConns, SetVoiceConns] = useState<
|
const [voiceConns, setVoiceConns] = useState<
|
||||||
Record<string, Record<string, number>>
|
Record<string, Record<string, number>>
|
||||||
>({});
|
>({});
|
||||||
|
|
||||||
|
const [speaking, setSpeaking] = useState<Record<number, number>>({});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const stored = Cookies.get("servers");
|
const stored = Cookies.get("servers");
|
||||||
if (!stored) return;
|
if (!stored) return;
|
||||||
@@ -40,8 +42,10 @@ export default function useApp(): App {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
speaking,
|
||||||
|
setSpeaking,
|
||||||
voiceConns,
|
voiceConns,
|
||||||
setVoiceConns: SetVoiceConns,
|
setVoiceConns,
|
||||||
privateMessages,
|
privateMessages,
|
||||||
currentChannel,
|
currentChannel,
|
||||||
setCurrentChannel,
|
setCurrentChannel,
|
||||||
|
|||||||
+10
-1
@@ -42,7 +42,7 @@ export async function startMic(onAudio: (data: Int16Array) => void) {
|
|||||||
chunk.set(buffer.splice(0, buffer.length));
|
chunk.set(buffer.splice(0, buffer.length));
|
||||||
}
|
}
|
||||||
onAudio(chunk);
|
onAudio(chunk);
|
||||||
}, 1000);
|
}, 500);
|
||||||
|
|
||||||
// Return cleanup function
|
// Return cleanup function
|
||||||
return () => {
|
return () => {
|
||||||
@@ -101,3 +101,12 @@ export function playPCM16(u8: Uint8Array) {
|
|||||||
src.connect(audioCtx.destination);
|
src.connect(audioCtx.destination);
|
||||||
src.start();
|
src.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function rms(samples: Int16Array): number {
|
||||||
|
let sum = 0;
|
||||||
|
for (let i = 0; i < samples.length; i++) {
|
||||||
|
const s = samples[i] / 32768; // normalize to [-1, 1]
|
||||||
|
sum += s * s;
|
||||||
|
}
|
||||||
|
return Math.sqrt(sum / samples.length);
|
||||||
|
}
|
||||||
|
|||||||
+5
-3
@@ -17,6 +17,7 @@ type AuthParams = {
|
|||||||
onVoice?: (userId: string, bytes: number[]) => void;
|
onVoice?: (userId: string, bytes: number[]) => void;
|
||||||
onVoiceJoin?: (userId: string, channelId: string, voiceId: number) => void;
|
onVoiceJoin?: (userId: string, channelId: string, voiceId: number) => void;
|
||||||
onVoiceLeave?: (userId: string, channelId: string, voiceId: number) => void;
|
onVoiceLeave?: (userId: string, channelId: string, voiceId: number) => void;
|
||||||
|
onSpeaking?: (voiceId: number) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default async function auth({
|
export default async function auth({
|
||||||
@@ -28,6 +29,7 @@ export default async function auth({
|
|||||||
onIndicator,
|
onIndicator,
|
||||||
onVoiceJoin,
|
onVoiceJoin,
|
||||||
onVoiceLeave,
|
onVoiceLeave,
|
||||||
|
onSpeaking,
|
||||||
}: AuthParams) {
|
}: AuthParams) {
|
||||||
console.log("Authenticating with server id:", id);
|
console.log("Authenticating with server id:", id);
|
||||||
|
|
||||||
@@ -53,6 +55,7 @@ export default async function auth({
|
|||||||
const buffer = await m.data.arrayBuffer();
|
const buffer = await m.data.arrayBuffer();
|
||||||
const bytes = new Uint8Array(buffer);
|
const bytes = new Uint8Array(buffer);
|
||||||
playPCM16(bytes);
|
playPCM16(bytes);
|
||||||
|
if (onSpeaking) onSpeaking(new DataView(buffer).getUint16(0, true));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,9 +85,8 @@ export default async function auth({
|
|||||||
|
|
||||||
switch (msg.type) {
|
switch (msg.type) {
|
||||||
case "authenticated":
|
case "authenticated":
|
||||||
if (!onIndicator) break;
|
if (onIndicator) msg.params.indicators.forEach(onIndicator);
|
||||||
msg.params.indicators.forEach(onIndicator);
|
if (onVoiceJoin)
|
||||||
if (!onVoiceJoin) break;
|
|
||||||
Object.entries(msg.params.voice_chat).forEach(([channelId, users]) =>
|
Object.entries(msg.params.voice_chat).forEach(([channelId, users]) =>
|
||||||
Object.entries(users).forEach(([userId, voiceId]) =>
|
Object.entries(users).forEach(([userId, voiceId]) =>
|
||||||
onVoiceJoin(userId, channelId, voiceId)
|
onVoiceJoin(userId, channelId, voiceId)
|
||||||
|
|||||||
@@ -38,4 +38,7 @@ export default interface App {
|
|||||||
|
|
||||||
voiceConns: Record<string, Record<string, number>>;
|
voiceConns: Record<string, Record<string, number>>;
|
||||||
setVoiceConns: SetState<Record<string, Record<string, number>>>;
|
setVoiceConns: SetState<Record<string, Record<string, number>>>;
|
||||||
|
|
||||||
|
speaking: Record<number, number>;
|
||||||
|
setSpeaking: SetState<Record<number, number>>;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user