Files
start-os/backend/src/sound.rs
2023-03-15 12:19:11 -06:00

419 lines
12 KiB
Rust

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<FileLock>,
}
impl SoundInterface {
#[instrument(skip_all)]
pub async fn lease() -> Result<Self, Error> {
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<Notes> {
pub tempo_qpm: u16,
pub note_sequence: Notes,
}
impl<'a, T> Song<T>
where
T: IntoIterator<Item = (Option<Note>, 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<Ordering> {
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: Clone, F: Fn(&T) -> T>(f: F, init: &T) -> impl Iterator<Item = T> {
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<Item = Note> {
iterate(|n| interval(&FIFTH, n), note)
}
pub fn circle_of_fourths(note: &Note) -> impl Iterator<Item = Note> {
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<Note>, TimeSlice);
fn next(&mut self) -> Option<Self::Item> {
let current = self.current;
let prev = std::mem::replace(&mut self.current, interval(&self.interval, &current));
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<Note>, TimeSlice) {
(
Some(Note {
semitone: semi,
octave,
}),
duration,
)
}
#[allow(dead_code)]
const fn rest(duration: TimeSlice) -> (Option<Note>, 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<Note>, TimeSlice); 1]> = song!(150, [note(A, 4, Sixteenth);]);
pub const CHIME: Song<[(Option<Note>, TimeSlice); 2]> = song!(400, [
note(B, 5, Eighth);
note(E, 6, Tie(&Dot(&Quarter), &Half));
]);
pub const SHUTDOWN: Song<[(Option<Note>, 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<Note>, 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<Note>, 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<Note>, 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<Note>, 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<Note>, 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<std::iter::Take<CircleOf<'static>>> = 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<std::iter::Take<CircleOf<'static>>> = 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()))
}
}