use std::cmp::Ordering; use std::time::Duration; use divrem::DivRem; use proptest_derive::Arbitrary; use tokio::process::Command; use tracing::instrument; use crate::util::{FileLock, Invoke}; use crate::{Error, ErrorKind}; lazy_static::lazy_static! { static ref SEMITONE_K: f64 = 2f64.powf(1f64 / 12f64); static ref A_4: f64 = 440f64; static ref C_0: f64 = *A_4 / SEMITONE_K.powf(9f64) / 2f64.powf(4f64); } pub const SOUND_LOCK_FILE: &'static str = "/etc/embassy/sound.lock"; struct SoundInterface { guard: Option, } impl SoundInterface { #[instrument(skip_all)] pub async fn lease() -> Result { let guard = FileLock::new(SOUND_LOCK_FILE, true).await?; Ok(SoundInterface { guard: Some(guard) }) } #[instrument(skip_all)] pub async fn close(mut self) -> Result<(), Error> { if let Some(lock) = self.guard.take() { lock.unlock().await?; } Ok(()) } #[instrument(skip_all)] pub async fn play_for_time_slice( &mut self, tempo_qpm: u16, note: &Note, time_slice: &TimeSlice, ) -> Result<(), Error> { Command::new("beep") .arg("-f") .arg(note.frequency().to_string()) .arg("-l") .arg(time_slice.to_duration(tempo_qpm).as_millis().to_string()) .invoke(ErrorKind::SoundError) .await?; Ok(()) } } pub struct Song { pub tempo_qpm: u16, pub note_sequence: Notes, } impl<'a, T> Song where T: IntoIterator, TimeSlice)> + Clone, { #[instrument(skip_all)] pub async fn play(&self) -> Result<(), Error> { let mut sound = SoundInterface::lease().await?; for (note, slice) in self.note_sequence.clone() { match note { None => tokio::time::sleep(slice.to_duration(self.tempo_qpm)).await, Some(n) => { sound .play_for_time_slice(self.tempo_qpm, &n, &slice) .await? } }; } sound.close().await?; Ok(()) } } impl Drop for SoundInterface { fn drop(&mut self) { let guard = self.guard.take(); tokio::spawn(async move { if let Some(guard) = guard { if let Err(e) = guard.unlock().await { tracing::error!("Failed to drop Sound Interface File Lock: {}", e); tracing::debug!("{:?}", e); } } }); } } #[derive(Debug, Clone, Copy, PartialEq, Eq, Arbitrary)] pub struct Note { pub semitone: Semitone, pub octave: i8, } impl Note { pub fn frequency(&self) -> f64 { SEMITONE_K.powf((self.semitone as isize) as f64) * (*C_0) * (2f64.powf(self.octave as f64)) } } impl PartialOrd for Note { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } impl Ord for Note { fn cmp(&self, other: &Self) -> Ordering { if self.octave == other.octave { self.semitone.cmp(&other.semitone) } else { self.octave.cmp(&other.octave) } } } #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Arbitrary)] pub enum Semitone { C = 0, Db = 1, D = 2, Eb = 3, E = 4, F = 5, Gb = 6, G = 7, Ab = 8, A = 9, Bb = 10, B = 11, } impl Semitone { pub fn rotate(&self, n: isize) -> Semitone { let temp = (*self as isize) + n; match temp.rem_euclid(12) { 0 => Semitone::C, 1 => Semitone::Db, 2 => Semitone::D, 3 => Semitone::Eb, 4 => Semitone::E, 5 => Semitone::F, 6 => Semitone::Gb, 7 => Semitone::G, 8 => Semitone::Ab, 9 => Semitone::A, 10 => Semitone::Bb, 11 => Semitone::B, _ => panic!("crate::sound::Semitone::rotate: Unreachable"), } } } #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Arbitrary)] pub struct Interval(isize); #[derive(Debug, Clone, Copy)] pub enum TimeSlice { Sixteenth, Eighth, Quarter, Half, Whole, Triplet(&'static TimeSlice), Dot(&'static TimeSlice), Tie(&'static TimeSlice, &'static TimeSlice), } impl TimeSlice { pub fn to_duration(&self, tempo_qpm: u16) -> Duration { let micros_per_quarter = 1_000_000f64 * 60f64 / tempo_qpm as f64; match &self { &Self::Sixteenth => Duration::from_micros((micros_per_quarter / 4.0) as u64), &Self::Eighth => Duration::from_micros((micros_per_quarter / 2.0) as u64), &Self::Quarter => Duration::from_micros(micros_per_quarter as u64), &Self::Half => Duration::from_micros((micros_per_quarter * 2.0) as u64), &Self::Whole => Duration::from_micros((micros_per_quarter * 4.0) as u64), &Self::Triplet(ts) => ts.to_duration(tempo_qpm) * 2 / 3, &Self::Dot(ts) => ts.to_duration(tempo_qpm) * 3 / 2, &Self::Tie(ts0, ts1) => ts0.to_duration(tempo_qpm) + ts1.to_duration(tempo_qpm), } } } pub fn interval(i: &Interval, note: &Note) -> Note { match (i, note) { (Interval(n), Note { semitone, octave }) => { use std::cmp::Ordering::*; let (o_t, s_t) = n.div_rem(12); let new_semitone = semitone.rotate(s_t); let new_octave = match (new_semitone.cmp(semitone), s_t.cmp(&0)) { (Greater, Less) => octave.clone() as isize + o_t - 1, (Less, Greater) => octave.clone() as isize + o_t + 1, _ => octave.clone() as isize + o_t, }; Note { semitone: new_semitone, octave: new_octave as i8, } } } } pub const MINOR_THIRD: Interval = Interval(3); pub const MAJOR_THIRD: Interval = Interval(4); pub const FOURTH: Interval = Interval(5); pub const FIFTH: Interval = Interval(7); fn iterate T>(f: F, init: &T) -> impl Iterator { let mut temp = init.clone(); let ff = move || { let next = f(&temp); let now = std::mem::replace(&mut temp, next); now }; std::iter::repeat_with(ff) } pub fn circle_of_fifths(note: &Note) -> impl Iterator { iterate(|n| interval(&FIFTH, n), note) } pub fn circle_of_fourths(note: &Note) -> impl Iterator { iterate(|n| interval(&FOURTH, n), note) } #[derive(Clone, Debug)] pub struct CircleOf<'a> { current: Note, duration: TimeSlice, interval: &'a Interval, } impl<'a> CircleOf<'a> { pub const fn new(interval: &'a Interval, start: Note, duration: TimeSlice) -> Self { CircleOf { current: start, duration, interval, } } } impl<'a> Iterator for CircleOf<'a> { type Item = (Option, TimeSlice); fn next(&mut self) -> Option { let current = self.current; let prev = std::mem::replace(&mut self.current, interval(&self.interval, ¤t)); Some((Some(prev), self.duration.clone())) } } macro_rules! song { ($tempo:expr, [$($note:expr;)*]) => { { #[allow(dead_code)] const fn note(semi: Semitone, octave: i8, duration: TimeSlice) -> (Option, TimeSlice) { ( Some(Note { semitone: semi, octave, }), duration, ) } #[allow(dead_code)] const fn rest(duration: TimeSlice) -> (Option, TimeSlice) { (None, duration) } use crate::sound::Semitone::*; use crate::sound::TimeSlice::*; Song { tempo_qpm: $tempo as u16, note_sequence: [ $( $note, )* ] } } }; } pub const BEP: Song<[(Option, TimeSlice); 1]> = song!(150, [note(A, 4, Sixteenth);]); pub const CHIME: Song<[(Option, TimeSlice); 2]> = song!(400, [ note(B, 5, Eighth); note(E, 6, Tie(&Dot(&Quarter), &Half)); ]); pub const SHUTDOWN: Song<[(Option, TimeSlice); 12]> = song!(200, [ note(C, 5, Eighth); rest(Eighth); note(G, 4, Triplet(&Eighth)); note(Gb, 4, Triplet(&Eighth)); note(G, 4, Triplet(&Eighth)); note(Ab, 4, Quarter); note(G, 4, Eighth); rest(Eighth); rest(Quarter); note(B, 4, Eighth); rest(Eighth); note(C, 5, Eighth); ]); pub const UPDATE_FAILED_1: Song<[(Option, TimeSlice); 5]> = song!(120, [ note(C, 4, Triplet(&Sixteenth)); note(Eb, 4, Triplet(&Sixteenth)); note(Gb, 4, Triplet(&Sixteenth)); note(A, 4, Quarter); rest(Eighth); ]); pub const UPDATE_FAILED_2: Song<[(Option, TimeSlice); 5]> = song!(110, [ note(B, 3, Triplet(&Sixteenth)); note(D, 4, Triplet(&Sixteenth)); note(F, 4, Triplet(&Sixteenth)); note(Ab, 4, Quarter); rest(Eighth); ]); pub const UPDATE_FAILED_3: Song<[(Option, TimeSlice); 5]> = song!(100, [ note(Bb, 3, Triplet(&Sixteenth)); note(Db, 4, Triplet(&Sixteenth)); note(E, 4, Triplet(&Sixteenth)); note(G, 4, Quarter); rest(Eighth); ]); pub const UPDATE_FAILED_4: Song<[(Option, TimeSlice); 5]> = song!(90, [ note(A, 3, Triplet(&Sixteenth)); note(C, 4, Triplet(&Sixteenth)); note(Eb, 4, Triplet(&Sixteenth)); note(Gb, 4, Tie(&Dot(&Quarter), &Quarter)); rest(Quarter); ]); pub const BEETHOVEN: Song<[(Option, TimeSlice); 9]> = song!(216, [ note(G, 4, Eighth); note(G, 4, Eighth); note(G, 4, Eighth); note(Eb, 4, Half); rest(Half); note(F, 4, Eighth); note(F, 4, Eighth); note(F, 4, Eighth); note(D, 4, Half); ]); lazy_static::lazy_static! { pub static ref CIRCLE_OF_5THS_SHORT: Song>> = Song { tempo_qpm: 300, note_sequence: CircleOf::new( &FIFTH, Note { semitone: Semitone::A, octave: 3, }, TimeSlice::Triplet(&TimeSlice::Eighth), ) .take(6), }; pub static ref CIRCLE_OF_4THS_SHORT: Song>> = Song { tempo_qpm: 300, note_sequence: CircleOf::new( &FOURTH, Note { semitone: Semitone::C, octave: 4, }, TimeSlice::Triplet(&TimeSlice::Eighth) ).take(6) }; } proptest::prop_compose! { fn arb_interval() (i in -88isize..88isize) -> Interval { Interval(i) } } proptest::prop_compose! { fn arb_note() (o in 0..8i8, s: Semitone) -> Note { Note { semitone: s, octave: o, } } } proptest::proptest! { #[test] fn positive_interval_greater(a in arb_note(), i in arb_interval()) { proptest::prop_assume!(i > Interval(0)); proptest::prop_assert!(interval(&i, &a) > a) } #[test] fn negative_interval_less(a in arb_note(), i in arb_interval()) { proptest::prop_assume!(i < Interval(0)); proptest::prop_assert!(interval(&i, &a) < a) } #[test] fn zero_interval_equal(a in arb_note()) { proptest::prop_assert!(interval(&Interval(0), &a) == a) } #[test] fn positive_negative_cancellation(a in arb_note(), i in arb_interval()) { let neg_i = match i { Interval(n) => Interval(0-n) }; proptest::prop_assert_eq!(interval(&neg_i, &interval(&i, &a)), a) } #[test] fn freq_conversion_preserves_ordering(a in arb_note(), b in arb_note()) { proptest::prop_assert_eq!(Some(a.cmp(&b)), a.frequency().partial_cmp(&b.frequency())) } }