Compare commits
No commits in common. "master" and "traits" have entirely different histories.
8 changed files with 49 additions and 158 deletions
17
README.md
17
README.md
|
|
@ -1,26 +1,13 @@
|
|||
# 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
|
||||
|
||||

|
||||
|
||||
|
||||
## Actual structure
|
||||
|
||||

|
||||
|
||||
!
|
||||
|
|
|
|||
|
|
@ -2,73 +2,51 @@ 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
|
||||
pub trait Listener : ClientHandler {
|
||||
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, // If successful, use the socket
|
||||
Ok(s) => s,
|
||||
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,77 +8,62 @@ 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()); // Open the file
|
||||
Decoder::new(file).unwrap() // Decode the file
|
||||
let file = BufReader::new(File::open(path).unwrap());
|
||||
Decoder::new(file).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
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); // 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 {
|
||||
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 || {
|
||||
// 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"); // Connect to the address
|
||||
sock.connect(addr.clone()).expect("Failed to connect");
|
||||
|
||||
// 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; // Update the offset
|
||||
offset += chunk_len;
|
||||
|
||||
// Create a PlaySound message
|
||||
let msg = Message::PlaySound(PlaySound {
|
||||
timestamp: (now + offset).as_micros(), // Timestamp for synchronization
|
||||
timestamp: (now + offset).as_micros(),
|
||||
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,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 std::thread;
|
||||
use ui_app::UIApp;
|
||||
|
||||
use eframe::egui;
|
||||
|
||||
// Declare the modules used in this project
|
||||
mod client_handler;
|
||||
mod dispatcher;
|
||||
mod protocol;
|
||||
|
|
@ -16,41 +14,37 @@ 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]), // Set initial window size
|
||||
viewport: egui::ViewportBuilder::default().with_inner_size([700.0, 400.0]),
|
||||
..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); // Print error message if binding fails
|
||||
// Try with port 3001 if port 3000 fails
|
||||
eprintln!("Failed to bind: {}", e.message);
|
||||
// Try with port 3001
|
||||
let result = NetworkListener::listen(3001);
|
||||
match result {
|
||||
Ok(_) => {}
|
||||
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(
|
||||
"My egui App", // Application title
|
||||
options, // Application options
|
||||
"My egui App",
|
||||
options,
|
||||
Box::new(|cc| {
|
||||
// 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())
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)] // Automatically generate code for serialization, deserialization, and debugging
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
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)] // Automatically generate code for serialization, deserialization, and debugging
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct PlaySound {
|
||||
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
|
||||
pub timestamp: u128,
|
||||
pub channels: u16,
|
||||
pub sample_rate: u32,
|
||||
pub sound_data: Vec<i16>,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,24 +1,15 @@
|
|||
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>) {
|
||||
// Create a SamplesBuffer from the provided sound data
|
||||
// let data = Cursor::new(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,51 +3,33 @@ 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,43 +1,33 @@
|
|||
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
|
||||
use crate::dispatcher::{Dispatcher, NetworkDispatcher};
|
||||
use eframe::egui;
|
||||
use rfd::FileDialog;
|
||||
use std::path::PathBuf;
|
||||
|
||||
// Struct representing the UI application
|
||||
pub struct UIApp {
|
||||
picked_file: Option<PathBuf>, // Optional field to store the picked file path
|
||||
addresses: Vec<String>, // Vector to store target device addresses
|
||||
picked_file: Option<PathBuf>,
|
||||
addresses: Vec<String>,
|
||||
}
|
||||
|
||||
// Implementing the Default trait for UIApp
|
||||
impl Default for UIApp {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
picked_file: None, // Initially no file is picked
|
||||
addresses: Vec::new(), // Initially no addresses are added
|
||||
picked_file: None,
|
||||
addresses: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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(); // Vector to store indices of addresses to be removed
|
||||
|
||||
// Iterating over addresses to create UI elements for each
|
||||
let mut to_remove = Vec::new();
|
||||
for (i, address) in self.addresses.iter_mut().enumerate() {
|
||||
ui.horizontal(|ui| {
|
||||
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
|
||||
ui.label("Enter IP of target device: ");
|
||||
ui.text_edit_singleline(address);
|
||||
if ui.button("Play sound").clicked() {
|
||||
if let Some(path) = &self.picked_file {
|
||||
NetworkDispatcher::handle_dispatch(
|
||||
|
|
@ -46,29 +36,21 @@ 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() {
|
||||
|
|
@ -76,18 +58,12 @@ impl eframe::App for UIApp {
|
|||
}
|
||||
}
|
||||
|
||||
// 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.label(format!("Picked file: {:?}", self.picked_file));
|
||||
|
||||
ui.add_space(25.0); // Adding space in the UI
|
||||
ui.separator(); // Adding a separator line
|
||||
ui.add_space(25.0); // Adding more space
|
||||
ui.add_space(25.0);
|
||||
ui.separator();
|
||||
ui.add_space(25.0);
|
||||
|
||||
// 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