Compare commits

...

7 commits

Author SHA1 Message Date
Silvio Brändle
ffb34753ce
Add design pattern 2025-01-27 23:59:42 +01:00
Cédric Ackermann
594a530852
Update actual structure 2025-01-27 18:20:41 +01:00
533bc57232
update README.md 2025-01-27 14:37:32 +01:00
Silvio Braendle
409bcfb768
Improve file pick label 2025-01-27 13:39:51 +01:00
Silvio Brändle
ccb46ce1ea
Merge pull request #2 from silvio2402/addComments
add some comments
2025-01-27 12:36:25 +01:00
044d3911dd
add some comments 2025-01-27 12:35:41 +01:00
Cédric Ackermann
9e3b573406
Merge pull request #1 from silvio2402/traits
Use traits and implement them
2025-01-27 12:14:08 +01:00
8 changed files with 158 additions and 49 deletions

View file

@ -1,13 +1,26 @@
# 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
- The user interface should be interactive and usable by any person.
- Sounds are played synchronously across multiple devices.
- Any mp3 file can be uploaded by the user to play.
## Planned structure
![syncaud(1)](https://github.com/user-attachments/assets/264a3af8-717f-43ed-9c51-2d1da54b8c8a)
## 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,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);
}
}

View file

@ -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");
}
});

View file

@ -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
}),
)
}

View file

@ -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
}

View file

@ -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();
}
}

View file

@ -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);
});
}

View file

@ -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,18 @@ impl eframe::App for UIApp {
}
}
ui.label(format!("Picked file: {:?}", self.picked_file));
// Displaying the picked file path
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);
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(