This commit is contained in:
2025-07-18 01:57:18 +02:00
commit 21164df8cd
60 changed files with 9988 additions and 0 deletions

24
.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

7
.vscode/extensions.json vendored Normal file
View File

@@ -0,0 +1,7 @@
{
"recommendations": [
"Vue.volar",
"tauri-apps.tauri-vscode",
"rust-lang.rust-analyzer"
]
}

7
README.md Normal file
View File

@@ -0,0 +1,7 @@
# Tauri + Vue 3
This template should help get you started developing with Tauri + Vue 3 in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
## Recommended IDE Setup
- [VS Code](https://code.visualstudio.com/) + [Vue - Official](https://marketplace.visualstudio.com/items?itemName=Vue.volar) + [Tauri](https://marketplace.visualstudio.com/items?itemName=tauri-apps.tauri-vscode) + [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer)

14
index.html Normal file
View File

@@ -0,0 +1,14 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Tauri + Vue 3 App</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

25
package.json Normal file
View File

@@ -0,0 +1,25 @@
{
"name": "ox_speak_client",
"private": true,
"version": "0.1.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"tauri": "tauri"
},
"dependencies": {
"@mdi/font": "^7.4.47",
"@tauri-apps/api": "^2",
"@tauri-apps/plugin-opener": "^2",
"vue": "^3.5.13"
},
"devDependencies": {
"@tauri-apps/cli": "^2",
"@vitejs/plugin-vue": "^5.2.1",
"vite": "^6.0.3",
"vite-plugin-vuetify": "^2.1.1",
"vuetify": "^3.8.12"
}
}

6
public/tauri.svg Normal file
View File

@@ -0,0 +1,6 @@
<svg width="206" height="231" viewBox="0 0 206 231" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M143.143 84C143.143 96.1503 133.293 106 121.143 106C108.992 106 99.1426 96.1503 99.1426 84C99.1426 71.8497 108.992 62 121.143 62C133.293 62 143.143 71.8497 143.143 84Z" fill="#FFC131"/>
<ellipse cx="84.1426" cy="147" rx="22" ry="22" transform="rotate(180 84.1426 147)" fill="#24C8DB"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M166.738 154.548C157.86 160.286 148.023 164.269 137.757 166.341C139.858 160.282 141 153.774 141 147C141 144.543 140.85 142.121 140.558 139.743C144.975 138.204 149.215 136.139 153.183 133.575C162.73 127.404 170.292 118.608 174.961 108.244C179.63 97.8797 181.207 86.3876 179.502 75.1487C177.798 63.9098 172.884 53.4021 165.352 44.8883C157.82 36.3744 147.99 30.2165 137.042 27.1546C126.095 24.0926 114.496 24.2568 103.64 27.6274C92.7839 30.998 83.1319 37.4317 75.8437 46.1553C74.9102 47.2727 74.0206 48.4216 73.176 49.5993C61.9292 50.8488 51.0363 54.0318 40.9629 58.9556C44.2417 48.4586 49.5653 38.6591 56.679 30.1442C67.0505 17.7298 80.7861 8.57426 96.2354 3.77762C111.685 -1.01901 128.19 -1.25267 143.769 3.10474C159.348 7.46215 173.337 16.2252 184.056 28.3411C194.775 40.457 201.767 55.4101 204.193 71.404C206.619 87.3978 204.374 103.752 197.73 118.501C191.086 133.25 180.324 145.767 166.738 154.548ZM41.9631 74.275L62.5557 76.8042C63.0459 72.813 63.9401 68.9018 65.2138 65.1274C57.0465 67.0016 49.2088 70.087 41.9631 74.275Z" fill="#FFC131"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M38.4045 76.4519C47.3493 70.6709 57.2677 66.6712 67.6171 64.6132C65.2774 70.9669 64 77.8343 64 85.0001C64 87.1434 64.1143 89.26 64.3371 91.3442C60.0093 92.8732 55.8533 94.9092 51.9599 97.4256C42.4128 103.596 34.8505 112.392 30.1816 122.756C25.5126 133.12 23.9357 144.612 25.6403 155.851C27.3449 167.09 32.2584 177.598 39.7906 186.112C47.3227 194.626 57.153 200.784 68.1003 203.846C79.0476 206.907 90.6462 206.743 101.502 203.373C112.359 200.002 122.011 193.568 129.299 184.845C130.237 183.722 131.131 182.567 131.979 181.383C143.235 180.114 154.132 176.91 164.205 171.962C160.929 182.49 155.596 192.319 148.464 200.856C138.092 213.27 124.357 222.426 108.907 227.222C93.458 232.019 76.9524 232.253 61.3736 227.895C45.7948 223.538 31.8055 214.775 21.0867 202.659C10.3679 190.543 3.37557 175.59 0.949823 159.596C-1.47592 143.602 0.768139 127.248 7.41237 112.499C14.0566 97.7497 24.8183 85.2327 38.4045 76.4519ZM163.062 156.711L163.062 156.711C162.954 156.773 162.846 156.835 162.738 156.897C162.846 156.835 162.954 156.773 163.062 156.711Z" fill="#24C8DB"/>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

1
public/vite.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

7
src-tauri/.gitignore vendored Normal file
View File

@@ -0,0 +1,7 @@
# Generated by Cargo
# will have compiled files and executables
/target/
# Generated by Tauri
# will have schema files for capabilities auto-completion
/gen/schemas

5563
src-tauri/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

36
src-tauri/Cargo.toml Normal file
View File

@@ -0,0 +1,36 @@
[package]
name = "ox_speak_client"
version = "0.1.0"
description = "A Tauri App"
authors = ["you"]
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
# The `_lib` suffix may seem redundant but it is necessary
# to make the lib name unique and wouldn't conflict with the bin name.
# This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519
name = "ox_speak_client_lib"
crate-type = ["staticlib", "cdylib", "rlib"]
[build-dependencies]
tauri-build = { version = "2", features = [] }
[dependencies]
tauri = { version = "2", features = [] }
tauri-plugin-opener = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
parking_lot = "0.12"
tokio = { version = "1.46", features = ["full"] }
cpal = "0.16"
opus = "0.3"
strum = {version = "0.27", features = ["derive"] }
uuid = {version = "1.17", features = ["v4", "serde"] }
event-listener = "5.4"
bytes = "1.10"
moka = {version = "0.12", features = ["future"] }
arc-swap = "1.7"
crossbeam-channel = "0.5"

3
src-tauri/build.rs Normal file
View File

@@ -0,0 +1,3 @@
fn main() {
tauri_build::build()
}

View File

@@ -0,0 +1,10 @@
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "Capability for the main window",
"windows": ["main"],
"permissions": [
"core:default",
"opener:default"
]
}

BIN
src-tauri/icons/128x128.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

BIN
src-tauri/icons/32x32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 974 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 903 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
src-tauri/icons/icon.icns Normal file

Binary file not shown.

BIN
src-tauri/icons/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

BIN
src-tauri/icons/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

1
src-tauri/src/app/mod.rs Normal file
View File

@@ -0,0 +1 @@
pub mod ox_speak_app;

View File

@@ -0,0 +1,113 @@
use std::sync::Arc;
use std::time::Duration;
use tauri::{AppHandle, Emitter, Listener};
use tokio;
use tokio::sync::mpsc;
use crate::core::capture::AudioCapture;
use crate::domain::event::{Event, EventBus};
use crate::network::udp::UdpSession;
use crate::runtime::dispatcher::Dispatcher;
pub struct OxSpeakApp {
// Communication inter-thread
event_bus: EventBus,
dispatcher: Dispatcher,
event_rx: Option<mpsc::Receiver<Event>>,
// Network
udp_session: UdpSession,
// audio
audio_capture: AudioCapture,
// Tauri handle
tauri_handle: AppHandle
}
impl OxSpeakApp {
pub fn new(tauri_handle: AppHandle) -> Self {
println!("Initializing OxSpeakApp");
// Event_bus - communication inter-components
println!("Creating event bus");
let (event_bus, event_rx) = EventBus::new();
// Audio
// todo : pour le moment, paramètre par défaut, on verra plus tard pour dynamiser ça
println!("Initializing audio capture");
let audio_capture = AudioCapture::default(event_bus.clone());
// UdpSession
println!("Initializing UDP session");
let udp_session = UdpSession::new(event_bus.clone());
// Dispatcher - Communication inter-components
println!("Initializing event dispatcher");
let mut dispatcher = Dispatcher::new(event_bus.clone(), udp_session.clone(), tauri_handle.clone());
println!("OxSpeakApp initialization complete");
Self {
event_bus,
dispatcher,
event_rx: Some(event_rx),
udp_session,
audio_capture,
tauri_handle,
}
}
pub async fn start(&mut self) {
println!("Starting OxSpeakApp");
// dispatcher - lancement du process pour la communication inter-process
println!("Starting event dispatcher");
let mut dispatcher = self.dispatcher.clone();
// Prendre l'ownership du receiver (event_rx)
if let Some(event_rx) = self.event_rx.take() {
tokio::spawn(async move {
dispatcher.start(event_rx).await
});
}
// Démarrer la connexion réseau
println!("Connecting to UDP server at 127.0.0.1:5000");
self.udp_session.connect("127.0.0.1:5000").await;
// Démarrer l'audio-capture
println!("Starting audio capture");
self.audio_capture.start().await;
println!("OxSpeakApp started successfully");
let _ = self.tick_tasks().await;
}
pub async fn stop(&mut self) {
println!("Stopping OxSpeakApp");
println!("Stopping audio capture");
self.audio_capture.stop().await;
println!("OxSpeakApp stopped successfully");
}
fn setup_tauri_events(&self) {
println!("Setting up Tauri event listeners");
let event_bus = self.event_bus.clone();
self.tauri_handle.listen("call", |event| {
println!("Received 'call' event from frontend");
event.payload(); // sera le contenu de l'event
});
println!("Tauri event listeners setup complete");
}
async fn tick_tasks(&self) {
let event_bus = self.event_bus.clone();
tokio::spawn(async move {
let mut interval = tokio::time::interval(Duration::from_secs(1));
loop {
interval.tick().await;
let _ = event_bus.emit(Event::TaskTick).await;
}
});
}
}

View File

@@ -0,0 +1,178 @@
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::thread;
use std::thread::JoinHandle;
use cpal::{default_host, BufferSize, Device, SampleRate, Stream, StreamConfig, SupportedStreamConfig};
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
use crate::core::opus::AudioOpus;
use crate::domain::event::{Event, EventBus};
use crate::utils::ringbuf::RingBuffer;
#[derive(Clone)]
pub struct Microphone {
device: Device,
}
pub struct AudioCapture {
event_bus: EventBus,
microphone: Microphone,
running: Arc<AtomicBool>,
ring_buffer: RingBuffer<i16>,
steam: Option<Stream>,
worker: Option<JoinHandle<()>>,
}
impl Microphone {
pub fn new(device: Device) -> Self {
println!("Initializing microphone with device: {}", device.name().unwrap_or_else(|_| "Unknown".to_string()));
Self {
device
}
}
pub fn default() -> Self {
println!("Creating default microphone");
let host = default_host();
let device = host.default_input_device().unwrap();
Self::new(device)
}
pub fn get_input_config(&self) -> SupportedStreamConfig {
self.device.default_input_config().unwrap()
}
pub fn get_stream_config(&self) -> StreamConfig {
let config = self.get_input_config();
let mut stream_config: StreamConfig = config.into();
stream_config.channels = 1;
stream_config.sample_rate = SampleRate(48000);
stream_config.buffer_size = BufferSize::Fixed(960);
stream_config
}
pub fn build_stream<F>(&self, callback: F) -> Stream
where
F: FnMut(&[i16], &cpal::InputCallbackInfo) + Send + 'static,
{
let config = self.get_stream_config();
self.device.build_input_stream(
&config,
callback,
|err| println!("Error input stream: {err}"),
None
).unwrap()
}
}
impl AudioCapture {
pub fn new(event_bus: EventBus, microphone: Microphone) -> Self {
println!("Creating new AudioCapture instance");
Self {
event_bus,
microphone,
running: Arc::new(AtomicBool::new(false)),
ring_buffer: RingBuffer::new(4096),
steam: None,
worker: None,
}
}
pub fn default(event_bus: EventBus) -> Self {
println!("Creating default AudioCapture");
Self::new(event_bus, Microphone::default())
}
pub async fn start(&mut self) {
println!("Starting audio capture");
self.running.store(true, Ordering::Relaxed);
// stream cpal
println!("Setting up audio stream");
let writer = self.ring_buffer.writer();
let stream_running = self.running.clone();
let stream = self.microphone.build_stream(move |data, _| {
if !stream_running.load(Ordering::Relaxed){
return;
}
writer.push_slice_overwrite(data);
});
stream.play().unwrap();
self.steam = Some(stream);
println!("Audio stream started");
// Audio processing worker
println!("Starting audio processing worker");
self.run_processing_worker();
println!("Audio capture fully initialized");
}
pub async fn stop(&mut self) {
println!("Stopping audio capture");
self.running.store(false, Ordering::Relaxed);
println!("Releasing audio stream");
self.steam = None;
self.ring_buffer.force_wake_up();
// code possiblement bloquant, wrap vers un thread tokio bloquant
if let Some(worker) = self.worker.take() {
println!("Waiting for audio processing worker to finish");
tokio::task::spawn_blocking(move || {
worker.join().unwrap();
}).await.unwrap();
}
println!("Clearing ring buffer");
self.ring_buffer.clear();
println!("Audio capture stopped");
}
fn run_processing_worker(&mut self){
println!("Configuring audio processing worker");
let worker_running = self.running.clone();
let event_bus = self.event_bus.clone();
let input_config = self.microphone.get_input_config();
println!("Audio input config: sample rate: {}, channels: {}", input_config.sample_rate().0, input_config.channels());
let opus = AudioOpus::new(input_config.sample_rate().0, input_config.channels(), "voip");
let mut encoder = opus.create_encoder().unwrap();
let reader = self.ring_buffer.reader();
println!("Spawning audio processing thread");
self.worker = Some(thread::spawn(move || {
println!("Audio processing thread started");
let mut frame = [0i16; 960];
let mut frame_count = 0;
while worker_running.load(Ordering::Relaxed) {
let _ = reader.pop_slice_blocking(&mut frame);
if !worker_running.load(Ordering::Relaxed){
println!("Audio processing thread stopping");
break;
}
frame_count += 1;
if frame_count % 100 == 0 {
println!("Processed {} audio frames", frame_count);
}
let raw_data = frame.to_vec();
event_bus.emit_sync(Event::AudioIn(raw_data));
match encoder.encode(&frame){
Ok(encoded_data) => {
event_bus.emit_sync(Event::AudioEncoded(encoded_data))
}
Err(e) => {
println!("Error encoding: {e}");
}
}
}
println!("Audio processing thread finished after processing {} frames", frame_count);
}));
}
}
impl AudioCapture {
fn audio_processing(){
}
}

View File

@@ -0,0 +1 @@
// aller pick l'audio des clients

View File

@@ -0,0 +1,6 @@
pub mod capture;
pub mod mixer;
pub mod opus;
pub mod playback;
pub mod rms;
pub mod stats;

110
src-tauri/src/core/opus.rs Normal file
View File

@@ -0,0 +1,110 @@
use opus::{Application, Channels, Decoder, Encoder};
#[derive(Clone)]
pub struct AudioOpus{
sample_rate: u32,
channels: u16,
application: Application
}
impl AudioOpus {
pub fn new(sample_rate: u32, channels: u16, application: &str) -> Self {
let application = match application {
"voip" => Application::Voip,
"audio" => Application::Audio,
"lowdelay" => Application::LowDelay,
_ => Application::Voip,
};
Self{sample_rate, channels, application}
}
pub fn create_encoder(&self) -> Result<AudioOpusEncoder, String> {
AudioOpusEncoder::new(self.clone())
}
pub fn create_decoder(&self) -> Result<AudioOpusDecoder, String> {
AudioOpusDecoder::new(self.clone())
}
}
pub struct AudioOpusEncoder{
audio_opus: AudioOpus,
encoder: opus::Encoder,
}
impl AudioOpusEncoder {
fn new(audio_opus: AudioOpus) -> Result<Self, String> {
let opus_channel = match audio_opus.channels {
1 => Channels::Mono,
2 => Channels::Stereo,
_ => Channels::Mono,
};
let mut encoder = Encoder::new(audio_opus.sample_rate, opus_channel, audio_opus.application)
.map_err(|e| format!("Échec de création de l'encodeur: {:?}", e))?;
match audio_opus.application {
Application::Voip => {
// Paramètres optimaux pour VoIP: bonne qualité vocale, CPU modéré
let _ = encoder.set_bitrate(opus::Bitrate::Bits(24000)); // 24kbps est bon pour la voix
let _ = encoder.set_vbr(true); // Variable bitrate économise du CPU
let _ = encoder.set_vbr_constraint(false); // Sans contrainte stricte de débit
// Pas de set_complexity (non supporté par la crate)
},
Application::Audio => {
// Musique: priorité à la qualité
let _ = encoder.set_bitrate(opus::Bitrate::Bits(64000));
let _ = encoder.set_vbr(true);
},
Application::LowDelay => {
// Priorité à la latence et l'efficacité CPU
let _ = encoder.set_bitrate(opus::Bitrate::Bits(18000));
let _ = encoder.set_vbr(true);
},
}
Ok(Self{audio_opus, encoder})
}
pub fn encode(&mut self, frames: &[i16]) -> Result<Vec<u8>, String> {
let mut output = vec![0u8; 1276]; // 1276 octets (la vraie worst-case recommandée par Opus).
let len = self.encoder.encode(frames, output.as_mut_slice())
.map_err(|e| format!("Erreur encodage: {:?}", e))?;
output.truncate(len);
Ok(output)
}
// 🔄 Approche avec buffer réutilisable (encore plus optimal)
fn encode_reuse(&mut self, frames: &[i16], output: &mut Vec<u8>) -> Result<usize, String> {
output.clear();
output.resize(1276, 0);
let len = self.encoder.encode(frames, output.as_mut_slice()).unwrap();
output.truncate(len);
Ok(len)
}
}
pub struct AudioOpusDecoder{
audio_opus: AudioOpus,
decoder: opus::Decoder,
}
impl AudioOpusDecoder {
fn new(audio_opus: AudioOpus) -> Result<Self, String> {
let opus_channel = match audio_opus.channels {
1 => Channels::Mono,
2 => Channels::Stereo,
_ => Channels::Mono,
};
let decoder = Decoder::new(audio_opus.sample_rate, opus_channel)
.map_err(|e| format!("Échec de création du décodeur: {:?}", e))?;
Ok(Self{audio_opus, decoder})
}
pub fn decode(&mut self, frames: &[u8]) -> Result<Vec<i16>, String> {
let mut output = vec![0i16; 5760];
let len = self.decoder.decode(frames, output.as_mut_slice(), false).map_err(|e| format!("Erreur décodage: {:?}", e))?;
output.truncate(len);
Ok(output)
}
}

View File

@@ -0,0 +1,100 @@
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::thread::JoinHandle;
use cpal::{default_host, BufferSize, Device, SampleRate, Stream, StreamConfig, SupportedStreamConfig};
use cpal::traits::{DeviceTrait, HostTrait};
use crate::domain::event::EventBus;
use crate::utils::real_time_event::RealTimeEvent;
#[derive(Clone)]
pub struct Speaker {
device: Device
}
pub struct AudioPlayback {
event_bus: EventBus,
speaker: Speaker,
running: Arc<AtomicBool>,
stream: Option<Stream>,
worker: Option<JoinHandle<()>>,
next_tick: RealTimeEvent
}
impl Speaker {
pub fn new(device: Device) -> Self {
Speaker {
device
}
}
pub fn default() -> Self {
let host = default_host();
let device = host.default_output_device().unwrap();
Speaker::new(device)
}
pub fn get_input_config(&self) -> SupportedStreamConfig {
self.device.default_output_config().unwrap()
}
pub fn get_stream_config(&self) -> StreamConfig {
let config = self.get_input_config();
let mut stream_config: StreamConfig = config.into();
stream_config.channels = 2;
stream_config.sample_rate = SampleRate(48000);
stream_config.buffer_size = BufferSize::Fixed(1920);
stream_config
}
pub fn build_stream<F>(&self, callback: F) -> Stream
where
F: FnMut(&mut [i16], &cpal::OutputCallbackInfo) + Send + 'static,
{
let config = self.get_stream_config();
self.device.build_output_stream(
&config,
callback,
|err| println!("Error output stream: {err}"),
None
).unwrap()
}
}
impl AudioPlayback {
pub fn new(event_bus: EventBus, speaker: Speaker) -> Self {
Self {
event_bus,
speaker,
running: Arc::new(AtomicBool::new(false)),
stream: None,
worker: None,
next_tick: RealTimeEvent::new(),
}
}
pub fn default(event_bus: EventBus) -> Self {
let speaker = Speaker::default();
AudioPlayback::new(event_bus, speaker)
}
pub async fn start(&mut self) {
}
pub async fn stop(&mut self) {
self.running.store(false, std::sync::atomic::Ordering::SeqCst);
// stream cpal
println!("Setting up audio playback stream...");
let stream_running = self.running.clone();
let stream = self.speaker.build_stream(move |data, _| {
if !stream_running.load(Ordering::Relaxed){
return;
}
// aller récupérer 1920 sur un buffer
// écrire le contenu dans data
});
}
}

View File

View File

View File

@@ -0,0 +1,87 @@
use std::sync::Arc;
use std::sync::atomic::AtomicU32;
use std::time::Duration;
use tokio::sync::mpsc;
use tokio::sync::oneshot;
use bytes::{Bytes};
use crate::core::opus::{AudioOpus, AudioOpusDecoder};
use crate::utils::ringbuf::{RingBufReader, RingBufWriter, RingBuffer};
use crate::utils::shared_store::SharedArcMap;
struct AudioClient {
uuid: uuid::Uuid,
decode_sender: mpsc::Sender<DecodeRequest>,
buffer_reader: RingBufReader<Vec<i16>>,
buffer_writer: RingBufWriter<Vec<i16>>
}
struct DecodeRequest {
data: Bytes,
sequence: u16,
}
#[derive(Clone)]
struct AudioClientManager {
audio_clients: SharedArcMap<uuid::Uuid, AudioClient>,
}
impl AudioClient {
pub fn new() -> Self {
let (writer, reader) = RingBuffer::<Vec<i16>>::new(1024).split();
let (decode_sender, mut decode_reader) = mpsc::channel::<DecodeRequest>(100);
let decode_handle = tokio::spawn(async move {
let mut decoder = AudioOpus::new(44800, 1, "voip")
.create_decoder().unwrap();
let mut last_sequence: u16 = 0;
while let Some(request) = decode_reader.recv().await {
// si la séquence est "trop vieille" on la drop. (voir plus tard pour un système de ratrapage si c'est possible)
if last_sequence < request.sequence {
// todo : si le décodage est trop long, voir pour le mettre dans un thread
// avec let result = tokio::task::spawn_blocking({
// let data = request.data.clone();
// move || decoder.decode(&data)
// }).await.unwrap();
let start = std::time::Instant::now();
let result = decoder.decode(&request.data);
if start.elapsed() > Duration::from_millis(1) {
println!("⚠️ Frame drop possible: {:?}", start.elapsed());
}
match result {
Ok(audio_frame) => {
// Pousser la frame complète dans le buffer
writer.push(audio_frame);
},
Err(e) => {
eprintln!("Erreur de décodage audio : {}", e);
}
}
last_sequence = request.sequence;
}
}
});
Self {
uuid: uuid::Uuid::new_v4(),
decode_sender,
buffer_reader: reader,
buffer_writer: writer,
}
}
pub async fn write_audio(&self, sequence: u16, data: Bytes) {
let _ = self.decode_sender.send(DecodeRequest {
data,
sequence
});
}
}
impl AudioClientManager {
fn new() -> Self {
Self {
audio_clients: SharedArcMap::new()
}
}
}

View File

@@ -0,0 +1,46 @@
use tokio::sync::mpsc;
use crate::network::protocol::{MessageClient, MessageServer};
pub enum Event {
AppStarted,
AppStopped,
AudioIn(Vec<i16>),
AudioEncoded(Vec<u8>),
NetConnected,
NetDisconnected,
NetIn(MessageServer),
NetOut(MessageClient),
UiStarted,
UiStopped,
TaskTick
}
#[derive(Clone)]
pub struct EventBus {
pub sender: mpsc::Sender<Event>
}
impl EventBus {
pub fn new() -> (Self, mpsc::Receiver<Event>) {
let (sender, receiver) = mpsc::channel(4096);
(Self { sender }, receiver)
}
pub async fn emit(&self, event: Event) {
// s'utilise de cette façon : bus.emit(Event::AudioIn {Vec[0,1,2,3]}.await;
let _ = self.sender.send(event).await;
}
pub fn emit_sync(&self, event: Event) {
let _ = self.sender.try_send(event);
}
pub fn clone_sender(&self) -> mpsc::Sender<Event> {
self.sender.clone()
}
}

View File

@@ -0,0 +1,2 @@
pub mod event;
pub mod audio_client;

7
src-tauri/src/lib.rs Normal file
View File

@@ -0,0 +1,7 @@
pub mod app;
pub mod core;
pub mod domain;
pub mod network;
pub mod runtime;
pub mod utils;
pub mod tauri_ctx;

7
src-tauri/src/main.rs Normal file
View File

@@ -0,0 +1,7 @@
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
#[tokio::main]
async fn main() {
ox_speak_client_lib::tauri_ctx::run().await;
}

View File

@@ -0,0 +1,2 @@
pub mod protocol;
pub mod udp;

View File

@@ -0,0 +1,281 @@
use std::collections::HashSet;
use std::net::SocketAddr;
use bytes::{Bytes, BytesMut, Buf, BufMut};
use uuid::Uuid;
use strum::{EnumIter, FromRepr};
#[repr(u8)]
#[derive(Debug, Clone, Copy, Hash, Eq, PartialEq, EnumIter, FromRepr)]
pub enum UDPMessageType {
Ping = 0,
Audio = 1,
// Futurs types ici...
}
/// Messages client → serveur (SERIALIZE ONLY)
#[derive(Debug, Clone, PartialEq)]
pub enum MessageClient {
Ping { message_id: Uuid },
Audio { sequence: u16, data: Bytes }, // Utilisation de Bytes pour zero-copy
}
/// Messages serveur → client (DESERIALIZE ONLY)
#[derive(Debug, Clone, PartialEq)]
pub enum MessageServer {
Ping { message_id: Uuid },
Audio { user: Uuid, sequence: u16, data: Bytes },
}
#[derive(Debug, Clone, PartialEq)]
pub struct UDPMessage {
pub data: MessageServer,
pub address: SocketAddr,
pub size: usize,
}
#[derive(Debug, Clone, PartialEq)]
pub enum ParseError {
EmptyData,
InvalidData,
InvalidMessageType,
InvalidUuid,
}
impl From<uuid::Error> for ParseError {
fn from(_: uuid::Error) -> Self {
ParseError::InvalidUuid
}
}
impl UDPMessageType {
pub fn from_u8(value: u8) -> Option<Self> {
Self::from_repr(value)
}
pub fn to_u8(self) -> u8 {
self as u8
}
pub fn from_message(message: &[u8]) -> Option<Self> {
if message.is_empty() {
return None;
}
Self::from_u8(message[0])
}
}
impl MessageClient {
/// Sérialisation optimisée avec BytesMut
pub fn to_bytes(&self) -> Bytes {
match self {
Self::Ping { message_id } => {
let mut buf = BytesMut::with_capacity(17);
buf.put_u8(UDPMessageType::Ping as u8);
buf.put_slice(message_id.as_bytes());
buf.freeze()
}
Self::Audio { sequence, data } => {
let mut buf = BytesMut::with_capacity(3 + data.len());
buf.put_u8(UDPMessageType::Audio as u8);
buf.put_u16(*sequence);
buf.put_slice(data);
buf.freeze()
}
}
}
pub fn to_vec(&self) -> Vec<u8> {
self.to_bytes().to_vec()
}
pub fn message_type(&self) -> UDPMessageType {
match self {
Self::Ping { .. } => UDPMessageType::Ping,
Self::Audio { .. } => UDPMessageType::Audio,
}
}
pub fn size(&self) -> usize {
match self {
Self::Ping { .. } => 17, // 1 + 16 (UUID)
Self::Audio { data, .. } => 3 + data.len(), // 1 + 2 + audio_data
}
}
// Constructeurs
pub fn ping(message_id: Uuid) -> Self {
Self::Ping { message_id }
}
pub fn audio(sequence: u16, data: Bytes) -> Self {
Self::Audio { sequence, data }
}
}
impl MessageServer {
/// Parsing zero-copy depuis Bytes
pub fn from_bytes(mut data: Bytes) -> Result<Self, ParseError> {
if data.is_empty() {
return Err(ParseError::EmptyData);
}
let msg_type = data.get_u8(); // Consomme 1 byte
match msg_type {
0 => { // Ping
if data.remaining() < 16 {
return Err(ParseError::InvalidData);
}
let uuid_bytes = data.split_to(16); // Zero-copy split
let message_id = Uuid::from_slice(&uuid_bytes)?;
Ok(Self::Ping { message_id })
}
1 => { // Audio
if data.remaining() < 18 { // 16 (UUID) + 2 (sequence)
return Err(ParseError::InvalidData);
}
let user_bytes = data.split_to(16);
let user = Uuid::from_slice(&user_bytes)?;
let sequence = data.get_u16();
let audio_data = data; // Le reste pour l'audio
Ok(Self::Audio { user, sequence, data: audio_data })
}
_ => Err(ParseError::InvalidMessageType),
}
}
/// Parsing depuis &[u8] - conversion simple vers Bytes puis appel from_bytes
pub fn from_slice(data: &[u8]) -> Result<Self, ParseError> {
let bytes = Bytes::copy_from_slice(data);
Self::from_bytes(bytes)
}
pub fn message_type(&self) -> UDPMessageType {
match self {
Self::Ping { .. } => UDPMessageType::Ping,
Self::Audio { .. } => UDPMessageType::Audio,
}
}
pub fn size(&self) -> usize {
match self {
Self::Ping { .. } => 17, // 1 + 16 (UUID)
Self::Audio { data, .. } => 19 + data.len(), // 1 + 16 + 2 + audio_data
}
}
// Constructeurs
pub fn ping(message_id: Uuid) -> Self {
Self::Ping { message_id }
}
pub fn audio(user: Uuid, sequence: u16, data: Bytes) -> Self {
Self::Audio { user, sequence, data }
}
}
impl UDPMessage {
/// Parsing depuis slice → Bytes (zero-copy si possible)
pub fn from_bytes(address: SocketAddr, data: &[u8]) -> Result<Self, ParseError> {
let original_size = data.len();
let bytes = Bytes::copy_from_slice(data); // Seule allocation
let data = MessageServer::from_bytes(bytes)?;
Ok(Self {
data,
address,
size: original_size
})
}
// Constructeurs
pub fn ping(address: SocketAddr, message_id: Uuid) -> Self {
let data = MessageServer::ping(message_id);
let size = data.size();
Self { data, address, size }
}
pub fn audio(address: SocketAddr, user: Uuid, sequence: u16, data: Bytes) -> Self {
let msg_data = MessageServer::audio(user, sequence, data);
let size = msg_data.size();
Self { data: msg_data, address, size }
}
// Helpers pour récupérer certains éléments des messages
pub fn get_message_id(&self) -> Option<Uuid> {
match &self.data {
MessageServer::Ping { message_id } => Some(*message_id),
_ => None,
}
}
pub fn get_user(&self) -> Option<Uuid> {
match &self.data {
MessageServer::Audio { user, .. } => Some(*user),
_ => None,
}
}
pub fn get_sequence(&self) -> Option<u16> {
match &self.data {
MessageServer::Audio { sequence, .. } => Some(*sequence),
_ => None,
}
}
pub fn get_audio_data(&self) -> Option<&Bytes> {
match &self.data {
MessageServer::Audio { data, .. } => Some(data),
_ => None,
}
}
pub fn message_type(&self) -> UDPMessageType {
self.data.message_type()
}
pub fn size(&self) -> usize {
self.size
}
pub fn address(&self) -> SocketAddr {
self.address
}
}
impl std::fmt::Display for ParseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ParseError::EmptyData => write!(f, "Empty data received"),
ParseError::InvalidData => write!(f, "Invalid data format"),
ParseError::InvalidMessageType => write!(f, "Invalid message type"),
ParseError::InvalidUuid => write!(f, "Invalid UUID format"),
}
}
}
impl std::error::Error for ParseError {}
/// Fonction utilitaire pour inspecter rapidement un message
pub fn peek_message_type(data: &[u8]) -> Option<UDPMessageType> {
if data.is_empty() {
return None;
}
UDPMessageType::from_u8(data[0])
}
/// Validation rapide sans parsing complet
pub fn validate_message_format(data: &[u8]) -> Result<UDPMessageType, ParseError> {
if data.is_empty() {
return Err(ParseError::EmptyData);
}
let msg_type = UDPMessageType::from_u8(data[0])
.ok_or(ParseError::InvalidMessageType)?;
// Validation basique des longueurs
match msg_type {
UDPMessageType::Ping if data.len() != 17 => Err(ParseError::InvalidData),
UDPMessageType::Audio if data.len() < 19 => Err(ParseError::InvalidData),
_ => Ok(msg_type),
}
}

View File

@@ -0,0 +1,216 @@
use std::net::SocketAddr;
use std::sync::Arc;
use parking_lot::RwLock;
use tokio::net::UdpSocket;
use tokio::task::AbortHandle;
use tokio::time::sleep;
use bytes::Bytes;
use crate::domain::event::{Event, EventBus};
use crate::network::protocol::{MessageClient, MessageServer};
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum UdpSessionState {
Disconnected,
Connecting,
Connected,
}
struct UdpSessionInner {
socket: Option<Arc<UdpSocket>>,
abort_handle: Option<Arc<AbortHandle>>,
state: UdpSessionState,
}
#[derive(Clone)]
pub struct UdpSession {
inner: Arc<RwLock<UdpSessionInner>>,
event_bus: EventBus,
}
impl UdpSession {
pub fn new(event_bus: EventBus) -> Self {
Self {
inner: Arc::new(RwLock::new(UdpSessionInner {
socket: None,
abort_handle: None,
state: UdpSessionState::Disconnected,
})),
event_bus,
}
}
pub async fn connect(&self, addr: &str) -> bool {
// Mettre à jour l'état à "Connecting"
{
let mut inner = self.inner.write();
inner.state = UdpSessionState::Connecting;
}
let server_addr: SocketAddr = match addr.parse() {
Ok(addr) => addr,
Err(_) => {
let mut inner = self.inner.write();
inner.state = UdpSessionState::Disconnected;
return false;
},
};
let socket = match UdpSocket::bind("0.0.0.0:0").await {
Ok(socket) => Arc::new(socket),
Err(_) => {
let mut inner = self.inner.write();
inner.state = UdpSessionState::Disconnected;
return false;
},
};
match socket.connect(server_addr).await {
Ok(_) => {},
Err(_) => {
let mut inner = self.inner.write();
inner.state = UdpSessionState::Disconnected;
return false;
}
};
// Réception en arrière-plan
let recv_socket = Arc::clone(&socket);
let event_bus = self.event_bus.clone();
println!("About to spawn receive task...");
let recv_task = tokio::spawn(async move {
println!("Receive task started! Listen {}", recv_socket.local_addr().unwrap());
event_bus.emit(Event::NetConnected).await;
let mut buf = [0u8; 1500];
loop {
match recv_socket.recv(&mut buf).await {
Ok((size)) => {
if let Ok(msg) = MessageServer::from_slice(&buf[..size]) {
event_bus.emit(Event::NetIn(msg)).await;
}
}
Err(_) => {
event_bus.emit(Event::NetDisconnected).await;
break;
},
}
}
});
// Tout mettre à jour en une fois
{
let mut inner = self.inner.write();
inner.socket = Some(socket);
inner.abort_handle = Some(Arc::new(recv_task.abort_handle()));
inner.state = UdpSessionState::Connected;
}
true
}
pub async fn send(&self, msg: MessageClient) -> bool {
// Récupérer la socket si connecté
let socket = {
let inner = self.inner.read();
if inner.state != UdpSessionState::Connected {
return false;
}
inner.socket.as_ref().map(Arc::clone)
};
match socket {
Some(socket) => {
match socket.send(&msg.to_bytes()).await {
Ok(_) => {
// notifier l'eventbus de l'envoie du message
self.event_bus.emit(Event::NetOut(msg)).await;
true
},
Err(_) => {
// En cas d'erreur, marquer comme déconnecté
let mut inner = self.inner.write();
inner.state = UdpSessionState::Disconnected;
false
}
}
}
None => false,
}
}
pub async fn disconnect(&self) {
// Attendre que la connexion soit établie si elle est en cours
loop {
let state = {
let inner = self.inner.read();
inner.state
};
if state != UdpSessionState::Connecting {
break;
}
sleep(std::time::Duration::from_millis(100)).await;
}
// Tout nettoyer en une fois
let abort_handle = {
let mut inner = self.inner.write();
let handle = inner.abort_handle.take();
inner.socket = None;
inner.state = UdpSessionState::Disconnected;
handle
};
// Arrêter la tâche de réception (en dehors du lock)
if let Some(handle) = abort_handle {
handle.abort();
}
}
pub async fn reconnect(&self, addr: &str) -> bool {
println!("Attempting to reconnect to {}", addr);
// Déconnecter proprement d'abord
self.disconnect().await;
// Attendre un peu avant de reconnecter
tokio::time::sleep(std::time::Duration::from_secs(2)).await;
// Tentatives de reconnexion (3 essais)
for attempt in 1..=3 {
println!("Reconnection attempt {}/3", attempt);
if self.connect(addr).await {
println!("Reconnection successful!");
return true;
}
if attempt < 3 {
println!("Reconnection failed, waiting before retry...");
tokio::time::sleep(std::time::Duration::from_secs(5)).await;
}
}
println!("All reconnection attempts failed");
false
}
// Méthodes utilitaires
pub fn get_state(&self) -> UdpSessionState {
let inner = self.inner.read();
inner.state
}
pub fn is_connected(&self) -> bool {
self.get_state() == UdpSessionState::Connected
}
pub fn is_connecting(&self) -> bool {
self.get_state() == UdpSessionState::Connecting
}
pub fn is_disconnected(&self) -> bool {
self.get_state() == UdpSessionState::Disconnected
}
}

View File

@@ -0,0 +1,185 @@
use std::collections::HashMap;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicU16, Ordering};
use std::time::Instant;
use tauri::{AppHandle, Emitter};
use tokio::sync::mpsc;
use bytes::Bytes;
use parking_lot::RwLock;
use tokio::task::AbortHandle;
use uuid::Uuid;
use crate::domain::event::{Event, EventBus};
use crate::network::protocol::{MessageClient, MessageServer};
use crate::network::udp::UdpSession;
#[derive(Debug, Clone)]
struct PingInfo {
sent_at: Instant,
response_time: Option<u64>, // en milliseconds
}
#[derive(Clone)]
pub struct Dispatcher {
event_bus: EventBus,
udp_session: UdpSession,
tauri_handle: AppHandle,
// todo : temporaire, le temps d'avoir un handler
sequence_counter: Arc<AtomicU16>,
can_send_audio: Arc<AtomicBool>,
ping_tracker: Arc<RwLock<HashMap<Uuid, PingInfo>>>,
}
impl PingInfo {
fn new() -> Self {
Self {
sent_at: Instant::now(),
response_time: None,
}
}
fn complete(&mut self) {
self.response_time = Some(self.sent_at.elapsed().as_millis() as u64);
}
fn is_completed(&self) -> bool {
self.response_time.is_some()
}
}
impl Dispatcher {
pub fn new(event_bus: EventBus, udp_session: UdpSession, tauri_handle: AppHandle) -> Self {
Self {
event_bus,
udp_session,
sequence_counter: Arc::new(AtomicU16::new(0)),
tauri_handle,
can_send_audio: Arc::new(AtomicBool::new(false)),
ping_tracker: Arc::new(RwLock::new(HashMap::new())),
}
}
pub async fn start(&mut self, mut receiver: mpsc::Receiver<Event>) {
let (_udp_in_abort_handle, udp_in_sender) = self.udp_in_handler().await;
let udp_session = self.udp_session.clone();
let sequence_counter = self.sequence_counter.clone();
while let Some(event) = receiver.recv().await {
match event {
Event::AudioIn(sample) => {
}
Event::AudioEncoded(sample_encoded) => {
// Conversion de Vec<u8> vers Bytes
let bytes_sample_encoded = Bytes::from(sample_encoded);
let message = MessageClient::audio(
sequence_counter.load(Ordering::Relaxed),
bytes_sample_encoded
);
udp_session.send(message).await;
sequence_counter.fetch_add(1, Ordering::Relaxed);
}
Event::NetIn(message_event) => {
println!("NetIn: {:?}", message_event);
let _ = udp_in_sender.send(message_event).await;
}
Event::NetOut(message_call) => {
if let MessageClient::Ping { message_id } = message_call {
self.ping_tracker.write().insert(message_id, PingInfo::new());
}
}
Event::NetConnected => {
// envoyer un ping pour annoncer au serveur son existence.
udp_session.send(MessageClient::ping(Uuid::new_v4())).await;
}
Event::NetDisconnected => {
println!("Network disconnected, attempting reconnection...");
// Lancer la reconnexion en arrière-plan
udp_session.reconnect("127.0.0.1:5000").await;
}
Event::TaskTick => {
let ping_id = Uuid::new_v4();
udp_session.send(MessageClient::ping(ping_id)).await;
}
_ => {
println!("Event non prit en charge !")
}
}
}
}
pub async fn udp_in_handler(&self) -> (AbortHandle, mpsc::Sender<MessageServer>) {
let (sender, mut consumer) = mpsc::channel::<MessageServer>(1024);
let ping_tracker = Arc::clone(&self.ping_tracker);
let task = tokio::spawn(async move {
while let Some(message) = consumer.recv().await {
match message {
MessageServer::Ping {message_id} => {
// Réponse au ping reçue
let mut tracker = ping_tracker.write();
if let Some(ping_info) = tracker.get_mut(&message_id) {
ping_info.complete();
println!("Ping response: {} -> {}ms",
message_id,
ping_info.response_time.unwrap()
);
} else {
println!("Received ping response for unknown ID: {}", message_id);
}
}
MessageServer::Audio {user, sequence, data} => {
// Audio reçu
}
}
}
});
(task.abort_handle(), sender)
}
// todo : ce qui suit est temporaire le temps d'avoir une vrai gestion de ping
pub fn get_ping_stats(&self) -> (u32, u32, Option<u64>) {
let tracker = self.ping_tracker.read();
let total_pings = tracker.len() as u32;
let completed_pings = tracker.values().filter(|p| p.is_completed()).count() as u32;
let avg_response_time = if completed_pings > 0 {
let total_time: u64 = tracker.values()
.filter_map(|p| p.response_time)
.sum();
Some(total_time / completed_pings as u64)
} else {
None
};
(total_pings, completed_pings, avg_response_time)
}
pub fn get_recent_pings(&self, limit: usize) -> Vec<(Uuid, u64)> {
let tracker = self.ping_tracker.read();
tracker.iter()
.filter_map(|(id, info)| {
info.response_time.map(|time| (*id, time))
})
.take(limit)
.collect()
}
// Nettoyer les vieux pings
pub fn cleanup_old_pings(&self, max_age_seconds: u64) {
let mut tracker = self.ping_tracker.write();
let now = Instant::now();
tracker.retain(|_, ping_info| {
now.duration_since(ping_info.sent_at).as_secs() < max_age_seconds
});
}
}

View File

@@ -0,0 +1 @@
pub mod dispatcher;

View File

@@ -0,0 +1,65 @@
use tauri::Manager;
use std::sync::Arc;
use tokio::sync::Mutex;
use crate::app::ox_speak_app::OxSpeakApp;
pub struct AppState {
pub app: Arc<Mutex<OxSpeakApp>>,
}
// Séparation du generate_context, sinon l'RustRover (l'ide) rame énormément dans la fonction run
fn get_tauri_context() -> tauri::Context<tauri::Wry> {
tauri::generate_context!()
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub async fn run() {
println!("Starting OxSpeak application");
tauri::async_runtime::set(tokio::runtime::Handle::current());
println!("Tokio runtime set");
println!("Generating Tauri context");
let context = get_tauri_context();
println!("Building Tauri application");
tauri::Builder::default()
.plugin(tauri_plugin_opener::init())
.setup(|app| {
println!("Setting up Tauri application");
let my_app = OxSpeakApp::new(app.handle().clone());
let app_state = AppState {
app: Arc::new(Mutex::new(my_app))
};
app.manage(app_state);
println!("App state managed by Tauri");
let tauri_handle = app.handle().clone();
println!("Spawning async task to start the application");
tauri::async_runtime::spawn(async move {
let state = tauri_handle.state::<AppState>();
let mut my_app = state.app.lock().await;
my_app.start().await;
});
println!("Tauri setup complete");
Ok(())
})
// .invoke_handler(tauri::generate_handler![greet])
.run(context)
.expect("error while running tauri application");
}
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
#[tauri::command]
async fn greet(name: &str) -> Result<String, String> {
println!("Hello from Rust: {}", name);
if name.is_empty() {
return Err("Le nom ne peut pas être vide".to_string());
}
// Simulation d'une opération async qui pourrait échouer
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
Ok(format!("Hello, {}!!! You've been greeted from Rust!", name))
}

View File

@@ -0,0 +1,3 @@
pub mod ringbuf;
pub mod real_time_event;
pub mod shared_store;

View File

@@ -0,0 +1,46 @@
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use event_listener::{Event, Listener};
struct RealTimeEventInner{
flag: AtomicBool,
event: Event,
}
#[derive(Clone)]
pub struct RealTimeEvent {
inner: Arc<RealTimeEventInner>,
}
impl RealTimeEvent{
pub fn new() -> Self{
Self{
inner: Arc::new(RealTimeEventInner{
flag: AtomicBool::new(false),
event: Event::new(),
})
}
}
pub fn notify(&self){
self.inner.flag.store(true, Ordering::Release);
self.inner.event.notify(usize::MAX);
}
pub fn wait(&self){
loop {
let listener = self.inner.event.listen();
if self.inner.flag.swap(false, Ordering::Acquire){
break
}
listener.wait();
}
}
}
impl Default for RealTimeEvent{
fn default() -> Self{
Self::new()
}
}

View File

@@ -0,0 +1,772 @@
// Optimisé pour performance audio temps réel avec overwrite automatique
// Version améliorée avec batch processing et gestion intelligente de l'overwrite
// todo : Code généré par IA, je le comprend pas trop trop encore, à peaufiner quand je maitriserais un peu mieux Rust.
use std::cell::UnsafeCell;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use crate::utils::real_time_event::RealTimeEvent;
// ============================================================================
// STRUCTURES PRINCIPALES
// ============================================================================
/// Celui qui écrit dans le buffer (producteur)
pub struct RingBufWriter<T> {
inner: Arc<InnerRingBuf<T>>,
}
/// Celui qui lit depuis le buffer (consommateur)
pub struct RingBufReader<T> {
inner: Arc<InnerRingBuf<T>>,
}
/// Le buffer circulaire interne partagé entre writer et reader
struct InnerRingBuf<T> {
// Le buffer qui contient nos données
buffer: Vec<UnsafeCell<T>>,
// Position où on écrit les nouvelles données
tail: AtomicUsize,
// Position où on lit les données
head: AtomicUsize,
// Pour réveiller le reader quand il y a des nouvelles données
notify: RealTimeEvent,
// Taille du buffer
cap: usize,
// Masque pour optimiser les calculs (cap - 1)
// Au lieu de faire "index % cap", on fait "index & mask" (plus rapide)
mask: usize,
}
// On dit à Rust que c'est safe de partager entre threads
unsafe impl<T: Send> Send for InnerRingBuf<T> {}
unsafe impl<T: Send> Sync for InnerRingBuf<T> {}
// ============================================================================
// FONCTION DE CRÉATION
// ============================================================================
/// Crée un nouveau ring buffer
/// IMPORTANT: cap DOIT être une puissance de 2 (2, 4, 8, 16, 32, 64, 128...)
/// Pourquoi ? Pour l'optimisation avec le masque binaire
pub fn ringbuf<T>(cap: usize) -> (RingBufWriter<T>, RingBufReader<T>) {
let buffer = RingBuffer::new(cap);
buffer.split()
}
#[derive(Clone)]
pub struct RingBuffer<T> {
inner: Arc<InnerRingBuf<T>>,
}
impl<T> RingBuffer<T> {
pub fn new(cap: usize) -> Self {
// Vérifications de sécurité
assert!(cap > 0, "La capacité doit être > 0");
assert!(cap.is_power_of_two(), "La capacité doit être une puissance de 2 (ex: 8, 16, 32...)");
// Crée le buffer avec des cases vides
let mut buffer = Vec::with_capacity(cap);
for _ in 0..cap {
// UnsafeCell permet de modifier même quand c'est partagé entre threads
// On met des valeurs "poubelle" au début
buffer.push(UnsafeCell::new(unsafe { std::mem::zeroed() }));
}
// Crée la structure interne
let inner = Arc::new(InnerRingBuf {
buffer,
tail: AtomicUsize::new(0), // On commence à écrire à l'index 0
head: AtomicUsize::new(0), // On commence à lire à l'index 0
notify: RealTimeEvent::new(),
cap,
mask: cap - 1, // Si cap=8, mask=7. 7 en binaire = 0111
});
Self {
inner
}
}
pub fn writer(&self) -> RingBufWriter<T> {
RingBufWriter { inner: self.inner.clone() }
}
pub fn reader(&self) -> RingBufReader<T> {
RingBufReader { inner: self.inner.clone() }
}
/// Récupère writer et reader en gardant l'accès au buffer original.
/// Utile pour : struct fields, monitoring, accès multiples.
pub fn both(&self) -> (RingBufWriter<T>, RingBufReader<T>) {
(
RingBufWriter { inner: self.inner.clone() },
RingBufReader { inner: self.inner.clone() }
)
}
/// Consomme le buffer et retourne writer/reader (optimisé).
/// Plus efficace que both() - évite 1 clone.
/// Utile pour : setup initial, factory functions.
pub fn split(self) -> (RingBufWriter<T>, RingBufReader<T>) {
(
RingBufWriter { inner: self.inner.clone() },
RingBufReader { inner: self.inner } // Move optimisé
)
}
/// 📊 Méthodes utilitaires directement sur le buffer
pub fn len(&self) -> usize {
let head = self.inner.head.load(Ordering::Relaxed);
let tail = self.inner.tail.load(Ordering::Relaxed);
(tail.wrapping_sub(head)) & self.inner.mask
}
pub fn is_empty(&self) -> bool {
let head = self.inner.head.load(Ordering::Relaxed);
let tail = self.inner.tail.load(Ordering::Relaxed);
head == tail
}
pub fn capacity(&self) -> usize {
self.inner.cap
}
pub fn clear(&self) {
let tail = self.inner.tail.load(Ordering::Acquire);
self.inner.head.store(tail, Ordering::Release);
}
pub fn force_wake_up(&self) {
self.inner.notify.notify()
}
}
// ============================================================================
// IMPLÉMENTATION DU WRITER (celui qui écrit) - VERSION OPTIMISÉE
// ============================================================================
impl<T: Copy> RingBufWriter<T> {
/// Ajoute un élément dans le buffer
/// Si le buffer est plein, écrase les anciens éléments
pub fn push(&self, value: T) {
// 1. Récupère la position actuelle d'écriture
let tail = self.inner.tail.load(Ordering::Relaxed);
// 2. Calcule la prochaine position (avec le masque pour optimiser)
let next_tail = (tail + 1) & self.inner.mask;
// 3. Vérifie si on va rattraper le lecteur
let head = self.inner.head.load(Ordering::Acquire);
if next_tail == head {
// Buffer plein ! On fait avancer le head pour écraser
let new_head = (head + 1) & self.inner.mask;
self.inner.head.store(new_head, Ordering::Release);
}
// 4. Écrit la donnée dans le buffer
unsafe {
// On écrit directement dans la case mémoire
std::ptr::write(self.inner.buffer[tail].get(), value);
}
// 5. Met à jour la position d'écriture
self.inner.tail.store(next_tail, Ordering::Release);
// 6. Réveille le reader s'il attend
self.inner.notify.notify();
}
/// ⚡ VERSION OPTIMISÉE : Ajoute plusieurs éléments d'un coup avec overwrite automatique
/// C'est LA méthode à utiliser pour l'audio temps réel !
pub fn push_slice_overwrite(&self, data: &[T]) -> usize {
let len = data.len();
if len == 0 {
return 0;
}
let mask = self.inner.mask;
let tail = self.inner.tail.load(Ordering::Relaxed);
let head = self.inner.head.load(Ordering::Acquire);
// Calcul de l'espace disponible
let current_used = (tail.wrapping_sub(head)) & mask;
let available = self.inner.cap - current_used;
if len <= available {
// ✅ Assez de place : écriture normale batch (cas le plus fréquent)
self.push_slice_internal(data, tail)
} else {
// ⚡ Pas assez de place : OVERWRITE automatique
// 1. Calculer combien d'éléments anciens on doit écraser
let needed_space = len - available;
// 2. Avancer le head pour libérer exactement l'espace nécessaire
let new_head = (head + needed_space) & mask;
self.inner.head.store(new_head, Ordering::Release);
// 3. Maintenant on a la place, écrire les nouvelles données
self.push_slice_internal(data, tail)
}
}
/// 🚀 Méthode interne optimisée pour l'écriture batch
#[inline]
fn push_slice_internal(&self, data: &[T], tail: usize) -> usize {
let mask = self.inner.mask;
let buffer = &self.inner.buffer;
let len = data.len();
// Optimisation : gestion des cas où on wrap autour du buffer
let tail_pos = tail & mask;
let space_to_end = self.inner.cap - tail_pos;
if len <= space_to_end {
// ✅ Cas simple : tout tient avant la fin du buffer
unsafe {
for (i, &item) in data.iter().enumerate() {
let pos = tail_pos + i;
std::ptr::write(buffer[pos].get(), item);
}
}
} else {
// 🔄 Cas wrap : on doit couper en deux parties
unsafe {
// Première partie : jusqu'à la fin du buffer
for (i, &item) in data[..space_to_end].iter().enumerate() {
let pos = tail_pos + i;
std::ptr::write(buffer[pos].get(), item);
}
// Deuxième partie : depuis le début du buffer
for (i, &item) in data[space_to_end..].iter().enumerate() {
std::ptr::write(buffer[i].get(), item);
}
}
}
// Mettre à jour tail en une seule fois (atomique)
let new_tail = (tail + len) & mask;
self.inner.tail.store(new_tail, Ordering::Release);
// Notifier les readers
self.inner.notify.notify();
len
}
/// Version classique pour compatibilité (utilise push_slice_overwrite en interne)
pub fn push_slice(&self, data: &[T]) -> usize {
self.push_slice_overwrite(data)
}
/// Version spécialisée pour vos frames audio de 960 échantillons
/// Retourne toujours true car overwrite automatique
pub fn push_audio_frame(&self, samples: &[T]) -> bool {
self.push_slice_overwrite(samples);
true // Toujours réussi grâce à l'overwrite
}
/// 📊 Nombre d'éléments qu'on peut écrire sans overwrite
pub fn available_space(&self) -> usize {
let head = self.inner.head.load(Ordering::Relaxed);
let tail = self.inner.tail.load(Ordering::Relaxed);
let used = (tail.wrapping_sub(head)) & self.inner.mask;
self.inner.cap - used
}
/// 📏 Capacité totale du buffer
pub fn capacity(&self) -> usize {
self.inner.cap
}
}
// ============================================================================
// IMPLÉMENTATION DU READER (celui qui lit) - VERSION OPTIMISÉE
// ============================================================================
impl<T: Copy> RingBufReader<T> {
/// Lit un élément en attendant s'il n'y en a pas (BLOQUANT)
pub fn pop_blocking(&self) -> T {
// D'abord on essaie plusieurs fois rapidement (spin)
for _ in 0..100 {
if let Some(val) = self.try_pop() {
return val;
}
// Petite pause pour ne pas surcharger le CPU
std::hint::spin_loop();
}
// Si toujours rien, on attend qu'on nous réveille
loop {
if let Some(val) = self.try_pop() {
return val;
}
// On attend que le writer nous réveille
self.inner.notify.wait();
}
}
/// Essaie de lire un élément (NON-BLOQUANT)
/// Retourne None s'il n'y a rien
pub fn try_pop(&self) -> Option<T> {
// 1. Récupère les positions actuelles
let head = self.inner.head.load(Ordering::Relaxed);
let tail = self.inner.tail.load(Ordering::Acquire);
// 2. Vérifie s'il y a quelque chose à lire
if head == tail {
return None; // Buffer vide
}
// 3. Lit la donnée
let value = unsafe {
std::ptr::read(self.inner.buffer[head & self.inner.mask].get())
};
// 4. Avance la position de lecture
let next_head = (head + 1) & self.inner.mask;
self.inner.head.store(next_head, Ordering::Release);
Some(value)
}
/// 🚀 VERSION OPTIMISÉE : Lit plusieurs éléments d'un coup dans un buffer
pub fn pop_slice(&self, output: &mut [T]) -> usize {
let head = self.inner.head.load(Ordering::Relaxed);
let tail = self.inner.tail.load(Ordering::Acquire);
if head == tail {
return 0; // Buffer vide
}
// Calcule combien d'éléments on peut lire
let available = (tail.wrapping_sub(head)) & self.inner.mask;
let to_read = std::cmp::min(available, output.len());
if to_read == 0 {
return 0;
}
let mask = self.inner.mask;
let buffer = &self.inner.buffer;
let head_pos = head & mask;
let space_to_end = self.inner.cap - head_pos;
if to_read <= space_to_end {
// ✅ Cas simple : tout tient avant la fin du buffer
unsafe {
for i in 0..to_read {
let pos = head_pos + i;
output[i] = std::ptr::read(buffer[pos].get());
}
}
} else {
// 🔄 Cas wrap : on doit lire en deux parties
unsafe {
// Première partie : jusqu'à la fin du buffer
for i in 0..space_to_end {
let pos = head_pos + i;
output[i] = std::ptr::read(buffer[pos].get());
}
// Deuxième partie : depuis le début du buffer
let remaining = to_read - space_to_end;
for i in 0..remaining {
output[space_to_end + i] = std::ptr::read(buffer[i].get());
}
}
}
// Mettre à jour head en une fois
let new_head = (head + to_read) & mask;
self.inner.head.store(new_head, Ordering::Release);
to_read
}
/// Version bloquante pour lire exactement N éléments
pub fn pop_slice_blocking(&self, output: &mut [T]) -> usize {
let mut total_read = 0;
while total_read < output.len() {
let read = self.pop_slice(&mut output[total_read..]);
total_read += read;
if total_read < output.len() {
// Pas assez d'éléments, on attend
self.inner.notify.wait();
}
}
total_read
}
/// Récupère les données disponibles, bloque uniquement si buffer vide
/// Combine la puissance de pop_slice (flexible) avec l'attente automatique
pub fn pop_slice_wait(&self, output: &mut [T]) -> usize {
// ⚡ Tentative non-bloquante d'abord
let read = self.pop_slice(output);
if read > 0 {
return read; // ✅ Données disponibles
}
// 🔔 Buffer vide - attend signal du producteur
self.inner.notify.wait();
// ⚡ Récupère ce qui est maintenant disponible
self.pop_slice(output)
}
/// Vide complètement le buffer
pub fn clear(&self) {
let tail = self.inner.tail.load(Ordering::Acquire);
self.inner.head.store(tail, Ordering::Release);
}
/// Nombre approximatif d'éléments dans le buffer
pub fn len(&self) -> usize {
let head = self.inner.head.load(Ordering::Relaxed);
let tail = self.inner.tail.load(Ordering::Relaxed);
(tail.wrapping_sub(head)) & self.inner.mask
}
/// Le buffer est-il vide ?
pub fn is_empty(&self) -> bool {
let head = self.inner.head.load(Ordering::Relaxed);
let tail = self.inner.tail.load(Ordering::Relaxed);
head == tail
}
/// 📏 Capacité totale du buffer
pub fn capacity(&self) -> usize {
self.inner.cap
}
}
// ============================================================================
// IMPLÉMENTATIONS CLONABLES (pour partager entre threads)
// ============================================================================
impl<T> Clone for RingBufWriter<T> {
fn clone(&self) -> Self {
RingBufWriter {
inner: self.inner.clone(),
}
}
}
impl<T> Clone for RingBufReader<T> {
fn clone(&self) -> Self {
RingBufReader {
inner: self.inner.clone(),
}
}
}
// ============================================================================
// RINGBUFFER AUDIO TEMPS RÉEL - GUIDE COMPLET DES CAS D'USAGE
// ============================================================================
/*
CRÉATION ET CONFIGURATION :
========================
// Création basique (taille DOIT être puissance de 2)
let (writer, reader) = ringbuf::<i16>(1024); // Buffer basique
let (writer, reader) = ringbuf::<f32>(32768); // Audio haute qualité (~0.7s à 48kHz)
let (writer, reader) = ringbuf::<u8>(8192); // Données binaires
// Distribution multi-threads
let buffer = RingBuffer::<i16>::new(16384);
let capture_buffer = buffer.clone(); // Pour thread capture
let encoder_buffer = buffer.clone(); // Pour thread encodage
let stats_buffer = buffer.clone(); // Pour thread statistiques
// Récupération des endpoints
let (writer, reader) = buffer.split(); // Consomme le buffer
let writer = buffer.writer(); // Endpoint writer seul
let reader = buffer.reader(); // Endpoint reader seul
MÉTHODES D'ÉCRITURE (Writer) :
=============================
// Écriture unitaire
writer.push(sample); // Bloque si buffer plein
writer.try_push(sample)?; // Non-bloquant, erreur si plein
// Écriture batch - Mode sécurisé
let written = writer.push_slice(&samples); // Écrit tous ou aucun
writer.try_push_slice(&samples)?; // Non-bloquant
// ⚡ Écriture batch - Mode temps réel (RECOMMANDÉ pour audio)
writer.push_slice_overwrite(&samples); // Jamais bloque, écrase les anciennes données
// Cas d'usage par contexte :
// - Callback audio temps réel
move |audio_data: &[i16], _info| {
writer.push_slice_overwrite(audio_data); // ✅ Performance garantie
}
// - Thread de capture manuel
loop {
let samples = microphone.read_samples()?;
writer.push_slice_overwrite(&samples); // ✅ Jamais de blocage
}
// - Écriture conditionnelle
if buffer.available_write() >= samples.len() {
writer.push_slice(&samples); // Mode sécurisé
} else {
writer.push_slice_overwrite(&samples); // Force l'écriture
}
MÉTHODES DE LECTURE (Reader) :
=============================
// Lecture unitaire
let sample = reader.pop(); // Bloque jusqu'à avoir un élément
let sample = reader.try_pop()?; // Non-bloquant, erreur si vide
// ⚡ Lecture batch - Mode flexible (RECOMMANDÉ)
let mut buffer = vec![0i16; 960];
let read = reader.pop_slice(&mut buffer); // Prend ce qui est dispo (0 à 960)
if read > 0 {
process_audio(&buffer[..read]); // Traite la taille réelle
}
// Lecture batch - Mode blocking (frame exact requis)
let mut buffer = vec![0i16; 960];
let read = reader.pop_slice_blocking(&mut buffer); // Remplit EXACTEMENT le buffer
assert_eq!(read, buffer.len()); // Toujours vrai
encode_fixed_frame(&buffer); // Encodeur exigeant 960 samples
// ⭐ Lecture batch - Mode wait (MEILLEUR DES DEUX)
let mut buffer = vec![0i16; 960];
let read = reader.pop_slice_wait(&mut buffer); // Prend dispo, bloque SEULEMENT si vide
if read > 0 {
process_flexible_audio(&buffer[..read]); // Taille variable OK
}
// Lecture avec timeout
let read = reader.pop_slice_wait_timeout(&mut buffer, Duration::from_millis(10));
match read {
0 => println!("Timeout - pas de données"),
n => process_audio(&buffer[..n]),
}
CAS D'USAGE PAR DOMAINE :
========================
🎵 AUDIO TEMPS RÉEL :
-------------------
// Thread capture (producteur temps réel)
move |data: &[i16], _info| {
writer.push_slice_overwrite(data); // ✅ Jamais bloque
}
// Thread encodage (consommateur temps réel)
loop {
let mut frame = vec![0i16; 960]; // 20ms frame
let samples = reader.pop_slice_wait(&mut frame); // ✅ Prend dispo, attend si vide
if samples >= 480 { // Au moins 10ms
encode_opus(&frame[..samples]);
} else if samples > 0 {
frame[samples..].fill(0); // Padding silence
encode_opus(&frame);
}
}
// Thread playback (deadline critique)
move |output: &mut [i16], _info| {
let read = reader.pop_slice(output); // ✅ Non-bloquant
if read < output.len() {
output[read..].fill(0); // Underrun -> silence
}
}
📊 TRAITEMENT BATCH NON-CRITIQUE :
---------------------------------
// Thread analyse (peut bloquer)
loop {
let mut chunk = vec![0i16; 4800]; // 100ms de données
let read = reader.pop_slice_blocking(&mut chunk); // ✅ OK de bloquer
analyze_frequency_spectrum(&chunk[..read]); // Traitement lourd
}
💾 SAUVEGARDE FICHIER :
----------------------
let mut file = File::create("recording.raw")?;
loop {
let mut buffer = vec![0i16; 8192];
let read = reader.pop_slice_blocking(&mut buffer);
if read == 0 { break; } // EOF
let bytes = bytemuck::cast_slice(&buffer[..read]);
file.write_all(bytes)?; // Écrit séquentiellement
}
🌐 RÉSEAU AVEC BUFFERISATION :
-----------------------------
// Thread envoi réseau
loop {
let mut packet = vec![0u8; 1400]; // MTU Ethernet
let read = reader.pop_slice_wait(&mut packet);
if read > 0 {
udp_socket.send_to(&packet[..read], addr)?;
}
}
// Thread réception réseau
loop {
let mut buffer = [0u8; 1500];
let (size, _addr) = udp_socket.recv_from(&mut buffer)?;
writer.push_slice_overwrite(&buffer[..size]); // Peut perdre paquets
}
PATTERNS AVANCÉS :
=================
🔄 MULTI-PRODUCTEUR, MULTI-CONSOMMATEUR :
----------------------------------------
let buffer = RingBuffer::<i16>::new(32768);
// Plusieurs producteurs (ex: micros)
let mic1_writer = buffer.clone().writer();
let mic2_writer = buffer.clone().writer();
// Plusieurs consommateurs (ex: encodage + stats)
let encoder_reader = buffer.clone().reader();
let stats_reader = buffer.clone().reader();
🏭 PIPELINE AUDIO COMPLEXE :
---------------------------
// Capture -> Filtre -> Encodage -> Réseau
let raw_buffer = RingBuffer::<i16>::new(16384);
let filtered_buffer = RingBuffer::<i16>::new(16384);
// Thread 1: Capture
std::thread::spawn({
let writer = raw_buffer.writer();
move || {
loop {
let samples = capture_audio();
writer.push_slice_overwrite(&samples);
}
}
});
// Thread 2: Filtrage
std::thread::spawn({
let reader = raw_buffer.reader();
let writer = filtered_buffer.writer();
move || {
let mut buffer = vec![0i16; 480];
loop {
let read = reader.pop_slice_wait(&mut buffer);
let filtered = apply_noise_reduction(&buffer[..read]);
writer.push_slice_overwrite(&filtered);
}
}
});
// Thread 3: Encodage + Réseau
std::thread::spawn({
let reader = filtered_buffer.reader();
move || {
let mut buffer = vec![0i16; 960];
loop {
let read = reader.pop_slice_wait(&mut buffer);
let encoded = encode_opus(&buffer[..read]);
send_to_network(&encoded);
}
}
});
OPTIMISATIONS ET BONNES PRATIQUES :
==================================
📏 SIZING DU BUFFER :
--------------------
// Calcul pour audio 48kHz, latence 100ms
const SAMPLE_RATE: usize = 48000;
const LATENCY_MS: usize = 100;
let buffer_size = (SAMPLE_RATE * LATENCY_MS / 1000).next_power_of_two();
let (writer, reader) = ringbuf::<i16>(buffer_size);
💾 GESTION MÉMOIRE :
-------------------
// ✅ Réutiliser les buffers
let mut reusable_buffer = vec![0i16; 960];
loop {
let read = reader.pop_slice(&mut reusable_buffer);
if read > 0 {
process_audio(&reusable_buffer[..read]); // Pas d'allocation
}
}
// ❌ Éviter allocations répétées
loop {
let read = reader.pop_slice_wait(&mut vec![0i16; 960]); // ❌ Alloc à chaque tour
}
📊 MONITORING ET SANTÉ :
-----------------------
// Surveillance utilisation buffer
let usage = buffer.len() as f32 / buffer.capacity() as f32;
match usage {
x if x > 0.9 => println!("⚠️ Buffer presque plein: {:.1}%", x * 100.0),
x if x < 0.1 => println!(" Buffer presque vide: {:.1}%", x * 100.0),
_ => {} // OK
}
// Qualité adaptative selon charge
let quality = match usage {
x if x > 0.8 => AudioQuality::Low, // Réduire latence
x if x < 0.2 => AudioQuality::High, // Augmenter qualité
_ => AudioQuality::Medium,
};
TABLEAU RÉCAPITULATIF DES MÉTHODES :
===================================
| CONTEXTE | ÉCRITURE | LECTURE | RAISON |
|-----------------------|-----------------------|-----------------------|---------------------------|
| Audio temps réel | push_slice_overwrite | pop_slice_wait | Performance + réactivité |
| Callback critique | push_slice_overwrite | pop_slice | Jamais bloquer |
| Traitement batch | push_slice | pop_slice_blocking | Garantie complétude |
| Réseau | push_slice_overwrite | pop_slice_wait | Robustesse + efficacité |
| Sauvegarde fichier | push_slice | pop_slice_blocking | Intégrité données |
| Pipeline flexibile | push_slice_overwrite | pop_slice_wait | Optimal général |
🏆 VOTRE RINGBUFFER = PUISSANCE DES CHANNELS + PERFORMANCE ZERO-COPY ! 🚀
*/

File diff suppressed because it is too large Load Diff

35
src-tauri/tauri.conf.json Normal file
View File

@@ -0,0 +1,35 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "ox_speak_client",
"version": "0.1.0",
"identifier": "com.oxspeak.app",
"build": {
"beforeDevCommand": "yarn dev",
"devUrl": "http://localhost:1420",
"beforeBuildCommand": "yarn build",
"frontendDist": "../dist"
},
"app": {
"windows": [
{
"title": "ox_speak_client",
"width": 1024,
"height": 768
}
],
"security": {
"csp": null
}
},
"bundle": {
"active": true,
"targets": "all",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
]
}
}

165
src/App.old.vue Normal file
View File

@@ -0,0 +1,165 @@
<script setup>
import { ref } from "vue";
import { invoke } from "@tauri-apps/api/core";
const greetMsg = ref("");
const name = ref("");
async function greet() {
try {
greetMsg.value = await invoke("greet", { name: name.value });
} catch (error) {
console.error("Erreur lors du greeting:", error);
greetMsg.value = "Erreur lors du greeting";
}
}
</script>
<template>
<main class="container">
<h1>Welcome to Tauri + Vue</h1>
<div class="row">
<a href="https://vite.dev" target="_blank">
<img src="/vite.svg" class="logo vite" alt="Vite logo" />
</a>
<a href="https://tauri.app" target="_blank">
<img src="/tauri.svg" class="logo tauri" alt="Tauri logo" />
</a>
<a href="https://vuejs.org/" target="_blank">
<img src="./assets/vue.svg" class="logo vue" alt="Vue logo" />
</a>
</div>
<p>Click on the Tauri, Vite, and Vue logos to learn more.</p>
<form class="row" @submit.prevent="greet">
<input id="greet-input" v-model="name" placeholder="Enter a name..." />
<button type="submit">Greet</button>
</form>
<p>{{ greetMsg }}</p>
</main>
</template>
<style scoped>
.logo.vite:hover {
filter: drop-shadow(0 0 2em #747bff);
}
.logo.vue:hover {
filter: drop-shadow(0 0 2em #249b73);
}
</style>
<style>
:root {
font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
font-size: 16px;
line-height: 24px;
font-weight: 400;
color: #0f0f0f;
background-color: #f6f6f6;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-text-size-adjust: 100%;
}
.container {
margin: 0;
padding-top: 10vh;
display: flex;
flex-direction: column;
justify-content: center;
text-align: center;
}
.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
transition: 0.75s;
}
.logo.tauri:hover {
filter: drop-shadow(0 0 2em #24c8db);
}
.row {
display: flex;
justify-content: center;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
h1 {
text-align: center;
}
input,
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
color: #0f0f0f;
background-color: #ffffff;
transition: border-color 0.25s;
box-shadow: 0 2px 2px rgba(0, 0, 0, 0.2);
}
button {
cursor: pointer;
}
button:hover {
border-color: #396cd8;
}
button:active {
border-color: #396cd8;
background-color: #e8e8e8;
}
input,
button {
outline: none;
}
#greet-input {
margin-right: 5px;
}
@media (prefers-color-scheme: dark) {
:root {
color: #f6f6f6;
background-color: #2f2f2f;
}
a:hover {
color: #24c8db;
}
input,
button {
color: #ffffff;
background-color: #0f0f0f98;
}
button:active {
background-color: #0f0f0f69;
}
}
</style>

46
src/App.vue Normal file
View File

@@ -0,0 +1,46 @@
<template>
<v-app>
<v-layout>
<!-- Sidebar Serveurs -->
<v-navigation-drawer
app
width="80"
permanent
>
<!-- ... -->
</v-navigation-drawer>
<!-- Sidebar Channels -->
<v-navigation-drawer
app
width="240"
permanent
>
<!-- ... -->
</v-navigation-drawer>
<!-- Liste des utilisateurs à DROITE -->
<v-navigation-drawer
app
location="right"
width="220"
permanent
>
<v-list>
<v-list-item title="hello"/>
</v-list>
</v-navigation-drawer>
<!-- Main (chat/messages) -->
<v-main>
<v-container>
<v-app-bar>
<v-app-bar-title>Hello</v-app-bar-title>
</v-app-bar>
</v-container>
</v-main>
</v-layout>
</v-app>
</template>
<script setup lang="ts">
</script>

1
src/assets/vue.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>

After

Width:  |  Height:  |  Size: 496 B

5
src/main.js Normal file
View File

@@ -0,0 +1,5 @@
import { createApp } from "vue";
import Vuetify from "./plugins/vuetify.js";
import App from "./App.vue";
createApp(App).use(Vuetify).mount("#app");

29
src/plugins/vuetify.js Normal file
View File

@@ -0,0 +1,29 @@
import '@mdi/font/css/materialdesignicons.css'
import 'vuetify/styles'
import { createVuetify } from 'vuetify'
import colors from 'vuetify/lib/util/colors.mjs'
export default createVuetify({
// components,
// directives,
icons: {
defaultSet: 'mdi',
},
// ssr: false,
theme: {
defaultTheme: "dark",
themes: {
dark: {
colors: {
primary: colors.blue.darken2,
accent: colors.grey.darken3,
secondary: colors.amber.darken3,
info: colors.teal.lighten1,
warning: colors.amber.base,
error: colors.deepOrange.accent4,
success: colors.green.accent3
}
}
}
}
});

32
vite.config.js Normal file
View File

@@ -0,0 +1,32 @@
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import vuetify from "vite-plugin-vuetify";
const host = process.env.TAURI_DEV_HOST;
// https://vite.dev/config/
export default defineConfig(async () => ({
plugins: [vue(), vuetify()],
// Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build`
//
// 1. prevent Vite from obscuring rust errors
clearScreen: false,
// 2. tauri expects a fixed port, fail if that port is not available
server: {
port: 1420,
strictPort: true,
host: host || false,
hmr: host
? {
protocol: "ws",
host,
port: 1421,
}
: undefined,
watch: {
// 3. tell Vite to ignore watching `src-tauri`
ignored: ["**/src-tauri/**"],
},
},
}));

629
yarn.lock Normal file
View File

@@ -0,0 +1,629 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
"@babel/helper-string-parser@^7.27.1":
version "7.27.1"
resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz#54da796097ab19ce67ed9f88b47bb2ec49367687"
integrity sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==
"@babel/helper-validator-identifier@^7.27.1":
version "7.27.1"
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz#a7054dcc145a967dd4dc8fee845a57c1316c9df8"
integrity sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==
"@babel/parser@^7.27.5":
version "7.28.0"
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.28.0.tgz#979829fbab51a29e13901e5a80713dbcb840825e"
integrity sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==
dependencies:
"@babel/types" "^7.28.0"
"@babel/types@^7.28.0":
version "7.28.0"
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.28.0.tgz#2fd0159a6dc7353933920c43136335a9b264d950"
integrity sha512-jYnje+JyZG5YThjHiF28oT4SIZLnYOcSBb6+SDaFIyzDVSkXQmQQYclJ2R+YxcdmK0AX6x1E5OQNtuh3jHDrUg==
dependencies:
"@babel/helper-string-parser" "^7.27.1"
"@babel/helper-validator-identifier" "^7.27.1"
"@esbuild/aix-ppc64@0.25.5":
version "0.25.5"
resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz#4e0f91776c2b340e75558f60552195f6fad09f18"
integrity sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==
"@esbuild/android-arm64@0.25.5":
version "0.25.5"
resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.25.5.tgz#bc766407f1718923f6b8079c8c61bf86ac3a6a4f"
integrity sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==
"@esbuild/android-arm@0.25.5":
version "0.25.5"
resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.25.5.tgz#4290d6d3407bae3883ad2cded1081a234473ce26"
integrity sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==
"@esbuild/android-x64@0.25.5":
version "0.25.5"
resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.25.5.tgz#40c11d9cbca4f2406548c8a9895d321bc3b35eff"
integrity sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==
"@esbuild/darwin-arm64@0.25.5":
version "0.25.5"
resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.25.5.tgz#49d8bf8b1df95f759ac81eb1d0736018006d7e34"
integrity sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==
"@esbuild/darwin-x64@0.25.5":
version "0.25.5"
resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.25.5.tgz#e27a5d92a14886ef1d492fd50fc61a2d4d87e418"
integrity sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==
"@esbuild/freebsd-arm64@0.25.5":
version "0.25.5"
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.5.tgz#97cede59d638840ca104e605cdb9f1b118ba0b1c"
integrity sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==
"@esbuild/freebsd-x64@0.25.5":
version "0.25.5"
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.25.5.tgz#71c77812042a1a8190c3d581e140d15b876b9c6f"
integrity sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==
"@esbuild/linux-arm64@0.25.5":
version "0.25.5"
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.25.5.tgz#f7b7c8f97eff8ffd2e47f6c67eb5c9765f2181b8"
integrity sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==
"@esbuild/linux-arm@0.25.5":
version "0.25.5"
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.25.5.tgz#2a0be71b6cd8201fa559aea45598dffabc05d911"
integrity sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==
"@esbuild/linux-ia32@0.25.5":
version "0.25.5"
resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.25.5.tgz#763414463cd9ea6fa1f96555d2762f9f84c61783"
integrity sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==
"@esbuild/linux-loong64@0.25.5":
version "0.25.5"
resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.25.5.tgz#428cf2213ff786a502a52c96cf29d1fcf1eb8506"
integrity sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==
"@esbuild/linux-mips64el@0.25.5":
version "0.25.5"
resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.25.5.tgz#5cbcc7fd841b4cd53358afd33527cd394e325d96"
integrity sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==
"@esbuild/linux-ppc64@0.25.5":
version "0.25.5"
resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.25.5.tgz#0d954ab39ce4f5e50f00c4f8c4fd38f976c13ad9"
integrity sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==
"@esbuild/linux-riscv64@0.25.5":
version "0.25.5"
resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.25.5.tgz#0e7dd30730505abd8088321e8497e94b547bfb1e"
integrity sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==
"@esbuild/linux-s390x@0.25.5":
version "0.25.5"
resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.25.5.tgz#5669af81327a398a336d7e40e320b5bbd6e6e72d"
integrity sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==
"@esbuild/linux-x64@0.25.5":
version "0.25.5"
resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.25.5.tgz#b2357dd153aa49038967ddc1ffd90c68a9d2a0d4"
integrity sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw==
"@esbuild/netbsd-arm64@0.25.5":
version "0.25.5"
resolved "https://registry.yarnpkg.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.5.tgz#53b4dfb8fe1cee93777c9e366893bd3daa6ba63d"
integrity sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw==
"@esbuild/netbsd-x64@0.25.5":
version "0.25.5"
resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.25.5.tgz#a0206f6314ce7dc8713b7732703d0f58de1d1e79"
integrity sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==
"@esbuild/openbsd-arm64@0.25.5":
version "0.25.5"
resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.5.tgz#2a796c87c44e8de78001d808c77d948a21ec22fd"
integrity sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw==
"@esbuild/openbsd-x64@0.25.5":
version "0.25.5"
resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.25.5.tgz#28d0cd8909b7fa3953af998f2b2ed34f576728f0"
integrity sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==
"@esbuild/sunos-x64@0.25.5":
version "0.25.5"
resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.25.5.tgz#a28164f5b997e8247d407e36c90d3fd5ddbe0dc5"
integrity sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==
"@esbuild/win32-arm64@0.25.5":
version "0.25.5"
resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.25.5.tgz#6eadbead38e8bd12f633a5190e45eff80e24007e"
integrity sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==
"@esbuild/win32-ia32@0.25.5":
version "0.25.5"
resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.25.5.tgz#bab6288005482f9ed2adb9ded7e88eba9a62cc0d"
integrity sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==
"@esbuild/win32-x64@0.25.5":
version "0.25.5"
resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.25.5.tgz#7fc114af5f6563f19f73324b5d5ff36ece0803d1"
integrity sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==
"@jridgewell/sourcemap-codec@^1.5.0":
version "1.5.4"
resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz#7358043433b2e5da569aa02cbc4c121da3af27d7"
integrity sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==
"@mdi/font@^7.4.47":
version "7.4.47"
resolved "https://registry.yarnpkg.com/@mdi/font/-/font-7.4.47.tgz#2ae522867da3a5c88b738d54b403eb91471903af"
integrity sha512-43MtGpd585SNzHZPcYowu/84Vz2a2g31TvPMTm9uTiCSWzaheQySUcSyUH/46fPnuPQWof2yd0pGBtzee/IQWw==
"@rollup/rollup-android-arm-eabi@4.44.2":
version "4.44.2"
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.44.2.tgz#6819b7f1e41a49af566f629a1556eaeea774d043"
integrity sha512-g0dF8P1e2QYPOj1gu7s/3LVP6kze9A7m6x0BZ9iTdXK8N5c2V7cpBKHV3/9A4Zd8xxavdhK0t4PnqjkqVmUc9Q==
"@rollup/rollup-android-arm64@4.44.2":
version "4.44.2"
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.44.2.tgz#7bd5591af68c64a75be1779e2b20f187878daba9"
integrity sha512-Yt5MKrOosSbSaAK5Y4J+vSiID57sOvpBNBR6K7xAaQvk3MkcNVV0f9fE20T+41WYN8hDn6SGFlFrKudtx4EoxA==
"@rollup/rollup-darwin-arm64@4.44.2":
version "4.44.2"
resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.44.2.tgz#e216c333e448c67973386e46dbfe8e381aafb055"
integrity sha512-EsnFot9ZieM35YNA26nhbLTJBHD0jTwWpPwmRVDzjylQT6gkar+zenfb8mHxWpRrbn+WytRRjE0WKsfaxBkVUA==
"@rollup/rollup-darwin-x64@4.44.2":
version "4.44.2"
resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.44.2.tgz#202f80eea3acfe3f67496fedffa006a5f1ce7f5a"
integrity sha512-dv/t1t1RkCvJdWWxQ2lWOO+b7cMsVw5YFaS04oHpZRWehI1h0fV1gF4wgGCTyQHHjJDfbNpwOi6PXEafRBBezw==
"@rollup/rollup-freebsd-arm64@4.44.2":
version "4.44.2"
resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.44.2.tgz#4880f9769f1a7eec436b9c146e1d714338c26567"
integrity sha512-W4tt4BLorKND4qeHElxDoim0+BsprFTwb+vriVQnFFtT/P6v/xO5I99xvYnVzKWrK6j7Hb0yp3x7V5LUbaeOMg==
"@rollup/rollup-freebsd-x64@4.44.2":
version "4.44.2"
resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.44.2.tgz#647d6e333349b1c0fb322c2827ba1a53a0f10301"
integrity sha512-tdT1PHopokkuBVyHjvYehnIe20fxibxFCEhQP/96MDSOcyjM/shlTkZZLOufV3qO6/FQOSiJTBebhVc12JyPTA==
"@rollup/rollup-linux-arm-gnueabihf@4.44.2":
version "4.44.2"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.44.2.tgz#7ba5c97a7224f49618861d093c4a7b40fa50867b"
integrity sha512-+xmiDGGaSfIIOXMzkhJ++Oa0Gwvl9oXUeIiwarsdRXSe27HUIvjbSIpPxvnNsRebsNdUo7uAiQVgBD1hVriwSQ==
"@rollup/rollup-linux-arm-musleabihf@4.44.2":
version "4.44.2"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.44.2.tgz#f858dcf498299d6c625ec697a5191e0e41423905"
integrity sha512-bDHvhzOfORk3wt8yxIra8N4k/N0MnKInCW5OGZaeDYa/hMrdPaJzo7CSkjKZqX4JFUWjUGm88lI6QJLCM7lDrA==
"@rollup/rollup-linux-arm64-gnu@4.44.2":
version "4.44.2"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.44.2.tgz#c0f1fc20c50666c61f574536a00cdd486b6aaae1"
integrity sha512-NMsDEsDiYghTbeZWEGnNi4F0hSbGnsuOG+VnNvxkKg0IGDvFh7UVpM/14mnMwxRxUf9AdAVJgHPvKXf6FpMB7A==
"@rollup/rollup-linux-arm64-musl@4.44.2":
version "4.44.2"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.44.2.tgz#0214efc3e404ddf108e946ad5f7e4ee2792a155a"
integrity sha512-lb5bxXnxXglVq+7imxykIp5xMq+idehfl+wOgiiix0191av84OqbjUED+PRC5OA8eFJYj5xAGcpAZ0pF2MnW+A==
"@rollup/rollup-linux-loongarch64-gnu@4.44.2":
version "4.44.2"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.44.2.tgz#8303c4ea2ae7bcbb96b2c77cfb53527d964bfceb"
integrity sha512-Yl5Rdpf9pIc4GW1PmkUGHdMtbx0fBLE1//SxDmuf3X0dUC57+zMepow2LK0V21661cjXdTn8hO2tXDdAWAqE5g==
"@rollup/rollup-linux-powerpc64le-gnu@4.44.2":
version "4.44.2"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.44.2.tgz#4197ffbc61809629094c0fccf825e43a40fbc0ca"
integrity sha512-03vUDH+w55s680YYryyr78jsO1RWU9ocRMaeV2vMniJJW/6HhoTBwyyiiTPVHNWLnhsnwcQ0oH3S9JSBEKuyqw==
"@rollup/rollup-linux-riscv64-gnu@4.44.2":
version "4.44.2"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.44.2.tgz#bcb99c9004c9b91e3704a6a70c892cb0599b1f42"
integrity sha512-iYtAqBg5eEMG4dEfVlkqo05xMOk6y/JXIToRca2bAWuqjrJYJlx/I7+Z+4hSrsWU8GdJDFPL4ktV3dy4yBSrzg==
"@rollup/rollup-linux-riscv64-musl@4.44.2":
version "4.44.2"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.44.2.tgz#3e943bae9b8b4637c573c1922392beb8a5e81acb"
integrity sha512-e6vEbgaaqz2yEHqtkPXa28fFuBGmUJ0N2dOJK8YUfijejInt9gfCSA7YDdJ4nYlv67JfP3+PSWFX4IVw/xRIPg==
"@rollup/rollup-linux-s390x-gnu@4.44.2":
version "4.44.2"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.44.2.tgz#dc43fb467bff9547f5b9937f38668da07fa8fa9f"
integrity sha512-evFOtkmVdY3udE+0QKrV5wBx7bKI0iHz5yEVx5WqDJkxp9YQefy4Mpx3RajIVcM6o7jxTvVd/qpC1IXUhGc1Mw==
"@rollup/rollup-linux-x64-gnu@4.44.2":
version "4.44.2"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.44.2.tgz#0699c560fa6ce6b846581a7e6c30c85c22a3f0da"
integrity sha512-/bXb0bEsWMyEkIsUL2Yt5nFB5naLAwyOWMEviQfQY1x3l5WsLKgvZf66TM7UTfED6erckUVUJQ/jJ1FSpm3pRQ==
"@rollup/rollup-linux-x64-musl@4.44.2":
version "4.44.2"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.44.2.tgz#9fb1becedcdc9e227d4748576eb8ba2fad8d2e29"
integrity sha512-3D3OB1vSSBXmkGEZR27uiMRNiwN08/RVAcBKwhUYPaiZ8bcvdeEwWPvbnXvvXHY+A/7xluzcN+kaiOFNiOZwWg==
"@rollup/rollup-win32-arm64-msvc@4.44.2":
version "4.44.2"
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.44.2.tgz#fcf3e62edd76c560252b819f69627685f65887d7"
integrity sha512-VfU0fsMK+rwdK8mwODqYeM2hDrF2WiHaSmCBrS7gColkQft95/8tphyzv2EupVxn3iE0FI78wzffoULH1G+dkw==
"@rollup/rollup-win32-ia32-msvc@4.44.2":
version "4.44.2"
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.44.2.tgz#45a5304491d6da4666f6159be4f739d4d43a283f"
integrity sha512-+qMUrkbUurpE6DVRjiJCNGZBGo9xM4Y0FXU5cjgudWqIBWbcLkjE3XprJUsOFgC6xjBClwVa9k6O3A7K3vxb5Q==
"@rollup/rollup-win32-x64-msvc@4.44.2":
version "4.44.2"
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.44.2.tgz#660018c9696ad4f48abe8c5d56db53c81aadba25"
integrity sha512-3+QZROYfJ25PDcxFF66UEk8jGWigHJeecZILvkPkyQN7oc5BvFo4YEXFkOs154j3FTMp9mn9Ky8RCOwastduEA==
"@tauri-apps/api@^2", "@tauri-apps/api@^2.6.0":
version "2.6.0"
resolved "https://registry.yarnpkg.com/@tauri-apps/api/-/api-2.6.0.tgz#efd873bf04b0d72cea81f9397e16218f5deafe0f"
integrity sha512-hRNcdercfgpzgFrMXWwNDBN0B7vNzOzRepy6ZAmhxi5mDLVPNrTpo9MGg2tN/F7JRugj4d2aF7E1rtPXAHaetg==
"@tauri-apps/cli-darwin-arm64@2.6.2":
version "2.6.2"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.6.2.tgz#c69478438cae93dd892ea43d6cf7934a1c7f7839"
integrity sha512-YlvT+Yb7u2HplyN2Cf/nBplCQARC/I4uedlYHlgtxg6rV7xbo9BvG1jLOo29IFhqA2rOp5w1LtgvVGwsOf2kxw==
"@tauri-apps/cli-darwin-x64@2.6.2":
version "2.6.2"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.6.2.tgz#912e837cd012acda602abe471f1a00f4aabed1c9"
integrity sha512-21gdPWfv1bP8rkTdCL44in70QcYcPaDM70L+y78N8TkBuC+/+wqnHcwwjzb+mUyck6UoEw2DORagSI/oKKUGJw==
"@tauri-apps/cli-linux-arm-gnueabihf@2.6.2":
version "2.6.2"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.6.2.tgz#8d97af536341b4d4cfe34933b721063ef0098083"
integrity sha512-MW8Y6HqHS5yzQkwGoLk/ZyE1tWpnz/seDoY4INsbvUZdknuUf80yn3H+s6eGKtT/0Bfqon/W9sY7pEkgHRPQgA==
"@tauri-apps/cli-linux-arm64-gnu@2.6.2":
version "2.6.2"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.6.2.tgz#c1c082a5907b615bd89a0b91f075b4d2853a4cf5"
integrity sha512-9PdINTUtnyrnQt9hvC4y1m0NoxKSw/wUB9OTBAQabPj8WLAdvySWiUpEiqJjwLhlu4T6ltXZRpNTEzous3/RXg==
"@tauri-apps/cli-linux-arm64-musl@2.6.2":
version "2.6.2"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.6.2.tgz#817fcee593e8a81f575200ef52f063e7d78bfbdc"
integrity sha512-LrcJTRr7FrtQlTDkYaRXIGo/8YU/xkWmBPC646WwKNZ/S6yqCiDcOMoPe7Cx4ZvcG6sK6LUCLQMfaSNEL7PT0A==
"@tauri-apps/cli-linux-riscv64-gnu@2.6.2":
version "2.6.2"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.6.2.tgz#e1b6195c9c8c548c01352227cdaed4b55270cf3b"
integrity sha512-GnTshO/BaZ9KGIazz2EiFfXGWgLur5/pjqklRA/ck42PGdUQJhV/Ao7A7TdXPjqAzpFxNo6M/Hx0GCH2iMS7IA==
"@tauri-apps/cli-linux-x64-gnu@2.6.2":
version "2.6.2"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.6.2.tgz#c87f22fb8cea94ea57a96873dc17b16761f1d8e4"
integrity sha512-QDG3WeJD6UJekmrtVPCJRzlKgn9sGzhvD58oAw5gIU+DRovgmmG2U1jH9fS361oYGjWWO7d/KM9t0kugZzi4lQ==
"@tauri-apps/cli-linux-x64-musl@2.6.2":
version "2.6.2"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.6.2.tgz#57d225784f564bef8c036d4f77699689f253555a"
integrity sha512-TNVTDDtnWzuVqWBFdZ4+8ZTg17tc21v+CT5XBQ+KYCoYtCrIaHpW04fS5Tmudi+vYdBwoPDfwpKEB6LhCeFraQ==
"@tauri-apps/cli-win32-arm64-msvc@2.6.2":
version "2.6.2"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.6.2.tgz#a241cb96c23d43066117f0f786a13f8e77462ec3"
integrity sha512-z77C1oa/hMLO/jM1JF39tK3M3v9nou7RsBnQoOY54z5WPcpVAbS0XdFhXB7sSN72BOiO3moDky9lQANQz6L3CA==
"@tauri-apps/cli-win32-ia32-msvc@2.6.2":
version "2.6.2"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.6.2.tgz#3c7797f67118c91ba9d3156130df9bd4d1d7a49e"
integrity sha512-TmD8BbzbjluBw8+QEIWUVmFa9aAluSkT1N937n1mpYLXcPbTpbunqRFiIznTwupoJNJIdtpF/t7BdZDRh5rrcg==
"@tauri-apps/cli-win32-x64-msvc@2.6.2":
version "2.6.2"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.6.2.tgz#f019a0b53be85285b745dd1ac03eb3a35c06e613"
integrity sha512-ItB8RCKk+nCmqOxOvbNtltz6x1A4QX6cSM21kj3NkpcnjT9rHSMcfyf8WVI2fkoMUJR80iqCblUX6ARxC3lj6w==
"@tauri-apps/cli@^2":
version "2.6.2"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli/-/cli-2.6.2.tgz#ecbb4a70f5dd9cebafbc68810d31dd9d75b5888f"
integrity sha512-s1/eyBHxk0wG1blLeOY2IDjgZcxVrkxU5HFL8rNDwjYGr0o7yr3RAtwmuUPhz13NO+xGAL1bJZaLFBdp+5joKg==
optionalDependencies:
"@tauri-apps/cli-darwin-arm64" "2.6.2"
"@tauri-apps/cli-darwin-x64" "2.6.2"
"@tauri-apps/cli-linux-arm-gnueabihf" "2.6.2"
"@tauri-apps/cli-linux-arm64-gnu" "2.6.2"
"@tauri-apps/cli-linux-arm64-musl" "2.6.2"
"@tauri-apps/cli-linux-riscv64-gnu" "2.6.2"
"@tauri-apps/cli-linux-x64-gnu" "2.6.2"
"@tauri-apps/cli-linux-x64-musl" "2.6.2"
"@tauri-apps/cli-win32-arm64-msvc" "2.6.2"
"@tauri-apps/cli-win32-ia32-msvc" "2.6.2"
"@tauri-apps/cli-win32-x64-msvc" "2.6.2"
"@tauri-apps/plugin-opener@^2":
version "2.4.0"
resolved "https://registry.yarnpkg.com/@tauri-apps/plugin-opener/-/plugin-opener-2.4.0.tgz#57eae5998e1c396791af16832a9dde16eca06439"
integrity sha512-43VyN8JJtvKWJY72WI/KNZszTpDpzHULFxQs0CJBIYUdCRowQ6Q1feWTDb979N7nldqSuDOaBupZ6wz2nvuWwQ==
dependencies:
"@tauri-apps/api" "^2.6.0"
"@types/estree@1.0.8":
version "1.0.8"
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.8.tgz#958b91c991b1867ced318bedea0e215ee050726e"
integrity sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==
"@vitejs/plugin-vue@^5.2.1":
version "5.2.4"
resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz#9e8a512eb174bfc2a333ba959bbf9de428d89ad8"
integrity sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==
"@vue/compiler-core@3.5.17":
version "3.5.17"
resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.5.17.tgz#23d291bd01b863da3ef2e26e7db84d8e01a9b4c5"
integrity sha512-Xe+AittLbAyV0pabcN7cP7/BenRBNcteM4aSDCtRvGw0d9OL+HG1u/XHLY/kt1q4fyMeZYXyIYrsHuPSiDPosA==
dependencies:
"@babel/parser" "^7.27.5"
"@vue/shared" "3.5.17"
entities "^4.5.0"
estree-walker "^2.0.2"
source-map-js "^1.2.1"
"@vue/compiler-dom@3.5.17":
version "3.5.17"
resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.5.17.tgz#7bc19a20e23b670243a64b47ce3a890239b870be"
integrity sha512-+2UgfLKoaNLhgfhV5Ihnk6wB4ljyW1/7wUIog2puUqajiC29Lp5R/IKDdkebh9jTbTogTbsgB+OY9cEWzG95JQ==
dependencies:
"@vue/compiler-core" "3.5.17"
"@vue/shared" "3.5.17"
"@vue/compiler-sfc@3.5.17":
version "3.5.17"
resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-3.5.17.tgz#c518871276e26593612bdab36f3f5bcd053b13bf"
integrity sha512-rQQxbRJMgTqwRugtjw0cnyQv9cP4/4BxWfTdRBkqsTfLOHWykLzbOc3C4GGzAmdMDxhzU/1Ija5bTjMVrddqww==
dependencies:
"@babel/parser" "^7.27.5"
"@vue/compiler-core" "3.5.17"
"@vue/compiler-dom" "3.5.17"
"@vue/compiler-ssr" "3.5.17"
"@vue/shared" "3.5.17"
estree-walker "^2.0.2"
magic-string "^0.30.17"
postcss "^8.5.6"
source-map-js "^1.2.1"
"@vue/compiler-ssr@3.5.17":
version "3.5.17"
resolved "https://registry.yarnpkg.com/@vue/compiler-ssr/-/compiler-ssr-3.5.17.tgz#14ba3b7bba6e0e1fd02002316263165a5d1046c7"
integrity sha512-hkDbA0Q20ZzGgpj5uZjb9rBzQtIHLS78mMilwrlpWk2Ep37DYntUz0PonQ6kr113vfOEdM+zTBuJDaceNIW0tQ==
dependencies:
"@vue/compiler-dom" "3.5.17"
"@vue/shared" "3.5.17"
"@vue/reactivity@3.5.17":
version "3.5.17"
resolved "https://registry.yarnpkg.com/@vue/reactivity/-/reactivity-3.5.17.tgz#169b5dcf96c7f23788e5ed9745ec8a7227f2125e"
integrity sha512-l/rmw2STIscWi7SNJp708FK4Kofs97zc/5aEPQh4bOsReD/8ICuBcEmS7KGwDj5ODQLYWVN2lNibKJL1z5b+Lw==
dependencies:
"@vue/shared" "3.5.17"
"@vue/runtime-core@3.5.17":
version "3.5.17"
resolved "https://registry.yarnpkg.com/@vue/runtime-core/-/runtime-core-3.5.17.tgz#b17bd41e13011e85e9b1025545292d43f5512730"
integrity sha512-QQLXa20dHg1R0ri4bjKeGFKEkJA7MMBxrKo2G+gJikmumRS7PTD4BOU9FKrDQWMKowz7frJJGqBffYMgQYS96Q==
dependencies:
"@vue/reactivity" "3.5.17"
"@vue/shared" "3.5.17"
"@vue/runtime-dom@3.5.17":
version "3.5.17"
resolved "https://registry.yarnpkg.com/@vue/runtime-dom/-/runtime-dom-3.5.17.tgz#8e325e29cd03097fe179032fc8df384a426fc83a"
integrity sha512-8El0M60TcwZ1QMz4/os2MdlQECgGoVHPuLnQBU3m9h3gdNRW9xRmI8iLS4t/22OQlOE6aJvNNlBiCzPHur4H9g==
dependencies:
"@vue/reactivity" "3.5.17"
"@vue/runtime-core" "3.5.17"
"@vue/shared" "3.5.17"
csstype "^3.1.3"
"@vue/server-renderer@3.5.17":
version "3.5.17"
resolved "https://registry.yarnpkg.com/@vue/server-renderer/-/server-renderer-3.5.17.tgz#9b8fd6a40a3d55322509fafe78ac841ede649fbe"
integrity sha512-BOHhm8HalujY6lmC3DbqF6uXN/K00uWiEeF22LfEsm9Q93XeJ/plHTepGwf6tqFcF7GA5oGSSAAUock3VvzaCA==
dependencies:
"@vue/compiler-ssr" "3.5.17"
"@vue/shared" "3.5.17"
"@vue/shared@3.5.17":
version "3.5.17"
resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.5.17.tgz#e8b3a41f0be76499882a89e8ed40d86a70fa4b70"
integrity sha512-CabR+UN630VnsJO/jHWYBC1YVXyMq94KKp6iF5MQgZJs5I8cmjw6oVMO1oDbtBkENSHSSn/UadWlW/OAgdmKrg==
"@vuetify/loader-shared@^2.1.0":
version "2.1.0"
resolved "https://registry.yarnpkg.com/@vuetify/loader-shared/-/loader-shared-2.1.0.tgz#29410dce04a78fa9cd40c4d9bc417b8d61ce5103"
integrity sha512-dNE6Ceym9ijFsmJKB7YGW0cxs7xbYV8+1LjU6jd4P14xOt/ji4Igtgzt0rJFbxu+ZhAzqz853lhB0z8V9Dy9cQ==
dependencies:
upath "^2.0.1"
csstype@^3.1.3:
version "3.1.3"
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81"
integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==
debug@^4.3.3:
version "4.4.1"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.1.tgz#e5a8bc6cbc4c6cd3e64308b0693a3d4fa550189b"
integrity sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==
dependencies:
ms "^2.1.3"
entities@^4.5.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48"
integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==
esbuild@^0.25.0:
version "0.25.5"
resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.25.5.tgz#71075054993fdfae76c66586f9b9c1f8d7edd430"
integrity sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==
optionalDependencies:
"@esbuild/aix-ppc64" "0.25.5"
"@esbuild/android-arm" "0.25.5"
"@esbuild/android-arm64" "0.25.5"
"@esbuild/android-x64" "0.25.5"
"@esbuild/darwin-arm64" "0.25.5"
"@esbuild/darwin-x64" "0.25.5"
"@esbuild/freebsd-arm64" "0.25.5"
"@esbuild/freebsd-x64" "0.25.5"
"@esbuild/linux-arm" "0.25.5"
"@esbuild/linux-arm64" "0.25.5"
"@esbuild/linux-ia32" "0.25.5"
"@esbuild/linux-loong64" "0.25.5"
"@esbuild/linux-mips64el" "0.25.5"
"@esbuild/linux-ppc64" "0.25.5"
"@esbuild/linux-riscv64" "0.25.5"
"@esbuild/linux-s390x" "0.25.5"
"@esbuild/linux-x64" "0.25.5"
"@esbuild/netbsd-arm64" "0.25.5"
"@esbuild/netbsd-x64" "0.25.5"
"@esbuild/openbsd-arm64" "0.25.5"
"@esbuild/openbsd-x64" "0.25.5"
"@esbuild/sunos-x64" "0.25.5"
"@esbuild/win32-arm64" "0.25.5"
"@esbuild/win32-ia32" "0.25.5"
"@esbuild/win32-x64" "0.25.5"
estree-walker@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac"
integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==
fdir@^6.4.4:
version "6.4.6"
resolved "https://registry.yarnpkg.com/fdir/-/fdir-6.4.6.tgz#2b268c0232697063111bbf3f64810a2a741ba281"
integrity sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==
fsevents@~2.3.2, fsevents@~2.3.3:
version "2.3.3"
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6"
integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==
magic-string@^0.30.17:
version "0.30.17"
resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.17.tgz#450a449673d2460e5bbcfba9a61916a1714c7453"
integrity sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==
dependencies:
"@jridgewell/sourcemap-codec" "^1.5.0"
ms@^2.1.3:
version "2.1.3"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
nanoid@^3.3.11:
version "3.3.11"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.11.tgz#4f4f112cefbe303202f2199838128936266d185b"
integrity sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==
picocolors@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b"
integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==
picomatch@^4.0.2:
version "4.0.2"
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.2.tgz#77c742931e8f3b8820946c76cd0c1f13730d1dab"
integrity sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==
postcss@^8.5.3, postcss@^8.5.6:
version "8.5.6"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.6.tgz#2825006615a619b4f62a9e7426cc120b349a8f3c"
integrity sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==
dependencies:
nanoid "^3.3.11"
picocolors "^1.1.1"
source-map-js "^1.2.1"
rollup@^4.34.9:
version "4.44.2"
resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.44.2.tgz#faedb27cb2aa6742530c39668092eecbaf78c488"
integrity sha512-PVoapzTwSEcelaWGth3uR66u7ZRo6qhPHc0f2uRO9fX6XDVNrIiGYS0Pj9+R8yIIYSD/mCx2b16Ws9itljKSPg==
dependencies:
"@types/estree" "1.0.8"
optionalDependencies:
"@rollup/rollup-android-arm-eabi" "4.44.2"
"@rollup/rollup-android-arm64" "4.44.2"
"@rollup/rollup-darwin-arm64" "4.44.2"
"@rollup/rollup-darwin-x64" "4.44.2"
"@rollup/rollup-freebsd-arm64" "4.44.2"
"@rollup/rollup-freebsd-x64" "4.44.2"
"@rollup/rollup-linux-arm-gnueabihf" "4.44.2"
"@rollup/rollup-linux-arm-musleabihf" "4.44.2"
"@rollup/rollup-linux-arm64-gnu" "4.44.2"
"@rollup/rollup-linux-arm64-musl" "4.44.2"
"@rollup/rollup-linux-loongarch64-gnu" "4.44.2"
"@rollup/rollup-linux-powerpc64le-gnu" "4.44.2"
"@rollup/rollup-linux-riscv64-gnu" "4.44.2"
"@rollup/rollup-linux-riscv64-musl" "4.44.2"
"@rollup/rollup-linux-s390x-gnu" "4.44.2"
"@rollup/rollup-linux-x64-gnu" "4.44.2"
"@rollup/rollup-linux-x64-musl" "4.44.2"
"@rollup/rollup-win32-arm64-msvc" "4.44.2"
"@rollup/rollup-win32-ia32-msvc" "4.44.2"
"@rollup/rollup-win32-x64-msvc" "4.44.2"
fsevents "~2.3.2"
source-map-js@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46"
integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==
tinyglobby@^0.2.13:
version "0.2.14"
resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.14.tgz#5280b0cf3f972b050e74ae88406c0a6a58f4079d"
integrity sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==
dependencies:
fdir "^6.4.4"
picomatch "^4.0.2"
upath@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/upath/-/upath-2.0.1.tgz#50c73dea68d6f6b990f51d279ce6081665d61a8b"
integrity sha512-1uEe95xksV1O0CYKXo8vQvN1JEbtJp7lb7C5U9HMsIp6IVwntkH/oNUzyVNQSd4S1sYk2FpSSW44FqMc8qee5w==
vite-plugin-vuetify@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/vite-plugin-vuetify/-/vite-plugin-vuetify-2.1.1.tgz#31c958f0c64c436a3165462b81196a7c2ae3a2ff"
integrity sha512-Pb7bKhQH8qPMzURmEGq2aIqCJkruFNsyf1NcrrtnjsOIkqJPMcBbiP0oJoO8/uAmyB5W/1JTbbUEsyXdMM0QHQ==
dependencies:
"@vuetify/loader-shared" "^2.1.0"
debug "^4.3.3"
upath "^2.0.1"
vite@^6.0.3:
version "6.3.5"
resolved "https://registry.yarnpkg.com/vite/-/vite-6.3.5.tgz#fec73879013c9c0128c8d284504c6d19410d12a3"
integrity sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==
dependencies:
esbuild "^0.25.0"
fdir "^6.4.4"
picomatch "^4.0.2"
postcss "^8.5.3"
rollup "^4.34.9"
tinyglobby "^0.2.13"
optionalDependencies:
fsevents "~2.3.3"
vue@^3.5.13:
version "3.5.17"
resolved "https://registry.yarnpkg.com/vue/-/vue-3.5.17.tgz#ea8a6a45abb2b0620e7d479319ce8434b55650cf"
integrity sha512-LbHV3xPN9BeljML+Xctq4lbz2lVHCR6DtbpTf5XIO6gugpXUN49j2QQPcMj086r9+AkJ0FfUT8xjulKKBkkr9g==
dependencies:
"@vue/compiler-dom" "3.5.17"
"@vue/compiler-sfc" "3.5.17"
"@vue/runtime-dom" "3.5.17"
"@vue/server-renderer" "3.5.17"
"@vue/shared" "3.5.17"
vuetify@^3.8.12:
version "3.8.12"
resolved "https://registry.yarnpkg.com/vuetify/-/vuetify-3.8.12.tgz#7c433b8b036011bb0a800f08f5a53d61067eeae8"
integrity sha512-XRX/yRel/V5rlas12ovujVCo8RDb/NwICyef/DVYzybqbYz/UGHZd23sN5q1zw0h9jUN8httXI6ytWN7OFugmA==