3
\$\begingroup\$

for learning purposes (and I found it kinda useful), I built a very basic command-line based metronome in rust. It's the first actual smaller project I built in rust.

It consists of 4 source files. What I'm most concerned about:

  • Audio Handling: At the moment i'm loading the audio files on the fly, every time I want to play a sound, which is redundant, obviously. Is there a way to embed the audio files into the executable? Or alternatively at least preload the files at the start of the app. I already tried this, but I had problems with the reusage of the buffer.
  • The metronome is supposed to be controllable from the main thread. Is that a sensible to implement that functionality? (Currently, only Stopping is implemented, as I first wanted to get the basic Threading functionality down)
  • I'm mainly coming from Python and C++, so I'm afraid I'm applying patterns I'm used to from those languages.

The source code:

main.rs

mod audio_handling;
mod io;
mod metronome;

fn main() {
    audio_handling::check_audio_files();

    let metronome: metronome::Metronome = io::ask_metronome_settings();

    let worker = metronome.start();
    loop {}
}

io.rs

use crate::metronome::Metronome;
use std::io;

pub fn ask_metronome_settings() -> Metronome {
    let mut bpm = String::new();
    let mut time_signature = String::new();

    println!("Please enter the BPM for ♩ = (0-65536):");
    io::stdin().read_line(&mut bpm).expect("You failed!");

    println!("Please enter the time signature, (Format: \"x/y\"):");
    io::stdin()
        .read_line(&mut time_signature)
        .expect("You failed!");

    let bpm: u16 = match bpm.trim().parse() {
        Ok(num) => num,
        Err(_) => 0,
    };

    let numbers: Vec<u8> = time_signature
        .trim()
        .split("/")
        .map(|x| x.parse().expect("Error parsing time signature"))
        .collect();

    Metronome::new(bpm, numbers[0], numbers[1])
}

metronome.rs

use std::sync::mpsc::{channel, Sender};
use std::thread;
use std::time::Duration;
use std::time::Instant;

use crate::audio_handling::AudioSettings;
use crate::audio_handling::TicToc;

pub fn calc_beat_delta(bpm: u16, lower: u8) -> Duration {
    let quarter_note_sec: f64 = 60f64 / bpm as f64;
    let factor: f64 = 4f64 / lower as f64;

    Duration::from_secs_f64(quarter_note_sec * factor)
}

#[derive(Debug, Copy, Clone)]
pub struct TimeSignature {
    upper: u8,
    lower: u8,
}

#[derive(Debug, Copy, Clone)]
pub struct MetronomeSettings {
    bpm: u16,
    time_signature: TimeSignature,
}

#[derive(Debug)]
pub enum MetronomeControls {
    Stop,
}

#[derive(Debug, Copy, Clone)]
pub struct Metronome {
    metronome_settings: MetronomeSettings,
    current_beat: u8,
    last_time_run: Instant,
    beat_delta: Duration,
}

impl Metronome {
    pub fn new(bpm: u16, upper: u8, lower: u8) -> Metronome {
        Metronome {
            metronome_settings: MetronomeSettings {
                bpm,
                time_signature: TimeSignature { upper, lower },
            },
            current_beat: 0,
            last_time_run: Instant::now(),
            beat_delta: calc_beat_delta(bpm, lower),
        }
    }
    fn next(&mut self) {
        self.current_beat = (self.current_beat + 1) % self.metronome_settings.time_signature.upper;
        self.last_time_run = std::time::Instant::now();
    }

    fn play_beat(self) {
        let audio_settings: AudioSettings = AudioSettings::load();
        match self.current_beat {
            0 => audio_settings.play(TicToc::Tic),
            _ => audio_settings.play(TicToc::Toc),
        }
    }

    pub fn start(mut self) -> Sender<MetronomeControls> {
        println!("Metronome started!");
        let (tx, rx) = channel();

        thread::spawn(move || loop {
            self.play_beat();
            let sleep_time = self.beat_delta - (Instant::now() - self.last_time_run);

            let msg = rx.recv_timeout(sleep_time);
            match msg {
                Ok(MetronomeControls::Stop) => break,
                Err(_) => (),
            }

            self.next();
        });

        tx
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_calc_beat_delta() {
        assert_eq!(calc_beat_delta(120, 4), Duration::from_millis(500));
        assert_eq!(calc_beat_delta(119, 4), Duration::from_secs_f64(0.50420168));
        assert_eq!(calc_beat_delta(119, 8), Duration::from_secs_f64(0.25210084));
    }

    #[test]
    fn test_metronome_next() {
        let mut metronome: Metronome = Metronome::new(120, 4, 4);
        assert_eq!(metronome.current_beat, 0);

        metronome.next();
        assert_eq!(metronome.current_beat, 1);
        metronome.next();
        metronome.next();
        assert_eq!(metronome.current_beat, 3);
        metronome.next();
        assert_eq!(metronome.current_beat, 0);
    }
}

audio_handling.rs

use std::io::BufReader;
use std::path::Path;

const TIC_PATH: &str = "./tic.wav";
const TOC_PATH: &str = "./toc.wav";
const AUDIO_FILE_PATHS: [&str; 2] = [TIC_PATH, TOC_PATH];

pub fn check_audio_files() {
    for file_path in AUDIO_FILE_PATHS.iter() {
        if !Path::new(&file_path).exists() {
            panic!("Audio files missing!");
        }
    }
}

#[derive(Debug)]
pub enum TicToc {
    Tic,
    Toc,
}

pub struct AudioSettings {
    device: rodio::Device,
}

impl AudioSettings {
    pub fn load() -> AudioSettings {
        AudioSettings {
            device: rodio::default_output_device().unwrap(),
        }
    }
    pub fn play(self, what: TicToc) {
        let file_path = match what {
            TicToc::Tic => TIC_PATH,
            TicToc::Toc => TOC_PATH,
        };
        let audio_file = std::fs::File::open(file_path).unwrap();
        rodio::play_once(&self.device, BufReader::new(audio_file))
            .unwrap()
            .detach();
    }
}

If you want to compile it, the code can be found here: https://github.com/zapfdk/rust-metronome

\$\endgroup\$

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Browse other questions tagged or ask your own question.