mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-26 10:21:52 +00:00
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:
committed by
Aiden McClelland
parent
073a8d2e8d
commit
f92db6fac8
79
appmgr/Cargo.lock
generated
79
appmgr/Cargo.lock
generated
@@ -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"
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
445
appmgr/src/sound.rs
Normal 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, ¤t));
|
||||
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()))
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user