Feature/sound (#389)

* WIP sound lib

* basically finishes sound interface, still needs circle of fifths for updates etc.

* finishes sound interface, includes light testing

* fixes loops to use euclidian remainder

* implements locking for the sound interface

* stop sleeping on blocks
This commit is contained in:
Keagan McClelland
2021-08-12 10:20:36 -06:00
committed by Aiden McClelland
parent 073a8d2e8d
commit f92db6fac8
5 changed files with 530 additions and 1 deletions

79
appmgr/Cargo.lock generated
View File

@@ -726,6 +726,12 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "212d0f5754cb6769937f4501cc0e67f4f4483c8d2c3e1e922ee9edbe4ab4c7c0"
[[package]]
name = "divrem"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69dde51e8fef5e12c1d65e0929b03d66e4c0c18282bc30ed2ca050ad6f44dd82"
[[package]]
name = "dotenv"
version = "0.15.0"
@@ -786,8 +792,10 @@ dependencies = [
"clap",
"cookie_store 0.15.0",
"digest 0.9.0",
"divrem",
"ed25519-dalek",
"emver",
"fd-lock-rs",
"futures",
"git-version",
"http",
@@ -804,6 +812,8 @@ dependencies = [
"patch-db",
"pin-project",
"prettytable-rs",
"proptest",
"proptest-derive",
"rand 0.7.3",
"regex",
"reqwest",
@@ -1254,7 +1264,7 @@ version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df004cfca50ef23c36850aaaa59ad52cc70d0e90243c3c7737a4dd32dc7a3c4f"
dependencies = [
"quick-error",
"quick-error 1.2.3",
]
[[package]]
@@ -2022,6 +2032,37 @@ dependencies = [
"unicode-xid 0.2.2",
]
[[package]]
name = "proptest"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e0d9cc07f18492d879586c92b485def06bc850da3118075cd45d50e9c95b0e5"
dependencies = [
"bit-set",
"bitflags",
"byteorder",
"lazy_static",
"num-traits",
"quick-error 2.0.1",
"rand 0.8.4",
"rand_chacha 0.3.1",
"rand_xorshift",
"regex-syntax",
"rusty-fork",
"tempfile",
]
[[package]]
name = "proptest-derive"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90b46295382dc76166cb7cf2bb4a97952464e4b7ed5a43e6cd34e1fec3349ddc"
dependencies = [
"proc-macro2 0.4.30",
"quote 0.6.13",
"syn 0.15.44",
]
[[package]]
name = "psl-types"
version = "2.0.7"
@@ -2056,6 +2097,12 @@ version = "1.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
[[package]]
name = "quick-error"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3"
[[package]]
name = "quote"
version = "0.6.13"
@@ -2169,6 +2216,15 @@ dependencies = [
"rand_core 0.6.3",
]
[[package]]
name = "rand_xorshift"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d25bf25ec5ae4a3f1b92f929810509a2f53d7dca2f50b794ff57e3face536c8f"
dependencies = [
"rand_core 0.6.3",
]
[[package]]
name = "redox_syscall"
version = "0.1.57"
@@ -2396,6 +2452,18 @@ version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61b3909d758bb75c79f23d4736fac9433868679d3ad2ea7a61e3c25cfda9a088"
[[package]]
name = "rusty-fork"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb3dcc6e454c328bb824492db107ab7c0ae8fcffe4ad210136ef014458c1bc4f"
dependencies = [
"fnv",
"quick-error 1.2.3",
"tempfile",
"wait-timeout",
]
[[package]]
name = "ryu"
version = "1.0.5"
@@ -3448,6 +3516,15 @@ version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe"
[[package]]
name = "wait-timeout"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6"
dependencies = [
"libc",
]
[[package]]
name = "want"
version = "0.3.0"

View File

@@ -55,8 +55,10 @@ chrono = { version = "0.4.19", features = ["serde"] }
clap = "2.33"
cookie_store = "0.15.0"
digest = "0.9.0"
divrem = "1.0.0"
ed25519-dalek = { version = "1.0.1", features = ["serde"] }
emver = { version = "0.1.2", features = ["serde"] }
fd-lock-rs = "*"
futures = "0.3.8"
git-version = "0.3.4"
http = "0.2.3"
@@ -73,6 +75,8 @@ openssl = { version = "0.10.30", features = ["vendored"] }
patch-db = { version = "*", path = "../../patch-db/patch-db" }
pin-project = "1.0.6"
prettytable-rs = "0.8.0"
proptest = "1.0.0"
proptest-derive = "0.3.0"
rand = "0.7.3"
regex = "1.4.2"
reqwest = { version = "0.11.2", features = ["stream", "json"] }

View File

@@ -48,6 +48,7 @@ pub enum ErrorKind {
Uninitialized = 40,
ParseNetAddress = 41,
ParseSshKey = 42,
SoundError = 43,
}
impl ErrorKind {
pub fn as_str(&self) -> &'static str {
@@ -95,6 +96,7 @@ impl ErrorKind {
Uninitialized => "Uninitialized",
ParseNetAddress => "Net Address Parsing Error",
ParseSshKey => "SSH Key Parsing Error",
SoundError => "Sound Interface Error",
}
}
}

View File

@@ -35,6 +35,7 @@ pub mod migration;
pub mod net;
pub mod registry;
pub mod s9pk;
pub mod sound;
pub mod ssh;
pub mod status;
pub mod util;

445
appmgr/src/sound.rs Normal file
View File

@@ -0,0 +1,445 @@
use crate::{Error, ErrorKind, ResultExt};
use divrem::DivRem;
use proptest_derive::Arbitrary;
use std::{cmp::Ordering, path::Path, time::Duration};
use tokio::sync::{Mutex, MutexGuard};
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);
static ref EXPORT_FILE: &'static Path = Path::new("/sys/class/pwm/pwmchip0/pwm0/export");
static ref UNEXPORT_FILE: &'static Path = Path::new("/sys/class/pwm/pwmchip0/pwm0/unexport");
static ref PERIOD_FILE: &'static Path = Path::new("/sys/class/pwm/pwmchip0/pwm0/period");
static ref DUTY_FILE: &'static Path = Path::new("/sys/class/pwm/pwmchip0/pwm0/duty_cycle");
static ref SWITCH_FILE: &'static Path = Path::new("/sys/class/pwm/pwmchip0/pwm0/enable");
static ref SOUND_MUTEX: Mutex<Option<fd_lock_rs::FdLock<tokio::fs::File>>> = Mutex::new(None);
}
pub const SOUND_LOCK_FILE: &'static str = "/TODO/AIDEN/CHANGEME";
struct SoundInterface(Option<MutexGuard<'static, Option<fd_lock_rs::FdLock<tokio::fs::File>>>>);
impl SoundInterface {
pub async fn lease() -> Result<Self, Error> {
tokio::fs::write(&*EXPORT_FILE, "0")
.await
.map_err(|e| Error {
source: e.into(),
kind: ErrorKind::SoundError,
revision: None,
})?;
let mut guard = SOUND_MUTEX.lock().await;
let sound_file = tokio::fs::File::create(SOUND_LOCK_FILE).await?;
*guard = Some(
tokio::task::spawn_blocking(move || {
fd_lock_rs::FdLock::lock(sound_file, fd_lock_rs::LockType::Exclusive, true)
})
.await
.map_err(|e| {
Error::new(
anyhow::anyhow!("Sound file lock panicked: {}", e),
ErrorKind::SoundError,
)
})?
.with_kind(ErrorKind::SoundError)?,
);
Ok(SoundInterface(Some(guard)))
}
pub async fn play(&mut self, note: &Note) -> Result<(), Error> {
{
let curr_period = tokio::fs::read_to_string(&*PERIOD_FILE).await?;
if curr_period == "0\n" {
tokio::fs::write(&*PERIOD_FILE, "1000").await?;
}
let new_period = ((1.0 / note.frequency()) * 1_000_000_000.0).round() as u64;
tokio::fs::write(&*DUTY_FILE, "0").await?;
tokio::fs::write(&*PERIOD_FILE, format!("{}", new_period)).await?;
tokio::fs::write(&*DUTY_FILE, format!("{}", new_period / 2)).await?;
tokio::fs::write(&*SWITCH_FILE, "1").await
}
.map_err(|e| Error {
source: e.into(),
kind: ErrorKind::SoundError,
revision: None,
})
}
pub async fn play_for_time_slice(
&mut self,
tempo_qpm: u16,
note: &Note,
time_slice: &TimeSlice,
) -> Result<(), Error> {
{
self.play(note).await?;
tokio::time::sleep(time_slice.to_duration((tempo_qpm as u64 * 19 / 20) as u16)).await;
self.stop().await?;
tokio::time::sleep(time_slice.to_duration(tempo_qpm / 20)).await;
Ok(())
}
.or_else(|e: Error| {
// we could catch this error and propagate but I'd much prefer the original error bubble up
let _mute = self.stop();
Err(e)
})
}
pub async fn stop(&mut self) -> Result<(), Error> {
tokio::fs::write(&*SWITCH_FILE, "0")
.await
.map_err(|e| Error {
source: e.into(),
kind: ErrorKind::SoundError,
revision: None,
})
}
}
pub struct Song<Notes> {
tempo_qpm: u16,
note_sequence: Notes,
}
impl<'a, T: 'a> Song<T>
where
&'a T: IntoIterator<Item = &'a (Option<Note>, TimeSlice)>,
{
pub async fn play(&'a self) -> Result<(), Error> {
let mut sound = SoundInterface::lease().await?;
for (note, slice) in &self.note_sequence {
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?,
};
}
Ok(())
}
}
impl Drop for SoundInterface {
fn drop(&mut self) {
let guard = self.0.take();
tokio::spawn(async move {
if let Err(e) = tokio::fs::write(&*UNEXPORT_FILE, "0").await {
log::error!("Failed to Unexport Sound Interface: {}", e)
}
if let Some(mut guard) = guard {
if let Some(lock) = guard.take() {
if let Err(e) = tokio::task::spawn_blocking(|| lock.unlock(true))
.await
.unwrap()
{
log::error!("Failed to drop Sound Interface File Lock: {}", e.1)
}
}
}
});
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Arbitrary)]
pub struct Note {
semitone: Semitone,
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 mut 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(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 = (tempo_qpm as f64) * 1_000_000f64;
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)
}
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;)*]) => {
{
const fn note(semi: Semitone, octave: i8, duration: TimeSlice) -> (Option<Note>, TimeSlice) {
(
Some(Note {
semitone: semi,
octave,
}),
duration,
)
}
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 MARIO_DEATH: Song<[(Option<Note>, TimeSlice); 12]> = song!(400, [
note(B, 4, Quarter);
note(F, 5, Quarter);
rest(Quarter);
note(F, 5, Quarter);
note(F, 5, Triplet(&Half));
note(E, 5, Triplet(&Half));
note(D, 5, Triplet(&Half));
note(C, 5, Quarter);
note(E, 5, Quarter);
rest(Quarter);
note(E, 5, Quarter);
note(C, 4, Half);
]);
pub const MARIO_POWER_UP: Song<[(Option<Note>, TimeSlice); 15]> = song!(400, [
note(G,4,Triplet(&Eighth));
note(B,4,Triplet(&Eighth));
note(D,5,Triplet(&Eighth));
note(G,5,Triplet(&Eighth));
note(B,5,Triplet(&Eighth));
note(Ab,4,Triplet(&Eighth));
note(C,5,Triplet(&Eighth));
note(Eb,5,Triplet(&Eighth));
note(Ab,5,Triplet(&Eighth));
note(C,5,Triplet(&Eighth));
note(Bb,4,Triplet(&Eighth));
note(D,5,Triplet(&Eighth));
note(F,5,Triplet(&Eighth));
note(Bb,5,Triplet(&Eighth));
note(D,6,Triplet(&Eighth));
]);
pub const MARIO_COIN: Song<[(Option<Note>, TimeSlice); 2]> = song!(400, [
note(B, 5, Eighth);
note(E, 6, Tie(&Dot(&Quarter), &Half));
]);
pub const BEETHOVEN: Song<[(Option<Note>, TimeSlice); 9]> = song!(216, [
note(E, 5, Eighth);
note(E, 5, Eighth);
note(E, 5, Eighth);
note(C, 5, Half);
rest(Half);
note(D, 5, Eighth);
note(D, 5, Eighth);
note(D, 5, Eighth);
note(B, 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()))
}
}