Compare commits
1 commit
master
...
addComment
| Author | SHA1 | Date | |
|---|---|---|---|
| 6ed2c01c05 |
6 changed files with 62 additions and 47 deletions
17
README.md
17
README.md
|
|
@ -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
|
||||||
|
|
||||||

|

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

|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -41,10 +41,11 @@ impl Listener for NetworkListener {
|
||||||
fn listen(port: u16) -> Result<()> {
|
fn listen(port: u16) -> Result<()> {
|
||||||
// Create the bind address string
|
// 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
|
// 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
|
// If there's an error, return a BindError
|
||||||
return Err(BindError {
|
return Err(BindError {
|
||||||
|
|
@ -60,14 +61,19 @@ impl Listener for NetworkListener {
|
||||||
loop {
|
loop {
|
||||||
// Buffer to store incoming data
|
// Buffer to store incoming data
|
||||||
let mut data = [0; 1_048_576];
|
let mut data = [0; 1_048_576];
|
||||||
|
|
||||||
// Receive data from the socket
|
// 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
|
// 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
|
// 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
|
// 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
|
// Handle the deserialized message
|
||||||
NetworkListener::handle_message(msg);
|
NetworkListener::handle_message(msg);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,8 +20,10 @@ pub struct NetworkDispatcher;
|
||||||
impl NetworkDispatcher {
|
impl NetworkDispatcher {
|
||||||
// Load a sound file and return a Decoder
|
// 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
|
// Open the file
|
||||||
Decoder::new(file).unwrap() // Decode the file
|
let file = BufReader::new(File::open(path).unwrap());
|
||||||
|
// Decode the file
|
||||||
|
Decoder::new(file).unwrap()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -39,23 +41,28 @@ impl Dispatcher for NetworkDispatcher {
|
||||||
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
|
// Length of each chunk to send
|
||||||
|
let chunk_len = Duration::from_millis(200);
|
||||||
|
|
||||||
let mut offset = Duration::from_secs(0); // Initial offset
|
// Initial offset
|
||||||
|
let mut offset = Duration::from_secs(0);
|
||||||
|
|
||||||
// Iterate over each address
|
// Iterate over each address
|
||||||
for addr in addrs {
|
for addr in addrs {
|
||||||
|
// Skip empty addresses
|
||||||
if addr.is_empty() {
|
if addr.is_empty() {
|
||||||
continue; // Skip empty addresses
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let source = source.clone(); // Clone the source for each thread
|
// Clone the source for each thread
|
||||||
|
let source = source.clone();
|
||||||
|
|
||||||
// Spawn a new thread for each address
|
// Spawn a new thread for each address
|
||||||
thread::spawn(move || {
|
thread::spawn(move || {
|
||||||
// Bind to a local UDP socket
|
// 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
|
// Connect to the address
|
||||||
|
sock.connect(addr.clone()).expect("Failed to connect");
|
||||||
|
|
||||||
// Loop until the entire duration is covered
|
// Loop until the entire duration is covered
|
||||||
while offset < duration {
|
while offset < duration {
|
||||||
|
|
@ -66,11 +73,13 @@ impl Dispatcher for NetworkDispatcher {
|
||||||
.take_duration(chunk_len)
|
.take_duration(chunk_len)
|
||||||
.collect::<Vec<i16>>();
|
.collect::<Vec<i16>>();
|
||||||
|
|
||||||
offset += chunk_len; // Update the offset
|
// Update the offset
|
||||||
|
offset += chunk_len;
|
||||||
|
|
||||||
// Create a PlaySound message
|
// Create a PlaySound message
|
||||||
let msg = Message::PlaySound(PlaySound {
|
let msg = Message::PlaySound(PlaySound {
|
||||||
timestamp: (now + offset).as_micros(), // Timestamp for synchronization
|
// 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,
|
||||||
|
|
|
||||||
26
src/main.rs
26
src/main.rs
|
|
@ -1,4 +1,5 @@
|
||||||
#![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 mode
|
||||||
|
|
||||||
// Import necessary modules and crates
|
// Import necessary modules and crates
|
||||||
use client_handler::{Listener, NetworkListener};
|
use client_handler::{Listener, NetworkListener};
|
||||||
|
|
@ -18,7 +19,8 @@ mod ui_app;
|
||||||
fn main() -> eframe::Result {
|
fn main() -> eframe::Result {
|
||||||
// Define options for the eframe application
|
// 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
|
// Set initial window size
|
||||||
|
viewport: egui::ViewportBuilder::default().with_inner_size([700.0, 400.0]),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -29,13 +31,15 @@ fn main() -> eframe::Result {
|
||||||
match result {
|
match result {
|
||||||
Ok(_) => {}
|
Ok(_) => {}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("Failed to bind: {}", e.message); // Print error message if binding fails
|
// Print error message if binding fails
|
||||||
// Try with port 3001 if port 3000 fails
|
eprintln!("Failed to bind: {}", e.message);
|
||||||
|
// Try with port 3001 if port 3000 fails
|
||||||
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
|
// Print error message if binding fails again
|
||||||
|
eprintln!("Failed to bind again: {}", e.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -44,13 +48,17 @@ fn main() -> eframe::Result {
|
||||||
|
|
||||||
// Run the eframe application
|
// Run the eframe application
|
||||||
eframe::run_native(
|
eframe::run_native(
|
||||||
"My egui App", // Application title
|
// Application title
|
||||||
options, // Application options
|
"My egui App",
|
||||||
|
// 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
|
// Install image loaders for egui context
|
||||||
|
egui_extras::install_image_loaders(&cc.egui_ctx);
|
||||||
|
|
||||||
Ok(Box::<UIApp>::default()) // Initialize the UI application
|
// Initialize the UI application
|
||||||
|
Ok(Box::<UIApp>::default())
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,25 @@
|
||||||
use serde::{Deserialize, Serialize}; // Importing necessary traits from serde for serialization and deserialization
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
// Importing necessary traits from serde for serialization and deserialization
|
||||||
|
|
||||||
// Define an enum called Message which can hold different types of messages
|
// 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)]
|
||||||
|
// 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),
|
||||||
|
// A variant of the Message enum that holds a PlaySound struct
|
||||||
}
|
}
|
||||||
|
|
||||||
// Define a struct called PlaySound which represents a sound to be played
|
// 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)]
|
||||||
|
// 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)
|
// The timestamp when the sound should be played
|
||||||
pub sample_rate: u32, // The sample rate of the audio (e.g., 44100 for 44.1 kHz)
|
pub channels: u16,
|
||||||
pub sound_data: Vec<i16>, // The actual sound data as a vector of 16-bit integers
|
// 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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -77,11 +77,7 @@ impl eframe::App for UIApp {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Displaying the picked file path
|
// Displaying the picked file path
|
||||||
let picked_file_label = match &self.picked_file {
|
ui.label(format!("Picked file: {:?}", 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); // Adding space in the UI
|
||||||
ui.separator(); // Adding a separator line
|
ui.separator(); // Adding a separator line
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue