diff --git a/src/client_handler.rs b/src/client_handler.rs index 8d294b4..0adc660 100644 --- a/src/client_handler.rs +++ b/src/client_handler.rs @@ -2,51 +2,73 @@ use crate::protocol::Message; use crate::sound_scheduler::SoundScheduler; use std::net::UdpSocket; +// Custom error type for binding errors pub struct BindError { pub message: String, } +// Type alias for Result with BindError as the error type type Result = std::result::Result; +// Trait for handling client messages pub trait ClientHandler { + // Function to handle incoming messages fn handle_message(msg: Message) { match msg { + // Match on the message type and handle accordingly Message::PlaySound(play_sound) => { + // Delegate to SoundScheduler to handle the play sound message SoundScheduler::handle_scheduled_sound(play_sound); } } } } -pub trait Listener : ClientHandler { +// Trait for listening to incoming messages +pub trait Listener: ClientHandler { + // Function to start listening on a given port fn listen(port: u16) -> Result<()>; } +// Struct representing a network listener pub struct NetworkListener; +// Implement ClientHandler for NetworkListener impl ClientHandler for NetworkListener {} +// Implement Listener for NetworkListener impl Listener for NetworkListener { fn listen(port: u16) -> Result<()> { + // Create the bind address string let bind_addr = format!("0.0.0.0:{}", port); + // Attempt to bind the UDP socket to the address let socket = UdpSocket::bind(bind_addr); let socket = match socket { - Ok(s) => s, + Ok(s) => s, // If successful, use the socket Err(e) => { + // If there's an error, return a BindError return Err(BindError { message: e.to_string(), }); } }; + // Print the local address the socket is bound to println!("Listening on {}", socket.local_addr().unwrap()); + // Enter an infinite loop to listen for incoming messages loop { + // Buffer to store incoming data let mut data = [0; 1_048_576]; + // Receive data from the socket let (amt, src) = socket.recv_from(&mut data).expect("Didn't receive data"); + // Print the amount of data received and the source address println!("Received {} bytes from {}", amt, src); + // Slice the buffer to the actual amount of data received let data = &mut data[..amt]; + // Deserialize the data into a Message let msg = bincode::deserialize::(data).expect("Failed to deserialize"); + // Handle the deserialized message NetworkListener::handle_message(msg); } } diff --git a/src/dispatcher.rs b/src/dispatcher.rs index c2f1898..b5ac568 100644 --- a/src/dispatcher.rs +++ b/src/dispatcher.rs @@ -8,62 +8,77 @@ use rodio::{Decoder, Source}; use crate::protocol::{Message, PlaySound}; +// Define a trait for dispatching tasks pub trait Dispatcher { + // Method to handle dispatching tasks to multiple addresses fn handle_dispatch(addrs: Vec, sound_path: String); } +// Struct for network-based dispatching pub struct NetworkDispatcher; impl NetworkDispatcher { + // Load a sound file and return a Decoder fn load_sound(path: String) -> Decoder> { - let file = BufReader::new(File::open(path).unwrap()); - Decoder::new(file).unwrap() + let file = BufReader::new(File::open(path).unwrap()); // Open the file + Decoder::new(file).unwrap() // Decode the file } } impl Dispatcher for NetworkDispatcher { fn handle_dispatch(addrs: Vec, sound_path: String) { + // Get the current system time since UNIX_EPOCH let now = SystemTime::now() .duration_since(SystemTime::UNIX_EPOCH) .unwrap(); + // Load and buffer the sound source let source = NetworkDispatcher::load_sound(sound_path).buffered(); + // Get sound properties let channels = source.channels(); let sample_rate = source.sample_rate(); let duration = source.total_duration().unwrap(); - let chunk_len = Duration::from_millis(200); + let chunk_len = Duration::from_millis(200); // Length of each chunk to send - let mut offset = Duration::from_secs(0); + let mut offset = Duration::from_secs(0); // Initial offset + // Iterate over each address for addr in addrs { if addr.is_empty() { - continue; + continue; // Skip empty addresses } - let source = source.clone(); + let source = source.clone(); // Clone the source for each thread + // Spawn a new thread for each address thread::spawn(move || { + // Bind to a local UDP socket let sock = UdpSocket::bind("0.0.0.0:0").expect("Failed to bind"); - sock.connect(addr.clone()).expect("Failed to connect"); + sock.connect(addr.clone()).expect("Failed to connect"); // Connect to the address + // Loop until the entire duration is covered while offset < duration { + // Collect sound data for the current chunk let sound_data = source .clone() .skip_duration(offset) .take_duration(chunk_len) .collect::>(); - offset += chunk_len; + offset += chunk_len; // Update the offset + // Create a PlaySound message let msg = Message::PlaySound(PlaySound { - timestamp: (now + offset).as_micros(), + timestamp: (now + offset).as_micros(), // Timestamp for synchronization channels: channels, sample_rate: sample_rate, sound_data: sound_data, }); + // Serialize the message let buf = bincode::serialize(&msg).expect("Failed to serialize"); + // Send the serialized message via UDP sock.send(&buf).expect("Failed to send"); } }); diff --git a/src/main.rs b/src/main.rs index fc80f00..3c000e8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,11 +1,13 @@ -#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // Hide console window on Windows in release mode +// Import necessary modules and crates use client_handler::{Listener, NetworkListener}; use std::thread; use ui_app::UIApp; use eframe::egui; +// Declare the modules used in this project mod client_handler; mod dispatcher; mod protocol; @@ -14,37 +16,41 @@ mod sound_scheduler; mod ui_app; fn main() -> eframe::Result { + // Define options for the eframe application let options = eframe::NativeOptions { - viewport: egui::ViewportBuilder::default().with_inner_size([700.0, 400.0]), + viewport: egui::ViewportBuilder::default().with_inner_size([700.0, 400.0]), // Set initial window size ..Default::default() }; + // Spawn a new thread to handle network listening thread::spawn(move || { + // Attempt to listen on port 3000 let result = NetworkListener::listen(3000); match result { Ok(_) => {} Err(e) => { - eprintln!("Failed to bind: {}", e.message); - // Try with port 3001 + eprintln!("Failed to bind: {}", e.message); // Print error message if binding fails + // Try with port 3001 if port 3000 fails let result = NetworkListener::listen(3001); match result { Ok(_) => {} Err(e) => { - eprintln!("Failed to bind again: {}", e.message); + eprintln!("Failed to bind again: {}", e.message); // Print error message if binding fails again } } } } }); + // Run the eframe application eframe::run_native( - "My egui App", - options, + "My egui App", // Application title + options, // Application options Box::new(|cc| { // This gives us image support: - egui_extras::install_image_loaders(&cc.egui_ctx); + egui_extras::install_image_loaders(&cc.egui_ctx); // Install image loaders for egui context - Ok(Box::::default()) + Ok(Box::::default()) // Initialize the UI application }), ) } diff --git a/src/protocol.rs b/src/protocol.rs index 84397d1..2450483 100644 --- a/src/protocol.rs +++ b/src/protocol.rs @@ -1,14 +1,16 @@ -use serde::{Deserialize, Serialize}; +use serde::{Deserialize, Serialize}; // Importing necessary traits from serde for serialization and deserialization -#[derive(Serialize, Deserialize, Debug)] +// Define an enum called Message which can hold different types of messages +#[derive(Serialize, Deserialize, Debug)] // Automatically generate code for serialization, deserialization, and debugging pub enum Message { - PlaySound(PlaySound), + PlaySound(PlaySound), // A variant of the Message enum that holds a PlaySound struct } -#[derive(Serialize, Deserialize, Debug)] +// Define a struct called PlaySound which represents a sound to be played +#[derive(Serialize, Deserialize, Debug)] // Automatically generate code for serialization, deserialization, and debugging pub struct PlaySound { - pub timestamp: u128, - pub channels: u16, - pub sample_rate: u32, - pub sound_data: Vec, + pub timestamp: u128, // The timestamp when the sound should be played + pub channels: u16, // The number of audio channels (e.g., 2 for stereo) + pub sample_rate: u32, // The sample rate of the audio (e.g., 44100 for 44.1 kHz) + pub sound_data: Vec, // The actual sound data as a vector of 16-bit integers } diff --git a/src/sound_player.rs b/src/sound_player.rs index 4bca269..875823d 100644 --- a/src/sound_player.rs +++ b/src/sound_player.rs @@ -1,15 +1,24 @@ use rodio::{buffer::SamplesBuffer, OutputStream, Sink}; +// Define a struct for the SoundPlayer pub struct SoundPlayer; impl SoundPlayer { + // Function to play sound given channels, sample rate, and sound data pub fn play_sound(channels: u16, sample_rate: u32, sound_data: Vec) { - // let data = Cursor::new(sound_data); + // Create a SamplesBuffer from the provided sound data let source = SamplesBuffer::new(channels, sample_rate, sound_data); + // Try to get the default output stream and handle let (_stream, stream_handle) = OutputStream::try_default().unwrap(); + + // Create a new Sink (audio output) using the stream handle let sink = Sink::try_new(&stream_handle).unwrap(); + + // Append the sound source to the sink sink.append(source); + + // Block the current thread until the sound finishes playing sink.sleep_until_end(); } } diff --git a/src/sound_scheduler.rs b/src/sound_scheduler.rs index 05f29b0..29f2620 100644 --- a/src/sound_scheduler.rs +++ b/src/sound_scheduler.rs @@ -3,33 +3,51 @@ use crate::sound_player::SoundPlayer; use std::thread::{self, sleep}; use std::time::{Duration, SystemTime, UNIX_EPOCH}; +// Define a struct for the SoundScheduler pub struct SoundScheduler; impl SoundScheduler { + // Function to handle scheduled sound playback pub fn handle_scheduled_sound(play_msg: PlaySound) { + // Spawn a new thread to handle the sound playback thread::spawn(move || { + // Destructure the PlaySound message let PlaySound { channels, sample_rate, sound_data, timestamp, } = play_msg; + + // Print the length of the sound data and the scheduled timestamp println!( "Scheduled sound: len={:?} at {:?}", sound_data.len(), timestamp ); + + // Get the current time in microseconds since UNIX_EPOCH let now_micros = SystemTime::now() .duration_since(UNIX_EPOCH) .expect("Could not get time") .as_micros(); + + // Calculate the wait time in microseconds let wait = timestamp as i128 - now_micros as i128; + + // If the wait time is less than or equal to zero, it's too late to play the sound if wait <= 0 { println!("Too late to play sound"); return; } + + // Sleep for the calculated wait time sleep(Duration::from_micros(wait as u64)); + + // Print a message indicating that the sound is being played println!("Playing sound..."); + + // Play the sound using the SoundPlayer SoundPlayer::play_sound(channels, sample_rate, sound_data); }); } diff --git a/src/ui_app.rs b/src/ui_app.rs index eee3b9c..0d28a20 100644 --- a/src/ui_app.rs +++ b/src/ui_app.rs @@ -1,33 +1,43 @@ -use crate::dispatcher::{Dispatcher, NetworkDispatcher}; -use eframe::egui; -use rfd::FileDialog; -use std::path::PathBuf; +use crate::dispatcher::{Dispatcher, NetworkDispatcher}; // Importing necessary modules and traits +use eframe::egui; // Importing eframe's egui module for UI +use rfd::FileDialog; // Importing rfd's FileDialog for file picking +use std::path::PathBuf; // Importing PathBuf for handling file paths +// Struct representing the UI application pub struct UIApp { - picked_file: Option, - addresses: Vec, + picked_file: Option, // Optional field to store the picked file path + addresses: Vec, // Vector to store target device addresses } +// Implementing the Default trait for UIApp impl Default for UIApp { fn default() -> Self { Self { - picked_file: None, - addresses: Vec::new(), + picked_file: None, // Initially no file is picked + addresses: Vec::new(), // Initially no addresses are added } } } +// Implementing the eframe::App trait for UIApp impl eframe::App for UIApp { fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { + // If no addresses are present, add a default address if self.addresses.is_empty() { self.addresses.push("localhost:3000".to_owned()); } + + // Creating the central panel for the UI egui::CentralPanel::default().show(ctx, |ui| { - let mut to_remove = Vec::new(); + let mut to_remove = Vec::new(); // Vector to store indices of addresses to be removed + + // Iterating over addresses to create UI elements for each for (i, address) in self.addresses.iter_mut().enumerate() { ui.horizontal(|ui| { - ui.label("Enter IP of target device: "); - ui.text_edit_singleline(address); + ui.label("Enter IP of target device: "); // Label for the address input + ui.text_edit_singleline(address); // Text input for the address + + // Button to play sound on the target device if ui.button("Play sound").clicked() { if let Some(path) = &self.picked_file { NetworkDispatcher::handle_dispatch( @@ -36,21 +46,29 @@ impl eframe::App for UIApp { ); } } + + // Button to remove the address if ui.button("Remove").clicked() { to_remove.push(i); } }); } + + // Removing addresses that were marked for removal for i in to_remove.iter().rev() { self.addresses.remove(*i); } + + // Button to add a new address if ui.button("Add address").clicked() { self.addresses.push("localhost:3000".to_owned()); } - ui.add_space(25.0); - ui.separator(); - ui.add_space(25.0); + ui.add_space(25.0); // Adding space in the UI + ui.separator(); // Adding a separator line + ui.add_space(25.0); // Adding more space + + // Button to pick a file if ui.button("Pick file").clicked() { let file = FileDialog::new().pick_file(); if !file.is_none() { @@ -58,12 +76,14 @@ impl eframe::App for UIApp { } } + // Displaying the picked file path ui.label(format!("Picked file: {:?}", self.picked_file)); - ui.add_space(25.0); - ui.separator(); - ui.add_space(25.0); + ui.add_space(25.0); // Adding space in the UI + ui.separator(); // Adding a separator line + ui.add_space(25.0); // Adding more space + // Button to play sound on all target devices if ui.button("Play sound").clicked() { if let Some(path) = &self.picked_file { NetworkDispatcher::handle_dispatch(