Compare commits

..

No commits in common. "master" and "traits" have entirely different histories.

8 changed files with 49 additions and 158 deletions

View file

@ -1,26 +1,13 @@
# syncaud # syncaud
## Requirements
- [At least one custom exception class that extends Exception should be written.](./src/client_handler.rs#L6) (The struct `BindError` is a custom exception, which defines the error type for binding errors.)
- [At least one inheritance implementation must be included.](./src/client_handler.rs#L37) (The `NetworkListener` implements the `ClientHandler` and `Listener` trait.)
- [At least one custom (non-Spring Boot interface) interface should be created and used.](./src/client_handler.rs#14) (The `ClientHandler` trait is like an interface in Rust.)
## Design Pattern
In our code we adhered to the Strategy Design Pattern as described [here](https://rust-unofficial.github.io/patterns/patterns/behavioural/strategy.html). For example, the ClientHandler trait acts as the strategy interface and the NetworkListener implements the strategy via the trait. This means that in the future, this strategy could be replaced to allow for a different communication medium, e.g. Serial.
## Featurelist ## Featurelist
- The user interface should be interactive and usable by any person. - The user interface should be interactive and usable by any person.
- Sounds are played synchronously across multiple devices. - Sounds are played synchronously across multiple devices.
- Any mp3 file can be uploaded by the user to play. - Any mp3 file can be uploaded by the user to play.
## Planned structure ## Planned structure
![syncaud(1)](https://github.com/user-attachments/assets/264a3af8-717f-43ed-9c51-2d1da54b8c8a) ![syncaud(1)](https://github.com/user-attachments/assets/264a3af8-717f-43ed-9c51-2d1da54b8c8a)
## Actual structure ## Actual structure
!![Syncaud](https://github.com/user-attachments/assets/97cc57de-a53d-48e3-b09e-8057bba05bac)
![Syncaud(2)](https://github.com/user-attachments/assets/0b610dec-5589-40e5-82c3-34384649bd61)

View file

@ -2,73 +2,51 @@ use crate::protocol::Message;
use crate::sound_scheduler::SoundScheduler; use crate::sound_scheduler::SoundScheduler;
use std::net::UdpSocket; use std::net::UdpSocket;
// Custom error type for binding errors
pub struct BindError { pub struct BindError {
pub message: String, pub message: String,
} }
// Type alias for Result with BindError as the error type
type Result<T> = std::result::Result<T, BindError>; type Result<T> = std::result::Result<T, BindError>;
// Trait for handling client messages
pub trait ClientHandler { pub trait ClientHandler {
// Function to handle incoming messages
fn handle_message(msg: Message) { fn handle_message(msg: Message) {
match msg { match msg {
// Match on the message type and handle accordingly
Message::PlaySound(play_sound) => { Message::PlaySound(play_sound) => {
// Delegate to SoundScheduler to handle the play sound message
SoundScheduler::handle_scheduled_sound(play_sound); SoundScheduler::handle_scheduled_sound(play_sound);
} }
} }
} }
} }
// Trait for listening to incoming messages pub trait Listener : ClientHandler {
pub trait Listener: ClientHandler {
// Function to start listening on a given port
fn listen(port: u16) -> Result<()>; fn listen(port: u16) -> Result<()>;
} }
// Struct representing a network listener
pub struct NetworkListener; pub struct NetworkListener;
// Implement ClientHandler for NetworkListener
impl ClientHandler for NetworkListener {} impl ClientHandler for NetworkListener {}
// Implement Listener for NetworkListener
impl Listener for NetworkListener { impl Listener for NetworkListener {
fn listen(port: u16) -> Result<()> { fn listen(port: u16) -> Result<()> {
// Create the bind address string
let bind_addr = format!("0.0.0.0:{}", port); 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 = UdpSocket::bind(bind_addr);
let socket = match socket { let socket = match socket {
Ok(s) => s, // If successful, use the socket Ok(s) => s,
Err(e) => { Err(e) => {
// If there's an error, return a BindError
return Err(BindError { return Err(BindError {
message: e.to_string(), message: e.to_string(),
}); });
} }
}; };
// Print the local address the socket is bound to
println!("Listening on {}", socket.local_addr().unwrap()); println!("Listening on {}", socket.local_addr().unwrap());
// Enter an infinite loop to listen for incoming messages
loop { loop {
// Buffer to store incoming data
let mut data = [0; 1_048_576]; 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"); 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); println!("Received {} bytes from {}", amt, src);
// Slice the buffer to the actual amount of data received
let data = &mut data[..amt]; let data = &mut data[..amt];
// Deserialize the data into a Message
let msg = bincode::deserialize::<Message>(data).expect("Failed to deserialize"); let msg = bincode::deserialize::<Message>(data).expect("Failed to deserialize");
// Handle the deserialized message
NetworkListener::handle_message(msg); NetworkListener::handle_message(msg);
} }
} }

View file

@ -8,77 +8,62 @@ use rodio::{Decoder, Source};
use crate::protocol::{Message, PlaySound}; use crate::protocol::{Message, PlaySound};
// Define a trait for dispatching tasks
pub trait Dispatcher { pub trait Dispatcher {
// Method to handle dispatching tasks to multiple addresses
fn handle_dispatch(addrs: Vec<String>, sound_path: String); fn handle_dispatch(addrs: Vec<String>, sound_path: String);
} }
// Struct for network-based dispatching
pub struct NetworkDispatcher; pub struct NetworkDispatcher;
impl NetworkDispatcher { impl NetworkDispatcher {
// Load a sound file and return a Decoder
fn load_sound(path: String) -> Decoder<BufReader<File>> { fn load_sound(path: String) -> Decoder<BufReader<File>> {
let file = BufReader::new(File::open(path).unwrap()); // Open the file let file = BufReader::new(File::open(path).unwrap());
Decoder::new(file).unwrap() // Decode the file Decoder::new(file).unwrap()
} }
} }
impl Dispatcher for NetworkDispatcher { impl Dispatcher for NetworkDispatcher {
fn handle_dispatch(addrs: Vec<String>, sound_path: String) { fn handle_dispatch(addrs: Vec<String>, sound_path: String) {
// Get the current system time since UNIX_EPOCH
let now = SystemTime::now() let now = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH) .duration_since(SystemTime::UNIX_EPOCH)
.unwrap(); .unwrap();
// Load and buffer the sound source
let source = NetworkDispatcher::load_sound(sound_path).buffered(); let source = NetworkDispatcher::load_sound(sound_path).buffered();
// Get sound properties
let channels = source.channels(); let channels = source.channels();
let sample_rate = source.sample_rate(); let sample_rate = source.sample_rate();
let duration = source.total_duration().unwrap(); let duration = source.total_duration().unwrap();
let chunk_len = Duration::from_millis(200); // Length of each chunk to send let chunk_len = Duration::from_millis(200);
let mut offset = Duration::from_secs(0); // Initial offset let mut offset = Duration::from_secs(0);
// Iterate over each address
for addr in addrs { for addr in addrs {
if addr.is_empty() { if addr.is_empty() {
continue; // Skip empty addresses continue;
} }
let source = source.clone(); // Clone the source for each thread let source = source.clone();
// Spawn a new thread for each address
thread::spawn(move || { thread::spawn(move || {
// Bind to a local UDP socket
let sock = UdpSocket::bind("0.0.0.0:0").expect("Failed to bind"); let sock = UdpSocket::bind("0.0.0.0:0").expect("Failed to bind");
sock.connect(addr.clone()).expect("Failed to connect"); // Connect to the address sock.connect(addr.clone()).expect("Failed to connect");
// Loop until the entire duration is covered
while offset < duration { while offset < duration {
// Collect sound data for the current chunk
let sound_data = source let sound_data = source
.clone() .clone()
.skip_duration(offset) .skip_duration(offset)
.take_duration(chunk_len) .take_duration(chunk_len)
.collect::<Vec<i16>>(); .collect::<Vec<i16>>();
offset += chunk_len; // Update the offset offset += chunk_len;
// Create a PlaySound message
let msg = Message::PlaySound(PlaySound { let msg = Message::PlaySound(PlaySound {
timestamp: (now + offset).as_micros(), // Timestamp for synchronization timestamp: (now + offset).as_micros(),
channels: channels, channels: channels,
sample_rate: sample_rate, sample_rate: sample_rate,
sound_data: sound_data, sound_data: sound_data,
}); });
// Serialize the message
let buf = bincode::serialize(&msg).expect("Failed to serialize"); let buf = bincode::serialize(&msg).expect("Failed to serialize");
// Send the serialized message via UDP
sock.send(&buf).expect("Failed to send"); sock.send(&buf).expect("Failed to send");
} }
}); });

View file

@ -1,13 +1,11 @@
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // Hide console window on Windows in release mode #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release
// Import necessary modules and crates
use client_handler::{Listener, NetworkListener}; use client_handler::{Listener, NetworkListener};
use std::thread; use std::thread;
use ui_app::UIApp; use ui_app::UIApp;
use eframe::egui; use eframe::egui;
// Declare the modules used in this project
mod client_handler; mod client_handler;
mod dispatcher; mod dispatcher;
mod protocol; mod protocol;
@ -16,41 +14,37 @@ mod sound_scheduler;
mod ui_app; mod ui_app;
fn main() -> eframe::Result { fn main() -> eframe::Result {
// Define options for the eframe application
let options = eframe::NativeOptions { let options = eframe::NativeOptions {
viewport: egui::ViewportBuilder::default().with_inner_size([700.0, 400.0]), // Set initial window size viewport: egui::ViewportBuilder::default().with_inner_size([700.0, 400.0]),
..Default::default() ..Default::default()
}; };
// Spawn a new thread to handle network listening
thread::spawn(move || { thread::spawn(move || {
// Attempt to listen on port 3000
let result = NetworkListener::listen(3000); let result = NetworkListener::listen(3000);
match result { match result {
Ok(_) => {} Ok(_) => {}
Err(e) => { Err(e) => {
eprintln!("Failed to bind: {}", e.message); // Print error message if binding fails eprintln!("Failed to bind: {}", e.message);
// Try with port 3001 if port 3000 fails // Try with port 3001
let result = NetworkListener::listen(3001); let result = NetworkListener::listen(3001);
match result { match result {
Ok(_) => {} Ok(_) => {}
Err(e) => { Err(e) => {
eprintln!("Failed to bind again: {}", e.message); // Print error message if binding fails again eprintln!("Failed to bind again: {}", e.message);
} }
} }
} }
} }
}); });
// Run the eframe application
eframe::run_native( eframe::run_native(
"My egui App", // Application title "My egui App",
options, // Application options options,
Box::new(|cc| { Box::new(|cc| {
// This gives us image support: // This gives us image support:
egui_extras::install_image_loaders(&cc.egui_ctx); // Install image loaders for egui context egui_extras::install_image_loaders(&cc.egui_ctx);
Ok(Box::<UIApp>::default()) // Initialize the UI application Ok(Box::<UIApp>::default())
}), }),
) )
} }

View file

@ -1,16 +1,14 @@
use serde::{Deserialize, Serialize}; // Importing necessary traits from serde for serialization and deserialization use serde::{Deserialize, Serialize};
// Define an enum called Message which can hold different types of messages #[derive(Serialize, Deserialize, Debug)]
#[derive(Serialize, Deserialize, Debug)] // Automatically generate code for serialization, deserialization, and debugging
pub enum Message { pub enum Message {
PlaySound(PlaySound), // A variant of the Message enum that holds a PlaySound struct PlaySound(PlaySound),
} }
// Define a struct called PlaySound which represents a sound to be played #[derive(Serialize, Deserialize, Debug)]
#[derive(Serialize, Deserialize, Debug)] // Automatically generate code for serialization, deserialization, and debugging
pub struct PlaySound { pub struct PlaySound {
pub timestamp: u128, // The timestamp when the sound should be played pub timestamp: u128,
pub channels: u16, // The number of audio channels (e.g., 2 for stereo) pub channels: u16,
pub sample_rate: u32, // The sample rate of the audio (e.g., 44100 for 44.1 kHz) pub sample_rate: u32,
pub sound_data: Vec<i16>, // The actual sound data as a vector of 16-bit integers pub sound_data: Vec<i16>,
} }

View file

@ -1,24 +1,15 @@
use rodio::{buffer::SamplesBuffer, OutputStream, Sink}; use rodio::{buffer::SamplesBuffer, OutputStream, Sink};
// Define a struct for the SoundPlayer
pub struct SoundPlayer; pub struct SoundPlayer;
impl 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>) { pub fn play_sound(channels: u16, sample_rate: u32, sound_data: Vec<i16>) {
// Create a SamplesBuffer from the provided sound data // let data = Cursor::new(sound_data);
let source = SamplesBuffer::new(channels, sample_rate, 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(); 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(); let sink = Sink::try_new(&stream_handle).unwrap();
// Append the sound source to the sink
sink.append(source); sink.append(source);
// Block the current thread until the sound finishes playing
sink.sleep_until_end(); sink.sleep_until_end();
} }
} }

View file

@ -3,51 +3,33 @@ use crate::sound_player::SoundPlayer;
use std::thread::{self, sleep}; use std::thread::{self, sleep};
use std::time::{Duration, SystemTime, UNIX_EPOCH}; use std::time::{Duration, SystemTime, UNIX_EPOCH};
// Define a struct for the SoundScheduler
pub struct SoundScheduler; pub struct SoundScheduler;
impl SoundScheduler { impl SoundScheduler {
// Function to handle scheduled sound playback
pub fn handle_scheduled_sound(play_msg: PlaySound) { pub fn handle_scheduled_sound(play_msg: PlaySound) {
// Spawn a new thread to handle the sound playback
thread::spawn(move || { thread::spawn(move || {
// Destructure the PlaySound message
let PlaySound { let PlaySound {
channels, channels,
sample_rate, sample_rate,
sound_data, sound_data,
timestamp, timestamp,
} = play_msg; } = play_msg;
// Print the length of the sound data and the scheduled timestamp
println!( println!(
"Scheduled sound: len={:?} at {:?}", "Scheduled sound: len={:?} at {:?}",
sound_data.len(), sound_data.len(),
timestamp timestamp
); );
// Get the current time in microseconds since UNIX_EPOCH
let now_micros = SystemTime::now() let now_micros = SystemTime::now()
.duration_since(UNIX_EPOCH) .duration_since(UNIX_EPOCH)
.expect("Could not get time") .expect("Could not get time")
.as_micros(); .as_micros();
// Calculate the wait time in microseconds
let wait = timestamp as i128 - now_micros as i128; 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 { if wait <= 0 {
println!("Too late to play sound"); println!("Too late to play sound");
return; return;
} }
// Sleep for the calculated wait time
sleep(Duration::from_micros(wait as u64)); sleep(Duration::from_micros(wait as u64));
// Print a message indicating that the sound is being played
println!("Playing sound..."); println!("Playing sound...");
// Play the sound using the SoundPlayer
SoundPlayer::play_sound(channels, sample_rate, sound_data); SoundPlayer::play_sound(channels, sample_rate, sound_data);
}); });
} }

View file

@ -1,43 +1,33 @@
use crate::dispatcher::{Dispatcher, NetworkDispatcher}; // Importing necessary modules and traits use crate::dispatcher::{Dispatcher, NetworkDispatcher};
use eframe::egui; // Importing eframe's egui module for UI use eframe::egui;
use rfd::FileDialog; // Importing rfd's FileDialog for file picking use rfd::FileDialog;
use std::path::PathBuf; // Importing PathBuf for handling file paths use std::path::PathBuf;
// Struct representing the UI application
pub struct UIApp { pub struct UIApp {
picked_file: Option<PathBuf>, // Optional field to store the picked file path picked_file: Option<PathBuf>,
addresses: Vec<String>, // Vector to store target device addresses addresses: Vec<String>,
} }
// Implementing the Default trait for UIApp
impl Default for UIApp { impl Default for UIApp {
fn default() -> Self { fn default() -> Self {
Self { Self {
picked_file: None, // Initially no file is picked picked_file: None,
addresses: Vec::new(), // Initially no addresses are added addresses: Vec::new(),
} }
} }
} }
// Implementing the eframe::App trait for UIApp
impl eframe::App for UIApp { impl eframe::App for UIApp {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { 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() { if self.addresses.is_empty() {
self.addresses.push("localhost:3000".to_owned()); self.addresses.push("localhost:3000".to_owned());
} }
// Creating the central panel for the UI
egui::CentralPanel::default().show(ctx, |ui| { egui::CentralPanel::default().show(ctx, |ui| {
let mut to_remove = Vec::new(); // Vector to store indices of addresses to be removed let mut to_remove = Vec::new();
// Iterating over addresses to create UI elements for each
for (i, address) in self.addresses.iter_mut().enumerate() { for (i, address) in self.addresses.iter_mut().enumerate() {
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.label("Enter IP of target device: "); // Label for the address input ui.label("Enter IP of target device: ");
ui.text_edit_singleline(address); // Text input for the address ui.text_edit_singleline(address);
// Button to play sound on the target device
if ui.button("Play sound").clicked() { if ui.button("Play sound").clicked() {
if let Some(path) = &self.picked_file { if let Some(path) = &self.picked_file {
NetworkDispatcher::handle_dispatch( NetworkDispatcher::handle_dispatch(
@ -46,29 +36,21 @@ impl eframe::App for UIApp {
); );
} }
} }
// Button to remove the address
if ui.button("Remove").clicked() { if ui.button("Remove").clicked() {
to_remove.push(i); to_remove.push(i);
} }
}); });
} }
// Removing addresses that were marked for removal
for i in to_remove.iter().rev() { for i in to_remove.iter().rev() {
self.addresses.remove(*i); self.addresses.remove(*i);
} }
// Button to add a new address
if ui.button("Add address").clicked() { if ui.button("Add address").clicked() {
self.addresses.push("localhost:3000".to_owned()); 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() { if ui.button("Pick file").clicked() {
let file = FileDialog::new().pick_file(); let file = FileDialog::new().pick_file();
if !file.is_none() { if !file.is_none() {
@ -76,18 +58,12 @@ impl eframe::App for UIApp {
} }
} }
// Displaying the picked file path ui.label(format!("Picked file: {:?}", self.picked_file));
let picked_file_label = match &self.picked_file {
Some(path) => format!("Picked file: {:?}", path),
None => "No file picked".to_owned(),
};
ui.label(picked_file_label);
ui.add_space(25.0); // Adding space in the UI ui.add_space(25.0);
ui.separator(); // Adding a separator line ui.separator();
ui.add_space(25.0); // Adding more space ui.add_space(25.0);
// Button to play sound on all target devices
if ui.button("Play sound").clicked() { if ui.button("Play sound").clicked() {
if let Some(path) = &self.picked_file { if let Some(path) = &self.picked_file {
NetworkDispatcher::handle_dispatch( NetworkDispatcher::handle_dispatch(