commit
ccb46ce1ea
7 changed files with 138 additions and 46 deletions
|
|
@ -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<T> = std::result::Result<T, BindError>;
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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::<Message>(data).expect("Failed to deserialize");
|
||||
// Handle the deserialized message
|
||||
NetworkListener::handle_message(msg);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<String>, 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<BufReader<File>> {
|
||||
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<String>, 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::<Vec<i16>>();
|
||||
|
||||
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");
|
||||
}
|
||||
});
|
||||
|
|
|
|||
24
src/main.rs
24
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::<UIApp>::default())
|
||||
Ok(Box::<UIApp>::default()) // Initialize the UI application
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<i16>,
|
||||
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<i16>, // The actual sound data as a vector of 16-bit integers
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<i16>) {
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<PathBuf>,
|
||||
addresses: Vec<String>,
|
||||
picked_file: Option<PathBuf>, // Optional field to store the picked file path
|
||||
addresses: Vec<String>, // 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(
|
||||
|
|
|
|||
Loading…
Reference in a new issue