mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-26 02:11:53 +00:00
* the only way to begin is by beginning * chore: Convert over 3444 migration * fix imports * wip * feat: convert volume * convert: system.rs * wip(convert): Setup * wip properties * wip notifications * wip * wip migration * wip init * wip auth/control * wip action * wip control * wiip 034 * wip 344 * wip some more versions converted * feat: Reserialize the version of the db * wip rest of the versions * wip s9pk/manifest * wip wifi * chore: net/keys * chore: net/dns * wip net/dhcp * wip manager manager-map * gut dependency errors * wip update/mod * detect breakages locally for updates * wip: manager/mod * wip: manager/health * wip: backup/target/mod * fix: Typo addresses * clean control.rs * fix system package id * switch to btreemap for now * config wip * wip manager/mod * install wip Co-authored-by: J H <Blu-J@users.noreply.github.com> * chore: Update the last of the errors * feat: Change the prelude de to borrow * feat: Adding in some more things * chore: add to the prelude * chore: Small fixes * chore: Fixing the small errors * wip: Cleaning up check errors * wip: Fix some of the issues * chore: Fix setup * chore:fix version * chore: prelude, mod, http_reader * wip backup_bulk * chore: Last of the errors * upadte package.json * chore: changes needed for a build * chore: Removing some of the linting errors in the manager * chore: Some linting 101 * fix: Wrong order of who owns what * chore: Remove the unstable * chore: Remove the test in the todo * @dr-bonez did a refactoring on the backup * chore: Make sure that there can only be one override guard at a time * resolve most todos * wip: Add some more tracing to debug an error * wip: Use a mv instead of rename * wip: Revert some of the missing code segments found earlier * chore: Make the build * chore: Something about the lib looks like it iis broken * wip: More instrument and dev working * kill netdummy before creating it * better db analysis tools * fixes from testing * fix: Make add start the service * fix status after install * make wormhole * fix missing icon file * fix data url for icons * fix: Bad deser * bugfixes * fix: Backup * fix: Some of the restor * fix: Restoring works * update frontend patch-db types * hack it in (#2424) * hack it in * optimize * slightly cleaner * handle config pointers * dependency config errs * fix compat * cache docker * fix dependency expectation * fix dependency auto-config --------- Co-authored-by: Aiden McClelland <me@drbonez.dev> Co-authored-by: Matt Hill <mattnine@protonmail.com> Co-authored-by: J H <Blu-J@users.noreply.github.com> Co-authored-by: Matt Hill <MattDHill@users.noreply.github.com>
419 lines
12 KiB
Rust
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: &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, ¤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<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()))
|
|
}
|
|
|
|
}
|