Muting and Deafening
This commit is contained in:
@@ -21,7 +21,10 @@ use ringbuf::{
|
|||||||
};
|
};
|
||||||
use tauri::State;
|
use tauri::State;
|
||||||
|
|
||||||
use crate::{commands::config::ConfigState, crypto::SessionCipher};
|
use crate::{
|
||||||
|
commands::config::{BackendConfig, ConfigState},
|
||||||
|
crypto::SessionCipher,
|
||||||
|
};
|
||||||
|
|
||||||
const TARGET_SAMPLE_RATE: u32 = 48_000;
|
const TARGET_SAMPLE_RATE: u32 = 48_000;
|
||||||
const PACKET_SAMPLES: usize = 960;
|
const PACKET_SAMPLES: usize = 960;
|
||||||
@@ -57,6 +60,8 @@ pub struct VoiceSession {
|
|||||||
|
|
||||||
pub producer_in: AudioProducer,
|
pub producer_in: AudioProducer,
|
||||||
pub consumer_out: AudioConsumer,
|
pub consumer_out: AudioConsumer,
|
||||||
|
|
||||||
|
pub backend_config: Arc<Mutex<BackendConfig>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn resolve_input_device(device_name: &Option<String>) -> Result<cpal::Device, String> {
|
fn resolve_input_device(device_name: &Option<String>) -> Result<cpal::Device, String> {
|
||||||
@@ -72,9 +77,9 @@ fn resolve_input_device(device_name: &Option<String>) -> Result<cpal::Device, St
|
|||||||
.unwrap_or(false)
|
.unwrap_or(false)
|
||||||
})
|
})
|
||||||
.ok_or_else(|| format!("Input device not found: {name}")),
|
.ok_or_else(|| format!("Input device not found: {name}")),
|
||||||
None => host.default_input_device().ok_or_else(|| {
|
None => host
|
||||||
format!("No default input device")
|
.default_input_device()
|
||||||
}),
|
.ok_or_else(|| format!("No default input device")),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,9 +96,9 @@ fn resolve_output_device(device_name: &Option<String>) -> Result<cpal::Device, S
|
|||||||
.unwrap_or(false)
|
.unwrap_or(false)
|
||||||
})
|
})
|
||||||
.ok_or_else(|| format!("Output device not found: {name}")),
|
.ok_or_else(|| format!("Output device not found: {name}")),
|
||||||
None => host.default_output_device().ok_or_else(|| {
|
None => host
|
||||||
format!("No default output device")
|
.default_output_device()
|
||||||
}),
|
.ok_or_else(|| format!("No default output device")),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -561,6 +566,11 @@ pub fn connect_to_vc(
|
|||||||
current_output_device: config.output_device_name.clone(),
|
current_output_device: config.output_device_name.clone(),
|
||||||
producer_in: Arc::clone(&shared_producer_in),
|
producer_in: Arc::clone(&shared_producer_in),
|
||||||
consumer_out: Arc::clone(&shared_consumer_out),
|
consumer_out: Arc::clone(&shared_consumer_out),
|
||||||
|
|
||||||
|
backend_config: Arc::new(Mutex::new(BackendConfig {
|
||||||
|
is_muted: false,
|
||||||
|
is_deaf: false,
|
||||||
|
})),
|
||||||
};
|
};
|
||||||
|
|
||||||
session.setup_input(config.input_device_name.clone(), Arc::clone(&state_inner))?;
|
session.setup_input(config.input_device_name.clone(), Arc::clone(&state_inner))?;
|
||||||
@@ -571,6 +581,7 @@ pub fn connect_to_vc(
|
|||||||
let input_socket = socket.clone();
|
let input_socket = socket.clone();
|
||||||
let shutdown = shutdown.clone();
|
let shutdown = shutdown.clone();
|
||||||
let cipher = cipher.clone();
|
let cipher = cipher.clone();
|
||||||
|
let backend_config = session.backend_config.clone();
|
||||||
|
|
||||||
thread::spawn(move || {
|
thread::spawn(move || {
|
||||||
let mut sequence = 0u32;
|
let mut sequence = 0u32;
|
||||||
@@ -581,6 +592,22 @@ pub fn connect_to_vc(
|
|||||||
let mut noise_floor: f32 = 0.0;
|
let mut noise_floor: f32 = 0.0;
|
||||||
|
|
||||||
while !shutdown.load(Ordering::Relaxed) {
|
while !shutdown.load(Ordering::Relaxed) {
|
||||||
|
let (is_muted, is_deaf) = {
|
||||||
|
let cfg = backend_config.lock().unwrap();
|
||||||
|
(cfg.is_muted, cfg.is_deaf)
|
||||||
|
};
|
||||||
|
|
||||||
|
if is_muted || is_deaf {
|
||||||
|
// Clear audio recorded while muted to avoid stale transmission on unmute
|
||||||
|
let available = consumer_in.occupied_len();
|
||||||
|
if available > 0 {
|
||||||
|
consumer_in.skip(available);
|
||||||
|
}
|
||||||
|
|
||||||
|
thread::sleep(Duration::from_millis(50));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if consumer_in.occupied_len() >= PACKET_SAMPLES {
|
if consumer_in.occupied_len() >= PACKET_SAMPLES {
|
||||||
let _ = consumer_in.pop_slice(&mut frame_buf);
|
let _ = consumer_in.pop_slice(&mut frame_buf);
|
||||||
|
|
||||||
@@ -654,6 +681,7 @@ pub fn connect_to_vc(
|
|||||||
let socket = socket.clone();
|
let socket = socket.clone();
|
||||||
let shutdown = shutdown.clone();
|
let shutdown = shutdown.clone();
|
||||||
let cipher = cipher.clone();
|
let cipher = cipher.clone();
|
||||||
|
let backend_config = session.backend_config.clone();
|
||||||
|
|
||||||
thread::spawn(move || {
|
thread::spawn(move || {
|
||||||
let mut packets: BTreeMap<u32, Vec<f32>> = BTreeMap::new();
|
let mut packets: BTreeMap<u32, Vec<f32>> = BTreeMap::new();
|
||||||
@@ -666,6 +694,16 @@ pub fn connect_to_vc(
|
|||||||
let mut loop_count: u64 = 0;
|
let mut loop_count: u64 = 0;
|
||||||
|
|
||||||
while !shutdown.load(Ordering::Relaxed) {
|
while !shutdown.load(Ordering::Relaxed) {
|
||||||
|
let is_deaf = { backend_config.lock().unwrap().is_deaf };
|
||||||
|
|
||||||
|
if is_deaf {
|
||||||
|
packets.clear();
|
||||||
|
is_prebuffering = true;
|
||||||
|
|
||||||
|
std::thread::sleep(Duration::from_secs(1));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
loop_count += 1;
|
loop_count += 1;
|
||||||
if let Ok(len) = socket.recv(&mut udp_buffer) {
|
if let Ok(len) = socket.recv(&mut udp_buffer) {
|
||||||
recv_count += 1;
|
recv_count += 1;
|
||||||
|
|||||||
@@ -102,3 +102,22 @@ pub fn get_config(app: AppHandle, state: State<ConfigState>) -> Result<Config, S
|
|||||||
|
|
||||||
Ok(config)
|
Ok(config)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct BackendConfig {
|
||||||
|
pub is_muted: bool,
|
||||||
|
pub is_deaf: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn update_backend_config(
|
||||||
|
voice_state: State<'_, VoiceState>,
|
||||||
|
config: BackendConfig,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
if let Some(v) = &mut *voice_state.inner.session.lock().unwrap() {
|
||||||
|
*v.backend_config.lock().unwrap() = config
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ pub fn run() {
|
|||||||
commands::config::update_config,
|
commands::config::update_config,
|
||||||
commands::config::save_config,
|
commands::config::save_config,
|
||||||
commands::config::get_config,
|
commands::config::get_config,
|
||||||
|
commands::config::update_backend_config,
|
||||||
])
|
])
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running tauri application");
|
.expect("error while running tauri application");
|
||||||
|
|||||||
+11
-6
@@ -10,7 +10,7 @@ import { Page } from "@/components/page/PageView";
|
|||||||
import { Channel } from "@/lib/types";
|
import { Channel } from "@/lib/types";
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
import { ClientMethod } from "./protocol";
|
import { ClientMethod } from "./protocol";
|
||||||
import { Config, getConfig } from "@/lib/config";
|
import { BackendConfig, Config, getConfig } from "@/lib/config";
|
||||||
|
|
||||||
ed.hashes.sha512 = sha512;
|
ed.hashes.sha512 = sha512;
|
||||||
|
|
||||||
@@ -35,11 +35,16 @@ export default class Enclave<P = Page> {
|
|||||||
public forceRender: () => void;
|
public forceRender: () => void;
|
||||||
public page?: P;
|
public page?: P;
|
||||||
public config?: Config;
|
public config?: Config;
|
||||||
|
public backendConfig: BackendConfig;
|
||||||
|
|
||||||
public constructor() {
|
public constructor() {
|
||||||
this.serverList = [];
|
this.serverList = [];
|
||||||
this.forceRender = () => {};
|
this.forceRender = () => {};
|
||||||
this.isSettingsOpen = false;
|
this.isSettingsOpen = false;
|
||||||
|
this.backendConfig = {
|
||||||
|
isMuted: false,
|
||||||
|
isDeaf: false,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public async init() {
|
public async init() {
|
||||||
@@ -113,12 +118,12 @@ export default class Enclave<P = Page> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const serveridx = this.serverList.findIndex((server) => {
|
const serveridx = this.serverList.findIndex(
|
||||||
server.hostname === hostname;
|
(server) => server.hostname === hostname,
|
||||||
});
|
);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
this.serverList[serveridx] &&
|
serveridx >= 0 &&
|
||||||
this.serverList[serveridx].publicKey !==
|
this.serverList[serveridx].publicKey !==
|
||||||
base58.encode(this.server.serverPublicKey)
|
base58.encode(this.server.serverPublicKey)
|
||||||
) {
|
) {
|
||||||
@@ -167,7 +172,7 @@ export default class Enclave<P = Page> {
|
|||||||
meta: metaResponse.data as any,
|
meta: metaResponse.data as any,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (this.serverList[serveridx]) {
|
if (serveridx >= 0) {
|
||||||
this.serverList[serveridx] = serverItem;
|
this.serverList[serveridx] = serverItem;
|
||||||
} else {
|
} else {
|
||||||
this.serverList.push(serverItem);
|
this.serverList.push(serverItem);
|
||||||
|
|||||||
@@ -1,11 +1,20 @@
|
|||||||
import { Card } from "../ui/card";
|
import { Card } from "../ui/card";
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from "../ui/avatar";
|
import { Avatar, AvatarFallback, AvatarImage } from "../ui/avatar";
|
||||||
import { CopyIcon, PhoneOff, Settings2Icon, Signal } from "lucide-react";
|
import {
|
||||||
|
CopyIcon,
|
||||||
|
Headset,
|
||||||
|
Mic,
|
||||||
|
MicOff,
|
||||||
|
PhoneOff,
|
||||||
|
Settings2Icon,
|
||||||
|
Signal,
|
||||||
|
} from "lucide-react";
|
||||||
import { Button } from "../ui/button";
|
import { Button } from "../ui/button";
|
||||||
import Enclave from "@/app/app";
|
import Enclave from "@/app/app";
|
||||||
import { base58 } from "@scure/base";
|
import { base58 } from "@scure/base";
|
||||||
import * as ed from "@noble/ed25519";
|
import * as ed from "@noble/ed25519";
|
||||||
import { Channel } from "@/lib/types";
|
import { Channel } from "@/lib/types";
|
||||||
|
import { updateBackendConfig } from "@/lib/config";
|
||||||
|
|
||||||
function findChannel(channels: Channel[], id: string): Channel | undefined {
|
function findChannel(channels: Channel[], id: string): Channel | undefined {
|
||||||
for (const channel of channels) {
|
for (const channel of channels) {
|
||||||
@@ -38,9 +47,9 @@ export default function StatusCard({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="absolute -left-16 right-16 bottom-0 w-[calc(100%+4rem-2rem)] z-10 ml-4 mb-4 @max-[150px]:hidden">
|
<div className="absolute -left-16 right-16 bottom-0 w-[calc(100%+4rem-2rem)] z-10 ml-4 mb-4 @max-[150px]:hidden">
|
||||||
<Card className="px-3 py-3 h-full w-full flex flex-col gap-0.5">
|
<Card className="px-3 py-3 h-full w-full flex flex-col gap-1.5">
|
||||||
{voiceChannel && (
|
{voiceChannel && (
|
||||||
<div className="text-emerald-500 flex flex-row items-center gap-2 border-b pb-2.5">
|
<div className="text-emerald-500 flex flex-row items-center gap-2 border-b pb-2">
|
||||||
<Signal className="h-5" />
|
<Signal className="h-5" />
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<span>Voice Connected</span>
|
<span>Voice Connected</span>
|
||||||
@@ -82,6 +91,46 @@ export default function StatusCard({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center text-muted-foreground">
|
<div className="flex items-center text-muted-foreground">
|
||||||
|
<Button
|
||||||
|
variant={
|
||||||
|
appRef.current?.backendConfig.isDeaf ? "destructive" : "ghost"
|
||||||
|
}
|
||||||
|
className="size-10"
|
||||||
|
onClick={() => {
|
||||||
|
if (!appRef.current) return;
|
||||||
|
|
||||||
|
appRef.current.backendConfig.isDeaf =
|
||||||
|
!appRef.current.backendConfig.isDeaf;
|
||||||
|
|
||||||
|
updateBackendConfig(appRef.current.backendConfig);
|
||||||
|
|
||||||
|
appRef.current.forceRender();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Headset className="size-5.5" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant={
|
||||||
|
appRef.current?.backendConfig.isMuted ? "destructive" : "ghost"
|
||||||
|
}
|
||||||
|
className="size-10"
|
||||||
|
onClick={() => {
|
||||||
|
if (!appRef.current) return;
|
||||||
|
|
||||||
|
appRef.current.backendConfig.isMuted =
|
||||||
|
!appRef.current.backendConfig.isMuted;
|
||||||
|
|
||||||
|
updateBackendConfig(appRef.current.backendConfig);
|
||||||
|
|
||||||
|
appRef.current.forceRender();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{appRef.current?.backendConfig.isMuted ? (
|
||||||
|
<MicOff className="size-5.5" />
|
||||||
|
) : (
|
||||||
|
<Mic className="size-5.5" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
className="size-10"
|
className="size-10"
|
||||||
|
|||||||
@@ -7,6 +7,11 @@ export type Config = {
|
|||||||
outputVolume: number;
|
outputVolume: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type BackendConfig = {
|
||||||
|
isMuted: boolean;
|
||||||
|
isDeaf: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
export async function updateConfig(config: Config): Promise<void> {
|
export async function updateConfig(config: Config): Promise<void> {
|
||||||
await invoke("update_config", { config });
|
await invoke("update_config", { config });
|
||||||
}
|
}
|
||||||
@@ -18,3 +23,9 @@ export async function saveConfig(): Promise<void> {
|
|||||||
export async function getConfig(): Promise<Config> {
|
export async function getConfig(): Promise<Config> {
|
||||||
return await invoke("get_config");
|
return await invoke("get_config");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function updateBackendConfig(
|
||||||
|
config: BackendConfig,
|
||||||
|
): Promise<void> {
|
||||||
|
await invoke("update_backend_config", { config });
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user