# ox_speak – Architecture Audio Temps Réel en Rust ## 📦 Structure actuelle du projet ```text src/ ├── main.rs # Point d'entrée, instancie App et démarre l'orchestration ├── lib.rs # Déclarations des modules principaux │ ├── app/ # Initialisation, gestion de haut niveau de l'application │ ├── mod.rs │ └── app.rs # Struct App : contient les stacks (audio, net) et le EventBus │ ├── core/ # Traitements audio bas-niveau (CPAL, Opus, stats...) │ ├── mod.rs │ ├── capture.rs # Capture micro avec CPAL │ ├── playback.rs # Playback via CPAL │ ├── mixer.rs # Mixage audio │ ├── opus.rs # Encodage/Décodage Opus │ ├── stats.rs # Statistiques audio │ └── rms.rs # Détection de silence via RMS │ ├── domain/ # Types globaux et événements partagés │ ├── mod.rs │ └── event.rs # EventBus (struct + enum Event) pour routing interne │ ├── network/ # Communication réseau (UDP, protocole) │ ├── mod.rs │ ├── udp.rs # Client UDP (envoi/réception) │ └── protocol.rs # Types et parsing de messages réseau │ ├── runtime/ # Logique d'exécution centrale (orchestration) │ ├── mod.rs │ └── dispatcher.rs # Dispatcher central (consomme les Event) ``` --- ## 🛠️ Tâches à faire dans l’ordre 1. **Compléter `domain/event.rs`** - Définir les `enum Event` utilisés dans ton application (ex : `AudioIn`, `EncodedFrame`, etc.) - Structurer `EventBus` avec `crossbeam_channel` 2. **Écrire un `dispatcher` simple dans `runtime/dispatcher.rs`** - Boucle bloquante `while let Ok(event) = rx.recv()` - Match sur chaque type d’événement (→ faire une "god function" au début) - Appeler directement les fonctions concernées (ex : `udp.send()`, `playback.play()`…) 3. **Initialiser `App` dans `app/app.rs`** - Créer une struct `App` contenant : - le `EventBus` - les instances des modules nécessaires (`capture`, `udp`, etc.) - Méthode `run()` qui spawn les threads d’entrée (`audio`, `network`), et lance le `dispatcher` 4. **Compléter les modules `core/` et `network/`** - Rendre chaque module autonome, avec une méthode `start()` ou `run()` prenant un `Sender` - Exemple : `capture.run(tx)` capture le micro et envoie des `AudioIn(Vec)` 5. **Dans `main.rs`** - Créer `App`, puis appeler `app.run()`. 6. (optionnel) **Plus tard : découpler le dispatcher en handlers** - Créer un `trait EventHandler` pour déléguer proprement la logique métier - Injecter les handlers dans le dispatcher pour garder `dispatcher.rs` léger --- ## 🔄 Orchestration globale (comment tout communique) ```text THREADS : - capture_thread : génère AudioIn - opus_encode_thread: encode AudioIn → EncodedFrame - udp_send_thread : reçoit EncodedFrame → envoie réseau - udp_recv_thread : reçoit NetIn → decode → AudioDecoded - playback_thread : lit AudioDecoded TOUS utilisent : Sender (cloné) ↑ EventBus central (domain/event.rs) RECEIVER : dispatcher (runtime/dispatcher.rs) → lit les events → décide quoi faire : encode, envoyer, lire... ``` --- ## 📝 Remarques - Tu peux rester **mono-thread** dans un premier temps pour simplifier le flow. - L’approche actuelle (dispatcher = "god function") est très bien pour commencer **et lisible**. - Tu peux logguer chaque événement dans le dispatcher pour debugger le flux. - `EventBus` est cloné et injecté dans chaque module. **Aucun module ne s'appelle entre eux.** --- ## 🧠 Notes de conception - Tu as choisi de garder la génération de la trame encodée **dans le thread de capture**, ce qui est parfaitement logique ici : tu as 20 ms de temps CPU garanti dans le callback CPAL pour encoder et publier un `Event::EncodedFrame`. - Cette approche évite la surcharge d’un thread supplémentaire et permet une architecture **performante et simple** tant que tu restes dans les contraintes temps réel. --- ## ❓Question > Pour le moment, elle ne se pose pas, car en réalité je vais garder l'ancienne logique du "c'est capture qui va générer la frame encodée", vu que le thread a 20 ms pour bosser entre chaque callback cpal, il a largement le temps de le faire. Donc aucun doute à ce stade. Si tu changes d’avis ou que la logique devient trop lourde dans `capture`, tu pourras refactorer vers un modèle où `AudioIn` est un event brut et l'encodage est fait côté dispatcher ou dans un worker dédié.