From e954df3f896bd18494cd27d77b26bbb2005de8a7 Mon Sep 17 00:00:00 2001 From: JSDurand Date: Wed, 24 Aug 2022 23:54:13 +0800 Subject: First commit Now the project is in a somewhat complete state, ready for future enhancements. --- .cargo/config | 8 + .gitignore | 4 + Cargo.toml | 13 + play.sh | 3 + songs/Ievan Polkka.rumu | 324 ++++++++ songs/Wellerman.rumu | 85 ++ songs/experiment.rumu | 66 ++ songs/katyusha.rumu | 44 + songs/take me hand.rumu | 33 + songs/test.rumu | 10 + src/instruments.rs | 485 +++++++++++ src/lib.rs | 279 +++++++ src/main.rs | 214 +++++ src/output/ffmpeg_output.rs | 417 ++++++++++ src/output/mod.rs | 68 ++ src/sheet/mod.rs | 1905 +++++++++++++++++++++++++++++++++++++++++++ src/sheet/play.rs | 314 +++++++ tests/sine.rs | 175 ++++ 18 files changed, 4447 insertions(+) create mode 100644 .cargo/config create mode 100644 .gitignore create mode 100644 Cargo.toml create mode 100755 play.sh create mode 100644 songs/Ievan Polkka.rumu create mode 100644 songs/Wellerman.rumu create mode 100644 songs/experiment.rumu create mode 100644 songs/katyusha.rumu create mode 100644 songs/take me hand.rumu create mode 100644 songs/test.rumu create mode 100644 src/instruments.rs create mode 100644 src/lib.rs create mode 100644 src/main.rs create mode 100644 src/output/ffmpeg_output.rs create mode 100644 src/output/mod.rs create mode 100644 src/sheet/mod.rs create mode 100644 src/sheet/play.rs create mode 100644 tests/sine.rs diff --git a/.cargo/config b/.cargo/config new file mode 100644 index 0000000..e455201 --- /dev/null +++ b/.cargo/config @@ -0,0 +1,8 @@ +[alias] +bf = "build --features ffmpeg" +rf = "run --features ffmpeg" +tf = "test --features ffmpeg" +bfr = "build --features ffmpeg --release" +rfr = "run --features ffmpeg --release" +tfr = "test --features ffmpeg --release" + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..57643ef --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +target/* +audio files/* +Cargo.lock +index.html diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..369674e --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "rumu" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +ac-ffmpeg = { version = "0.17.4", optional = true } + +[features] +default = [] +ffmpeg = ["dep:ac-ffmpeg"] diff --git a/play.sh b/play.sh new file mode 100755 index 0000000..73013fe --- /dev/null +++ b/play.sh @@ -0,0 +1,3 @@ +#!/bin/sh + +ffplay -f f64le -ar 44100 -autoexit $1 diff --git a/songs/Ievan Polkka.rumu b/songs/Ievan Polkka.rumu new file mode 100644 index 0000000..35afb1c --- /dev/null +++ b/songs/Ievan Polkka.rumu @@ -0,0 +1,324 @@ +; This song comes from the following website: +; + +; high part + +{ + iv1 iipiano + + ib40 + + rp3 + + ib70 + + rp5 + + ib100 + + rp4 + +; first line over + + ib120 + + rp4 ; rest for 16 beats in total + + o4 e4 a a a a2 a4 b > c c < a2 a > c + + o4 b4 b g g g2 b > c < a a1 + +; second line over + + o4 e2 a a a4 b > c 2 < a4 a a2 a4 > c + + o5 e2 d4 d c2 < b4 b > c2 < a a > c4 c + + e2 e4 e d2 c < b g g b + + > d d c < b4 b > c2 < a4 a a2 > c + +; third line over + + o5 e4 e e e d2 c < b g g g4 b + + o5 d2 d c < b > c4 < a a2 a1 + + rp4 + + o4 e2 a a a4 b > c2 < a a a4 > c + +; fourth line over + + o4 b2 g4 g g2 b > c < a a a + + e a4 a a2 a4 b > c 2 < a a a4 > c + + o5 e2 d4 d c2 < b4 b > c2 < a a > c + + o5 e2 e4 e d2 c < b g g b + +; fifth line over + + o5 d d4 d c2 < b > c < a4 a a2 > c + + e4 e e e d2 c < b g g g4 b + + o5 d2 d c < b > c4 < a a2 a1 + + o4 e2 a a a4 b > c2 < a a > c + +; first page over + + o4 b g g g4 b > c2 < a a1 + + e2 a4 a a2 b > c < a a > c + + e d c < b a a a1 + + o5 e4 e e e d2 c < b g g b + + > d d4 d c2 < b > c < a4 a a2 > c + +; first line over + + e4 e e e d2 c < b g g g4 b + + o5 d2 d c < b > c4 < a a2 a1 + + e4 e a a a a b b > c c < a a a2 > c + +; second line over + + o4 b4 b g g g g b b > c c < a2 a1 + + e4 e a a a2 b > c4 c < a a a2 > c + + e4 e d d c c < b b > c c < a2 a1 + +; third line over + + o5 e4 e e e d2 c < b4 b g g g2 b + + o5 d4 d d d c c < b b > c c < a2 a > c + + e4 e e e d d c c < b2 g4 g g2 b + +; fourth line over + + o5 d4 d d d c c < b b > c c < a2 a1 +} + +; low part + +{ + iv1 iipiano + + ib40 ; this will change + + o2 a1 e a + + ib70 ; speed up + + e + + (a1 e)*2 + + ib100 ; again speed up + + (a1 e ) * 2 + +; first line over + + ib120 + + (a1 e ) * 4 + + e g a e + +; second line over + + a e a e + + e g a2 a a1 + + a e e g + + e g a e + +; third line over + + a e e g + + e g > c4 < a a2 a1 + + a e a e + + a e a e + +; fourth line over + + e g a e + + a e a e + + e g > c2 < a a a + + a1 e e g + +; fifth line over + + e g a e + + a e e g + + e g > c4 < a a2 a1 + + (a2 a a a)*2 + +; first page over + + g g g g a a a a + + (a a a a) * 2 + + g g g g a a a1 + + a2 a a a g g g g + + g g g g a a a a + + a a a a g g g g + + g g g g > c4 < a a2 a1 + + a e a e + +; second line over + + e g a e + + a e a e + + e g a e + +; third line over + + a e e g + + e g a e + + a e e g + +; fourth line over + + e g a a +} + +; middle part + +{ + iv1 iipiano + + ib40 + + r2 o3 a4 a r2 a r a4 a + + ib70 + + r2 a + + (r2 a4 a r2 a) * 2 + + ib100 + + (r2 a4 a4 r2 a) * 2 + +; first line over + + ib120 + + (r2 a4 a r2 a) * 2 + + r2 a4 a r2 a4 a r2 a r a + + r g4 g r2 g r a r a + +; second line over + + r2 a r a4 a r2 a4 a r2 a4 a + + r2 g4 g r2 g4 g a2 a a1 + + r2 a4 a r2 a r g r g + + r g4 g r2 g r a4 a r2 a + +; third line over + + r a4 a r2 a r g r g + + r g r g > c4 < a a2 a1 + + r2 a4 a r2 a r a4 a r2 a + + r a r a4 a r2 a r a4 a + +; fourth line over + + r2 g4 g r2 g r a r a + + r a4 a r2 a4 a r2 a r a4 a + + r2 g4 g r2 g4 g o4 c2 < a a a + + r a4 a r2 a r g r g + +; fifth line over + + r g4 g r2 g r a4 a r2 a + + r a4 a r2 a r g r g4 g + + r2 g r g > c4 < a a2 a1 + + (o3 a2 a a a)*2 + +; first page over + + g g g g a a a a + + ( a a a a) * 2 + + g g g g a a a1 + + a2 a a a g g g g + + g g g g a a a a + + a a a a g g g g + + g g g g > c4 < a a2 a1 + + ( r2 a ) * 4 + +; second line over + + r g r g r a r a + + ( r a ) * 4 + + r g r g r a r a + +; third line over + + r a r a r g r g + + r g r g r a r a + + r a r a r g r g + +; fourth line over + + r g r g a1 a +} diff --git a/songs/Wellerman.rumu b/songs/Wellerman.rumu new file mode 100644 index 0000000..fc6ba77 --- /dev/null +++ b/songs/Wellerman.rumu @@ -0,0 +1,85 @@ +; This song is from https://musescore.com/user/35843650/scores/6583881 + +{ + iipiano ib168 iv1 + +; The key signature is the E flat major, or the C minor. +; +; This has three flat notes, which are Bb, Eb, and Ab. +; +; The notes of this scale are: +; +; 3 , 5, 7, 8 , 10, 0, 2, 3 +; Eb, F, G, Ab, Bb, C, D, Eb +; +; The chord Eb has the notes: Eb, G, Bb. +; The chord Ebmaj7 has the notes: Eb, G, bb, D. + + o3 g p4 c1 c2 c c1 e- + + g g g. g2 + + f / a- / > c p4 + + o4 c 2 c < g1 g. g2 + + c1 c c e- + + g g g g + + f f e- 2 e- d1 c p4 + + o3 a- / > c / e- p2 c1. < a-2 + + o3 e- / g / b- 2 b- g 1 g1. g2 + +; third line over + + o3 f / a- / > c 1 < f f2 g a-1 + + o4 c / e- / g 1 o3 g g p2 + +} + +{ + iipiano ib168 iv1 + + r p4 + + o2 g / c o3 e- / c + + c + + c / e- / g + + o2 g / e- + + o3 e- / c + + o2 f p2 + + e- / b- 1 f / > c o2 c / g 1. c 2 c / g p2 + + a-1. a-2 o3 a-1 < a- + + o2 e- 1. e-2 > e- 1 < e- + +; third line over + + o2 f 1. f2 f1 f + + o2 c1. c2 o3 c1 o2 c + +} + + +{ +; Occasionnally there is one more voice. + + iipiano ib168 iv1 + + rp12 r1 + + o3 f2 f f1 f2 a- + +} \ No newline at end of file diff --git a/songs/experiment.rumu b/songs/experiment.rumu new file mode 100644 index 0000000..cfc8f86 --- /dev/null +++ b/songs/experiment.rumu @@ -0,0 +1,66 @@ +{ + iipiano iv1 ib70 + +; E flat major scale: +; 3 5 7 8 10 12 14 15 17 19 20 22 24 26 27 +; Eb F G Ab Bb C D Eb F G Ab Bb C D Eb + +; o4 e- / g / b- / > d / f / a / > c + +; B flat major scale: +; 10 12 14 15 17 19 21 22 24 26 27 29 31 33 34 +; Bb C D Eb F G A Bb C D Eb F G A Bb + +; o3 b- / > e- / f / a- / > c + +; B flat minor scale: +; 10 12 13 15 17 18 20 22 24 25 27 29 30 32 34 +; Bb C Db Eb F Gb Ab Bb C Db Eb F Gb Ab Bb + + +; Let's try some chords in the B flat major scale and the +; corresponding minor scale. + +; the chord progression is 4536 + +; First the B flat major scale + +; the 4th chord is Eb G Bb +; the 5th chord is F A C +; the 3rd chord is D F A +; the 6th chord is G Bb D + + (o3 e- / g / b- 1) * 4 + (o3 f / a / > c 1) * 4 + (o3 d / f / a 1) * 4 + (o3 g / b- / >d 1) * 4 + +; Then the C major scale +; +; the 4th chord is F A C E +; the 5th chord is G B D F +; the 3rd chord is E G B D +; the 6th chord is A C E +; the 2nd chord is D F A C +; the 5th chord is G B D F +; the 1st chord is C E G + + ( + + (o4 f / a / c / e p1)*4 + + (o4 g / f / b / d) * 4 + + (o4 e / g / c / < e) * 4 + + (o4 d / f / a / c) * 4 + + (g / b / d / f) * 4 + + (c / e / g) * 4 + + )*1 + +} diff --git a/songs/katyusha.rumu b/songs/katyusha.rumu new file mode 100644 index 0000000..417f555 --- /dev/null +++ b/songs/katyusha.rumu @@ -0,0 +1,44 @@ +{ + iv0.7 ib126 + + ( r2 o3 f+ / d / < b ) * 10 + + ( + r2 o3 f+ / c+ / < a+ + r o3 f+ / c+ / < a + ) * 4 + + ( r o3 f+ / d / < b ) * 2 + + ( r2 o3 b / f+ / < b )*2 + + r o3 a / f+ / d r a / f+ / d+ + + (r b / g / e)*2 + + (r b / f+ / < b)*2 + + (r b / g / e) * 2 + + (r b / f+ / < b)*2 + + r a+ / f+ / c+ + r a+ / f+ / c + + b f+ + +} + +{ + iv1.3 ib126 + + rp4 + + o3 b1. > c2 d1. < b2 >d d c < b > c1 < f > c1. d2 e1. c2 e e d c < bp2 + + o4 f1 b a b2 a g2. g4 f2 e f1 < b + + r2 g1 e2 f1. d2 + + e2. e4 d2 c < b1 +} \ No newline at end of file diff --git a/songs/take me hand.rumu b/songs/take me hand.rumu new file mode 100644 index 0000000..a3ec84d --- /dev/null +++ b/songs/take me hand.rumu @@ -0,0 +1,33 @@ +; The full name of the song is "DAISHI DANCE, Cecile Corbel - Take Me +; Hand". +; +; It can be found here: . + + +{ + iipiano iv1 ib123 + + o6 f2 d e1. < b2 r + o6 f2 d e1. f2 r < b + o6 f2 d e1. < b2 r + b + o6 f2 d e1. f2 r < b + + + + +} + +{ + iipiano iv0.5 ib123 + + o3 g1 > d b. < a1 > e + + o5 c+1. o3 b 1 > f+ b + + f+ + + o5 d < f+ > d + + +} diff --git a/songs/test.rumu b/songs/test.rumu new file mode 100644 index 0000000..ed485f1 --- /dev/null +++ b/songs/test.rumu @@ -0,0 +1,10 @@ +; This is just a simple sine wave to test my fast fourier transform +; algorithms. + +{ + iv1 iisinus ib60 + + (o4 c1 d e f g a b > c < b a g f e d c) * 4 + + +} \ No newline at end of file diff --git a/src/instruments.rs b/src/instruments.rs new file mode 100644 index 0000000..15ac7d4 --- /dev/null +++ b/src/instruments.rs @@ -0,0 +1,485 @@ +//! This file implements an interface for instruments. +//! +//! An instrument is a type that can produce a `Wave` when given a +//! note in semitone, a sample rate, and a duration in beats. + +use super::*; +use std::{ + f64::consts::PI, + fmt::{self, Display, Formatter}, +}; + +/// The interface of an instrument. +pub trait Instrument { + /// Produce pulses for a given note, sample rate, and duration. + /// + /// This will be separately modified to account for the effect of + /// attack-decay-sustain-release. See + /// [Wikipedia](). + fn play(&mut self, note: T, rate: U, bpm: S, duration: V, volume: R) -> Wave + where + T: Into, + S: Into, + U: Into, + V: Into, + R: Into; +} + +// Some instruments + +#[derive(Default)] +pub struct Sinus {} + +impl Display for Sinus { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "the pure sine instrument") + } +} + +impl Instrument for Sinus { + fn play(&mut self, note: T, rate: U, bpm: S, duration: V, volume: R) -> Wave + where + T: Into, + S: Into, + U: Into, + V: Into, + R: Into, + { + let note = note.into(); + let rate = rate.into(); + let duration = duration.into(); + let volume = volume.into(); + + let bpm = bpm.into(); + let duration_per_beat: Seconds = bpm.into(); + let duration: Seconds = (*duration * *duration_per_beat).into(); + + let nb_samples = (*duration * *rate).floor() as usize; + + let note_h: Hertz = note.into(); + + let step = *note_h * 2f64 * PI / *rate; + + let mut theta = 0f64; + + let mut result: Vec = vec![]; + + let mut current_adsr = 0f64; + + for i in 0..nb_samples { + current_adsr = adsr(i, nb_samples, current_adsr); + + let y: f64 = (*volume * f64::sin(theta)) * current_adsr; + + let y = if y >= 1f64 { + 1f64 + } else if y <= -1f64 { + -1f64 + } else { + y + }; + + result.push(y.into()); + + theta += step; + } + + Wave::new(result) + } +} + +// Other instruments: tangent, and variations of piano. + +// Tangent + +#[derive(Default)] +pub struct Tangent {} + +impl Display for Tangent { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "the pure tangent instrument") + } +} + +impl Instrument for Tangent { + fn play(&mut self, note: T, rate: U, bpm: S, duration: V, volume: R) -> Wave + where + T: Into, + S: Into, + U: Into, + V: Into, + R: Into, + { + let note = note.into(); + let rate = rate.into(); + let duration = duration.into(); + let volume = volume.into(); + + let bpm = bpm.into(); + let duration_per_beat: Seconds = bpm.into(); + let duration: Seconds = (*duration * *duration_per_beat).into(); + + let nb_samples = (*duration * *rate).floor() as usize; + + let note_h: Hertz = note.into(); + + let step = *note_h * 2f64 * PI / *rate; + + let mut theta = 0f64; + + let mut result: Vec = vec![]; + + let mut current_adsr = 0f64; + + for i in 0..nb_samples { + current_adsr = adsr(i, nb_samples, current_adsr); + + let y: f64 = (*volume * f64::tan(theta)) * current_adsr; + + let y = if y >= 1f64 { + 1f64 + } else if y <= -1f64 { + -1f64 + } else { + y + }; + + result.push(y.into()); + + theta += step; + } + + Wave::new(result) + } +} + +// Piano Without overtones + +#[derive(Default)] +pub struct PianoWOver {} + +impl Display for PianoWOver { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "the piano instrument without overtones") + } +} + +impl Instrument for PianoWOver { + fn play(&mut self, note: T, rate: U, bpm: S, duration: V, volume: R) -> Wave + where + T: Into, + S: Into, + U: Into, + V: Into, + R: Into, + { + let note = note.into(); + let rate = rate.into(); + let duration = duration.into(); + let volume = volume.into(); + + let bpm = bpm.into(); + let duration_per_beat: Seconds = bpm.into(); + let duration: Seconds = (*duration * *duration_per_beat).into(); + + let nb_samples = (*duration * *rate).floor() as usize; + + let note_h: Hertz = note.into(); + + let step = *note_h * 2f64 * PI / *rate; + + let mut theta = 0f64; + + let mut result: Vec = vec![]; + + for i in 0..nb_samples { + let mut y = f64::sin(theta); + // let mut y = 0.6f64 * f64::sin(theta) + 0.4f64 * f64::sin(2f64 * theta); + y *= f64::exp((-0.0035f64) * theta); + y += y * y * y; + y *= 1f64 + 16f64 * i as f64 * f64::exp((-6f64) * i as f64); + + y *= *volume; + + result.push(y.into()); + + theta += step; + } + + Wave::new(result) + } +} + +#[derive(Default)] +pub struct PianoVideo {} + +impl Display for PianoVideo { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "the piano instrument that I obtained from a video") + } +} + +impl Instrument for PianoVideo { + fn play(&mut self, note: T, rate: U, bpm: S, duration: V, volume: R) -> Wave + where + T: Into, + S: Into, + U: Into, + V: Into, + R: Into, + { + let note = note.into(); + let rate = rate.into(); + let duration = duration.into(); + let volume = volume.into(); + + let bpm = bpm.into(); + let duration_per_beat: Seconds = bpm.into(); + let duration: Seconds = (*duration * *duration_per_beat).into(); + + let nb_samples = (*duration * *rate).floor() as usize; + + let note_h: Hertz = note.into(); + + let step = *note_h * 2f64 * PI / *rate; + + let mut theta = 0f64; + + let mut result: Vec = vec![]; + + for i in 0..nb_samples { + let mut y = 0.6f64 * f64::sin(theta) + 0.4f64 * f64::sin(2f64 * theta); + y *= f64::exp((-0.0015f64) * theta); + y += y * y * y; + y *= 1f64 + 16f64 * i as f64 * f64::exp((-6f64) * i as f64); + + y *= *volume; + + result.push(y.into()); + + theta += step; + } + + Wave::new(result) + } +} + +#[derive(Default)] +pub struct PianoStackoverflow {} + +impl Display for PianoStackoverflow { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!( + f, + "the piano instrument that I obtained from a stackoverflow answer." + ) + } +} + +impl Instrument for PianoStackoverflow { + fn play(&mut self, note: T, rate: U, bpm: S, duration: V, volume: R) -> Wave + where + T: Into, + S: Into, + U: Into, + V: Into, + R: Into, + { + let note = note.into(); + let rate = rate.into(); + let duration = duration.into(); + let volume = volume.into(); + + let bpm = bpm.into(); + let duration_per_beat: Seconds = bpm.into(); + let duration: Seconds = (*duration * *duration_per_beat).into(); + + let nb_samples = (*duration * *rate).floor() as usize; + + let note_h: Hertz = note.into(); + + // dbg!(note); + // dbg!(note_h); + + let step = *note_h * 2f64 * PI / *rate; + + let mut theta = 0f64; + + let mut result: Vec = vec![]; + + for i in 0..nb_samples { + let mut y = f64::sin(theta) + + (1f64 / 2f64) * f64::sin(2f64 * theta) + + (1f64 / 4f64) * f64::sin(3f64 * theta) + + (1f64 / 8f64) * f64::sin(4f64 * theta) + + (1f64 / 16f64) * f64::sin(5f64 * theta) + + (1f64 / 32f64) * f64::sin(6f64 * theta); + y *= f64::exp((-0.0015f64) * theta); + y += y * y * y; + y *= 1f64 + 16f64 * i as f64 * f64::exp((-6f64) * i as f64); + + y *= *volume; + + result.push(y.into()); + + theta += step; + } + + Wave::new(result) + } +} + +#[allow(dead_code)] +fn adsr(step: usize, total: usize, current: f64) -> f64 { + if step < 4320 { + if current <= 1f64 { + current + 0.0002314814814814815f64 + } else { + 1f64 + } + } else if total - step < 4320 { + if current >= 0f64 { + current - 0.0002314814814814815f64 + } else { + 0f64 + } + } else { + current + } +} + +#[derive(Default)] +pub struct PianoFromC {} + +impl Display for PianoFromC { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "the piano instrument from the last project in C.") + } +} + +impl Instrument for PianoFromC { + fn play(&mut self, note: T, rate: U, bpm: S, duration: V, volume: R) -> Wave + where + T: Into, + S: Into, + U: Into, + V: Into, + R: Into, + { + let note = note.into(); + let rate = rate.into(); + let duration = duration.into(); + let volume = volume.into(); + + let bpm = bpm.into(); + let duration_per_beat: Seconds = bpm.into(); + let duration: Seconds = (*duration * *duration_per_beat).into(); + + let nb_samples = (*duration * *rate).floor() as usize; + + let note_h: Hertz = note.into(); + + // dbg!(note); + // dbg!(note_h); + + let step = *note_h * 2f64 * PI / *rate; + + let mut theta = 0f64; + + let mut result: Vec = vec![]; + + let mut adsr_factor = 1f64; + + #[allow(unused_variables)] + for i in 0..nb_samples { + let mut y = 0.65f64 * f64::sin(theta) + + 0.475f64 * f64::sin(2f64 * theta) + + 0.05f64 * f64::sin(3f64 * theta); + y *= f64::exp((-0.0015f64) * theta); + y += y * y * y; + y *= 1f64 + 16f64 * i as f64 * f64::exp((-6f64) * i as f64); + + adsr_factor = adsr(i, nb_samples, adsr_factor); + y *= adsr_factor; + + y *= *volume; + + // if y >= 1f64 { + // y = 1f64; + // } else if y <= -1f64 { + // y = -1f64; + // } + + result.push(y.into()); + + theta += step; + } + + Wave::new(result) + } +} + +#[derive(Default)] +pub struct Violin {} + +impl Display for Violin { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "an attempt at the violin instrument") + } +} + +impl Instrument for Violin { + fn play(&mut self, note: T, rate: U, bpm: S, duration: V, volume: R) -> Wave + where + T: Into, + S: Into, + U: Into, + V: Into, + R: Into, + { + let note = note.into(); + let rate = rate.into(); + let duration = duration.into(); + let volume = volume.into(); + + let bpm = bpm.into(); + let duration_per_beat: Seconds = bpm.into(); + let duration: Seconds = (*duration * *duration_per_beat).into(); + + let nb_samples = (*duration * *rate).floor() as usize; + + let note_h: Hertz = note.into(); + + let step = *note_h * 2f64 * PI / *rate; + + let mut theta = 0f64; + + let mut result: Vec = vec![]; + + for i in 0..nb_samples { + let mut y = f64::sin(theta) + + f64::sin(2f64 * theta) + + (3f64 / 4f64) * f64::sin(3f64 * theta) + + (1f64 / 8f64) * f64::sin(4f64 * theta) + + (1f64 / 16f64) * f64::sin(5f64 * theta) + + (1f64 / 32f64) * f64::sin(6f64 * theta) + + (1f64 / 64f64) * f64::sin(7f64 * theta) + + (10f64 / 128f64) * f64::sin(8f64 * theta) + + (1f64 / 256f64) * f64::sin(9f64 * theta) + + (1f64 / 512f64) * f64::sin(10f64 * theta) + + (1f64 / 1024f64) * f64::sin(11f64 * theta) + + (1f64 / 2048f64) * f64::sin(12f64 * theta); + y *= f64::exp((-0.00051f64) * theta); + // y += y * y * y; + y *= 1f64 + 16f64 * i as f64 * f64::exp((-6f64) * i as f64); + + y *= *volume; + + result.push(y.into()); + + theta += step; + } + + Wave::new(result) + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..5fa7e79 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,279 @@ +//! This crate implements a simple utility to make music as pure +//! numbers. + +use std::{ + fmt::{self, Display, Formatter}, + ops::{Deref, DerefMut}, +}; + +macro_rules! deftype { + ($($x:ident),*) => { + $( + #[derive(Debug, Copy, Clone)] + pub struct $x(f64); + + impl From for $x { + fn from(n: f64) -> Self { + Self(n) + } + } + + impl Deref for $x { + type Target = f64; + + fn deref(&self) -> &Self::Target { + &self.0 + } + } + + impl DerefMut for $x { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } + } + )* + }; +} + +deftype!(Volume, Samples, Hertz, Seconds, Pulse, BPM, Beats, Semitones); + +impl Display for Semitones { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + let translated = **self + 9f64; + + let quotient = translated.div_euclid(12f64) as i64; + + let octave_string = (quotient + 4).to_string(); + + let remainder = translated.rem_euclid(12f64) as i64; + + // Due to round-off error, there is the possibility that the + // remainder is equal to the divider's absolute value, + // according to the official documentation, so one more + // conversion is necessary. + let remainder = if remainder == 12i64 { 0i64 } else { remainder }; + + let note_string = match remainder { + 0i64 => String::from("C"), + 1i64 => String::from("C#"), + 2i64 => String::from("D"), + 3i64 => String::from("D#"), + 4i64 => String::from("E"), + 5i64 => String::from("F"), + 6i64 => String::from("F#"), + 7i64 => String::from("G"), + 8i64 => String::from("G#"), + 9i64 => String::from("A"), + 10i64 => String::from("A#"), + 11i64 => String::from("B"), + _ => panic!("remainder not in the range [0, 11]: {remainder}"), + }; + + write!(f, "{note_string}{octave_string}") + } +} + +#[allow(clippy::derivable_impls)] +impl Default for Pulse { + fn default() -> Self { + Self(f64::default()) + } +} + +#[derive(Debug, Clone, Default)] +pub struct Wave { + pulses: Vec, +} + +impl Wave { + /// Construct a new wave + pub fn new(pulses: Vec) -> Self { + Self { pulses } + } + + /// Return the vector contained inside it + pub fn to_vec(self) -> Vec { + self.pulses + } +} + +#[allow(dead_code)] +fn mix_waves_no_divide(waves: Vec) -> Wave { + let mut index = 0; + + let mut result_pulses = Vec::new(); + + loop { + let mut count = 0usize; + let mut local_result = 0f64; + + for wave in waves.iter() { + if index < wave.len() { + local_result += *wave[index]; + count += 1; + } + } + + match count { + 0 => { + break; + } + _ => { + result_pulses.push(local_result.into()); + } + } + + index += 1; + } + + Wave::new(result_pulses) +} + +#[allow(dead_code)] +fn mix_waves_exact(waves: Vec) -> Wave { + let mut index = 0; + + let mut result_pulses = Vec::new(); + + let waves_len = waves.len(); + + loop { + let mut count = 0usize; + let mut local_result = 0f64; + + for wave in waves.iter() { + if index < wave.len() { + local_result += *wave[index]; + count += 1; + } + } + + match count { + 0 => { + break; + } + _ => { + result_pulses.push((local_result / waves_len as f64).into()); + } + } + + index += 1; + } + + Wave::new(result_pulses) +} + +#[allow(dead_code)] +fn mix_waves(waves: Vec) -> Wave { + let mut index = 0; + + let mut result_pulses = Vec::new(); + + loop { + let mut count = 0usize; + let mut local_result = 0f64; + + for wave in waves.iter() { + if index < wave.len() { + local_result += *wave[index]; + count += 1; + } + } + + match count { + 0 => { + break; + } + 1 => { + result_pulses.push(local_result.into()); + } + _ => { + result_pulses.push((local_result / count as f64).into()); + } + } + + index += 1; + } + + Wave::new(result_pulses) +} + +impl Deref for Wave { + type Target = [Pulse]; + + fn deref(&self) -> &Self::Target { + &self.pulses + } +} + +// The twelveth root of two. +const STD_BASE: Hertz = Hertz(1.059463094359295f64); + +impl Default for Volume { + fn default() -> Self { + 1f64.into() + } +} + +impl Default for BPM { + fn default() -> Self { + 120f64.into() + } +} + +impl Default for Beats { + fn default() -> Self { + 1f64.into() + } +} + +impl From for Seconds { + fn from(b: BPM) -> Self { + (60f64 / *b).into() + } +} + +impl Default for Samples { + fn default() -> Self { + 48000f64.into() + } +} + +impl Default for Hertz { + fn default() -> Self { + 440f64.into() + } +} + +impl From for Hertz { + fn from(st: Semitones) -> Self { + (*Self::default() * STD_BASE.powf(*st)).into() + } +} + +pub mod instruments; +pub mod output; +pub mod sheet; + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_stoh() { + let st: Semitones = 1f64.into(); + + let hertz: Hertz = st.into(); + + println!("hertz = {hertz:?}"); + } + + #[test] + fn test_btos() { + let b = BPM::default(); + + let s: Seconds = b.into(); + + println!("second = {s:?}"); + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..ef4b93f --- /dev/null +++ b/src/main.rs @@ -0,0 +1,214 @@ +use rumu::{self, output::Output}; +use std::env; +use std::path::PathBuf; + +#[derive(Debug)] +struct MainError { + mes: &'static str, +} + +impl std::fmt::Display for MainError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "main error: {}", self.mes) + } +} + +impl std::error::Error for MainError {} + +impl From<&'static str> for MainError { + fn from(mes: &'static str) -> Self { + Self { mes } + } +} + +fn get_root() -> std::io::Result { + let path = env::current_dir()?; + let path_ancestors = path.as_path().ancestors(); + + for p in path_ancestors { + let has_cargo_p = std::fs::read_dir(p)? + .into_iter() + .any(|p| p.unwrap().file_name() == std::ffi::OsString::from("Cargo.toml")); + + if has_cargo_p { + return Ok(PathBuf::from(p)); + } + } + + Err(std::io::Error::new( + std::io::ErrorKind::NotFound, + "Cannot find project root", + )) +} + +fn main() -> Result<(), Box> { + let args: Vec<_> = env::args().skip(1).collect(); + + if args.iter().any(|string| string as &str == "-h") { + println!( + "Usage: cargo run [--features ffmpeg] -- [-h] [sheet filename] [output filename] [encoder]" + ); + + println!(); + + let prefix = " ".repeat(7); + + println!("{prefix}-h: just print this usage and exit"); + + println!(); + + println!("{prefix}sheet filename \t the file to read the sheet from"); + println!("{prefix}output filename \t the name of the output audio file"); + println!("{prefix}encoder \t\t the encoder used to encode the output"); + + println!("{}", "-".repeat(89)); + + println!("{prefix}To skip a paramter, use \"-\" in its place"); + + return Ok(()); + } + + let mut sheet_name = "Ievan Polkka".to_owned(); + + let mut output_name = "Ievan Polkka.opus".to_owned(); + + let mut encoder = if cfg!(feature = "ffmpeg") { + "opus" + } else { + "plain" + }; + + match args.len() { + 0 => {} + 1 => { + if args[0] != "-" { + sheet_name = args[0].to_owned(); + output_name = format!("{sheet_name}.opus"); + } + } + 2 => { + if args[0] != "-" { + sheet_name = args[0].to_owned(); + } + + if args[1] != "-" { + output_name = args[1].to_owned(); + } else { + output_name = format!("{sheet_name}.opus"); + } + } + 3 => { + if args[0] != "-" { + sheet_name = args[0].to_owned(); + } + + let mut output_not_specified = false; + + if args[1] != "-" { + output_name = args[1].to_owned(); + } else { + output_not_specified = true; + output_name = format!("{sheet_name}.{encoder}"); + } + + if args[2] != "-" { + encoder = &args[2]; + + if output_not_specified { + output_name = format!("{sheet_name}.{encoder}"); + } + } + } + _ => { + println!( + "Usage: cargo run [--features ffmpeg] -- \ + [-h] [sheet filename] [output filename] [encoder]" + ); + + println!(); + + let prefix = " ".repeat(7); + + println!("{prefix}-h: just print this usage and exit"); + + println!(); + + println!("{prefix}sheet filename \t the file to read the sheet from"); + println!("{prefix}output filename \t the name of the output audio file"); + println!("{prefix}encoder \t\t the encoder used to encode the output"); + + println!("{}", "-".repeat(89)); + + println!("{prefix}To skip a paramter, use \"-\" in its place"); + + std::process::exit(1); + } + } + + let project_root = get_root()?.to_str().unwrap().to_owned(); + + output_name = format!("{project_root}/audio files/{output_name}"); + + let source = std::fs::read_to_string(format!("{project_root}/songs/{sheet_name}.rumu"))?; + + let mut sheet: rumu::sheet::Sheet = source.parse()?; + + let wave: rumu::Wave = (&mut sheet).into(); + + let rate: rumu::Samples = 44100f64.into(); + + match encoder { + "opus" => { + if !cfg!(feature = "ffmpeg") { + return Err( + "To use the opus encoder one has to enable the \"ffmpeg\" feature.".into(), + ); + } + + #[cfg(feature = "ffmpeg")] + { + let output = rumu::output::ffmpeg_output::OpusOutput::default(); + + output.save(wave, rate, &output_name)?; + } + } + "mp3" => { + if !cfg!(feature = "ffmpeg") { + return Err( + "To use the mp3 encoder one has to enable the \"ffmpeg\" feature.".into(), + ); + } + + #[cfg(feature = "ffmpeg")] + { + let output = rumu::output::ffmpeg_output::MP3Output::default(); + + output.save(wave, rate, &output_name)?; + } + } + "aac" => { + if !cfg!(feature = "ffmpeg") { + return Err( + "To use the aac encoder one has to enable the \"ffmpeg\" feature.".into(), + ); + } + + #[cfg(feature = "ffmpeg")] + { + let output = rumu::output::ffmpeg_output::AACOutput::default(); + + output.save(wave, rate, &output_name)?; + } + } + "plain" => { + let output = rumu::output::PlainOutput::default(); + + output.save(wave, rate, &output_name)?; + } + _ => { + return Err("Unrecognized encoder: {encoder}".into()); + } + } + + Ok(()) +} diff --git a/src/output/ffmpeg_output.rs b/src/output/ffmpeg_output.rs new file mode 100644 index 0000000..2132394 --- /dev/null +++ b/src/output/ffmpeg_output.rs @@ -0,0 +1,417 @@ +//! This file implements the output to save an audio file using ffmpeg +//! libraries. + +use super::*; + +#[allow(unused_imports)] +use std::{fs::File, time::Duration}; + +#[allow(unused_imports)] +use ac_ffmpeg::{ + codec::{ + audio::{ + self, + frame::{ChannelLayout, SampleFormat}, + AudioEncoder, AudioFrameMut, + }, + CodecParameters, Encoder, + }, + format::{ + io::IO, + muxer::{Muxer, OutputFormat}, + }, + time::{TimeBase, Timestamp}, + Error as FFError, +}; + +impl From for OutputError { + fn from(ff: FFError) -> Self { + OutputError::FFMpeg(ff.to_string()) + } +} + +/// Open a given output file. +fn open_output(path: &str, elementary_streams: &[CodecParameters]) -> Result, FFError> { + let output_format = OutputFormat::guess_from_file_name(path) + .ok_or_else(|| FFError::new(format!("unable to guess output format for file: {}", path)))?; + + let output = std::fs::OpenOptions::new() + .create(true) + .write(true) + .open(path) + .map_err(|err| FFError::new(format!("unable to create output file {}: {}", path, err)))?; + + let io = IO::from_seekable_write_stream(output); + + let mut muxer_builder = Muxer::builder(); + + for codec_parameters in elementary_streams { + muxer_builder.add_stream(codec_parameters)?; + } + + muxer_builder.build(io, output_format) +} + +/// A dummy struct to hold a generic function for saving waves. +#[derive(Default)] +pub struct OpusOutput {} + +impl Output for OpusOutput { + fn save(&self, data: Wave, rate: Samples, name: &str) -> Result<(), OutputError> { + if data.is_empty() { + dbg!(); + return Err(OutputError::EmptyWaves); + } + + if std::fs::metadata(name).is_ok() { + std::fs::remove_file(name)?; + } + + let sample_format: SampleFormat = "flt".parse().unwrap(); + + let channel_layout = ChannelLayout::from_channels(2u32).unwrap(); + + let encoder_builder = AudioEncoder::builder("libopus")?; + + let time_base = TimeBase::new(1, 50); + + let duration = Duration::from_nanos((data.len() * *rate as usize) as u64); + + let mut encoder = encoder_builder + .sample_rate(*rate as u32) + .sample_format(sample_format) + .channel_layout(channel_layout) + .bit_rate(96000u64) + .time_base(time_base) + .build()?; + + let codec_parameters = encoder.codec_parameters().into(); + + let mut muxer = open_output(name, &[codec_parameters])?; + + // NOTE: FFMPEG assumes the bytes to be laid out in the + // platform-natural endianness, so this is actually portable, + // in the sense that this must be used with FFMPEG, which + // knows how to deal with this endianness. + let bytes: Vec = data + .iter() + .flat_map(|pulse| (**pulse as f32).to_ne_bytes()) + .collect(); + + let mut frame_idx = 0; + let mut frame_timestamp = Timestamp::new(frame_idx, time_base); + let max_timestamp = Timestamp::from_millis(0) + duration; + + // NOTE: Each frame occupies 20 milliseconds, as recommended + // in the official documentation page for FFMPEG. + let samples_per_frame = (*rate * 0.02f64) as usize; + + let mut bytes_index = 0usize; + + while frame_timestamp < max_timestamp && bytes_index < bytes.len() { + let mut frame = AudioFrameMut::silence( + channel_layout, + sample_format, + *rate as u32, + samples_per_frame, + ) + .with_time_base(time_base) + .with_pts(frame_timestamp); + + let mut planes = frame.planes_mut(); + + let data = planes[0].data_mut(); + + let number_of_bytes = std::cmp::min(samples_per_frame, (bytes.len() - bytes_index) / 4); + + for i in 0..number_of_bytes { + data[i * 8] = bytes[bytes_index + 4 * i]; + data[i * 8 + 1] = bytes[bytes_index + 4 * i + 1]; + data[i * 8 + 2] = bytes[bytes_index + 4 * i + 2]; + data[i * 8 + 3] = bytes[bytes_index + 4 * i + 3]; + data[i * 8 + 4] = bytes[bytes_index + 4 * i]; + data[i * 8 + 5] = bytes[bytes_index + 4 * i + 1]; + data[i * 8 + 6] = bytes[bytes_index + 4 * i + 2]; + data[i * 8 + 7] = bytes[bytes_index + 4 * i + 3]; + } + + let frame = frame.freeze(); + + encoder.push(frame)?; + + while let Some(packet) = encoder.take()? { + muxer.push(packet.with_stream_index(0))?; + } + + frame_idx += 1; + + frame_timestamp = Timestamp::new(frame_idx, time_base); + + bytes_index += samples_per_frame * 4; + } + + encoder.flush()?; + + while let Some(packet) = encoder.take()? { + muxer.push(packet.with_stream_index(0))?; + } + + muxer.flush()?; + + Ok(()) + } +} + +/// A dummy struct to hold a generic function for saving waves. +#[derive(Default)] +pub struct MP3Output {} + +impl Output for MP3Output { + fn save(&self, data: Wave, rate: Samples, name: &str) -> Result<(), OutputError> { + if data.is_empty() { + dbg!(); + return Err(OutputError::EmptyWaves); + } + + if std::fs::metadata(name).is_ok() { + std::fs::remove_file(name)?; + } + + let sample_format: SampleFormat = "fltp".parse().unwrap(); + + let channel_layout = ChannelLayout::from_channels(2u32).unwrap(); + + let encoder_builder = AudioEncoder::builder("libmp3lame")?; + + let time_base = TimeBase::new(1, 50); + + let duration = Duration::from_nanos((data.len() * *rate as usize) as u64); + + let mut encoder = encoder_builder + .sample_rate(*rate as u32) + .sample_format(sample_format) + .channel_layout(channel_layout) + .bit_rate(96000u64) + .time_base(time_base) + .build()?; + + let codec_parameters = encoder.codec_parameters().into(); + + let mut muxer = open_output(name, &[codec_parameters])?; + + // NOTE: FFMPEG assumes the bytes to be laid out in the + // platform-natural endianness, so this is actually portable, + // in the sense that this must be used with FFMPEG, which + // knows how to deal with this endianness. + let bytes: Vec = data + .iter() + .flat_map(|pulse| (**pulse as f32).to_ne_bytes()) + .collect(); + + let mut frame_idx = 0; + let mut frame_timestamp = Timestamp::new(frame_idx, time_base); + let max_timestamp = Timestamp::from_millis(0) + duration; + + // NOTE: Each frame occupies 20 milliseconds, as recommended + // in the official documentation page for FFMPEG. + let samples_per_frame = (*rate * 0.02f64) as usize; + + let mut bytes_index = 0usize; + + while frame_timestamp < max_timestamp && bytes_index < bytes.len() { + let mut frame = AudioFrameMut::silence( + channel_layout, + sample_format, + *rate as u32, + samples_per_frame, + ) + .with_time_base(time_base) + .with_pts(frame_timestamp); + + let mut planes = frame.planes_mut(); + + let number_of_bytes = std::cmp::min(samples_per_frame, (bytes.len() - bytes_index) / 4); + + for i in 0..number_of_bytes { + planes[0].data_mut()[i * 4] = bytes[bytes_index + 4 * i]; + planes[0].data_mut()[i * 4 + 1] = bytes[bytes_index + 4 * i + 1]; + planes[0].data_mut()[i * 4 + 2] = bytes[bytes_index + 4 * i + 2]; + planes[0].data_mut()[i * 4 + 3] = bytes[bytes_index + 4 * i + 3]; + planes[1].data_mut()[i * 4] = bytes[bytes_index + 4 * i]; + planes[1].data_mut()[i * 4 + 1] = bytes[bytes_index + 4 * i + 1]; + planes[1].data_mut()[i * 4 + 2] = bytes[bytes_index + 4 * i + 2]; + planes[1].data_mut()[i * 4 + 3] = bytes[bytes_index + 4 * i + 3]; + } + + let frame = frame.freeze(); + + encoder.push(frame)?; + + while let Some(packet) = encoder.take()? { + muxer.push(packet.with_stream_index(0))?; + } + + frame_idx += 1; + + frame_timestamp = Timestamp::new(frame_idx, time_base); + + bytes_index += samples_per_frame * 4; + } + + encoder.flush()?; + + while let Some(packet) = encoder.take()? { + muxer.push(packet.with_stream_index(0))?; + } + + muxer.flush()?; + + Ok(()) + } +} + +/// A dummy struct to hold a generic function for saving waves. +#[derive(Default)] +pub struct AACOutput {} + +impl Output for AACOutput { + fn save(&self, data: Wave, rate: Samples, name: &str) -> Result<(), OutputError> { + if data.is_empty() { + dbg!(); + return Err(OutputError::EmptyWaves); + } + + if std::fs::metadata(name).is_ok() { + std::fs::remove_file(name)?; + } + + let sample_format: SampleFormat = "fltp".parse().unwrap(); + + let channel_layout = ChannelLayout::from_channels(2u32).unwrap(); + + let encoder_builder = AudioEncoder::builder("aac")?; + + let time_base = TimeBase::new(1, 50); + + let duration = Duration::from_nanos((data.len() * *rate as usize) as u64); + + let mut encoder = encoder_builder + .sample_rate(*rate as u32) + .sample_format(sample_format) + .channel_layout(channel_layout) + .bit_rate(96000u64) + .time_base(time_base) + .build()?; + + let codec_parameters = encoder.codec_parameters().into(); + + let mut muxer = open_output(name, &[codec_parameters])?; + + // NOTE: FFMPEG assumes the bytes to be laid out in the + // platform-natural endianness, so this is actually portable, + // in the sense that this must be used with FFMPEG, which + // knows how to deal with this endianness. + let bytes: Vec = data + .iter() + .flat_map(|pulse| (**pulse as f32).to_ne_bytes()) + .collect(); + + let mut frame_idx = 0; + let mut frame_timestamp = Timestamp::new(frame_idx, time_base); + let max_timestamp = Timestamp::from_millis(0) + duration; + + // NOTE: Each frame occupies 20 milliseconds, as recommended + // in the official documentation page for FFMPEG. + let samples_per_frame = (*rate * 0.02f64) as usize; + + let mut bytes_index = 0usize; + + while frame_timestamp < max_timestamp && bytes_index < bytes.len() { + let mut frame = AudioFrameMut::silence( + channel_layout, + sample_format, + *rate as u32, + samples_per_frame, + ) + .with_time_base(time_base) + .with_pts(frame_timestamp); + + let mut planes = frame.planes_mut(); + + let number_of_bytes = std::cmp::min(samples_per_frame, (bytes.len() - bytes_index) / 4); + + for i in 0..number_of_bytes { + planes[0].data_mut()[i * 4] = bytes[bytes_index + 4 * i]; + planes[0].data_mut()[i * 4 + 1] = bytes[bytes_index + 4 * i + 1]; + planes[0].data_mut()[i * 4 + 2] = bytes[bytes_index + 4 * i + 2]; + planes[0].data_mut()[i * 4 + 3] = bytes[bytes_index + 4 * i + 3]; + planes[1].data_mut()[i * 4] = bytes[bytes_index + 4 * i]; + planes[1].data_mut()[i * 4 + 1] = bytes[bytes_index + 4 * i + 1]; + planes[1].data_mut()[i * 4 + 2] = bytes[bytes_index + 4 * i + 2]; + planes[1].data_mut()[i * 4 + 3] = bytes[bytes_index + 4 * i + 3]; + } + + let frame = frame.freeze(); + + encoder.push(frame)?; + + while let Some(packet) = encoder.take()? { + muxer.push(packet.with_stream_index(0))?; + } + + frame_idx += 1; + + frame_timestamp = Timestamp::new(frame_idx, time_base); + + bytes_index += samples_per_frame * 4; + } + + encoder.flush()?; + + while let Some(packet) = encoder.take()? { + muxer.push(packet.with_stream_index(0))?; + } + + muxer.flush()?; + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_ffmpeg_format() { + let _output_format = OutputFormat::guess_from_file_name("test.opus").unwrap(); + let encoder = AudioEncoder::builder("libopus").unwrap(); + let format: SampleFormat = "flt".parse().unwrap(); + let channel_layout = ChannelLayout::from_channels(2u32).unwrap(); + + encoder + .sample_rate(48000u32) + .sample_format(format) + .channel_layout(channel_layout) + .bit_rate(96000u64) + .time_base(TimeBase::new(1, 25)) + .build() + .unwrap(); + + assert!(!format.is_planar()); + + let mut frame = AudioFrameMut::silence(channel_layout, format, 48000u32, 960); + + let mut planes = frame.planes_mut(); + + println!("number of planes: {}", planes.len()); + + let data = planes[0].data_mut(); + + println!("frame size = {}", data.len()); + + data[0] = 16u8; + + println!("Successfully changed a byte of the silent frame!"); + } +} diff --git a/src/output/mod.rs b/src/output/mod.rs new file mode 100644 index 0000000..520a500 --- /dev/null +++ b/src/output/mod.rs @@ -0,0 +1,68 @@ +//! This file implements the interface to save waves into a file. + +use std::{ + error::Error, + fmt::{self, Display, Formatter}, + io::{self, Write}, +}; + +use super::*; + +#[derive(Debug)] +pub enum OutputError { + Io(std::io::Error), + FFMpeg(String), + EmptyWaves, +} + +impl Display for OutputError { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + Self::Io(e) => write!(f, "io: {e}"), + Self::EmptyWaves => write!(f, "empty waves"), + OutputError::FFMpeg(s) => write!(f, "FFMPEG error: {s}"), + } + } +} + +impl Error for OutputError {} + +impl From for OutputError { + fn from(io: io::Error) -> Self { + Self::Io(io) + } +} + +/// A type that implements this trait implements a generic function +/// for saving waves into files. +pub trait Output { + /// Save waves DATA with RATE samples per second into a file with + /// NAME. + fn save(&self, data: Wave, rate: Samples, name: &str) -> Result<(), OutputError>; +} + +/// A dummy struct to hold a generic function for saving waves. +#[derive(Default)] +pub struct PlainOutput {} + +impl Output for PlainOutput { + fn save(&self, data: Wave, _rate: Samples, name: &str) -> Result<(), OutputError> { + if data.is_empty() { + return Err(OutputError::EmptyWaves); + } + + let bytes: Vec = data.iter().flat_map(|pulse| pulse.to_le_bytes()).collect(); + + let mut file = std::fs::OpenOptions::new() + .create(true) + .write(true) + .open(name)?; + + file.write_all(&bytes)?; + + Ok(()) + } +} + +#[cfg(feature = "ffmpeg")] +pub mod ffmpeg_output; diff --git a/src/sheet/mod.rs b/src/sheet/mod.rs new file mode 100644 index 0000000..581301c --- /dev/null +++ b/src/sheet/mod.rs @@ -0,0 +1,1905 @@ +#![deny(missing_docs)] +//! This file implements a simple recursive-descent parser for parsing +//! plain text music sheets into some intermediate data structure that +//! can later generate the waves to save to an audio file. +//! +//! The format of the sheet can be described as follows. +//! +//! # 1. Assignments +//! +//! Read a string "\"name\"" followed by a block +//! +//! (assign the block to the name) +//! +//! # 2. Blocks +//! +//! Read block+ enclosed in "{}", nor not, if there is only one block +//! +//! ## 2.1. Groups +//! +//! Read group+ enclosed in "()" or not +//! +//! ### 2.1.1. Unit +//! +//! Read unit* +//! +//! #### 2.1.1.1. Instruction +//! +//! Read an instruction starting with an "i" +//! +//! ##### 2.1.1.1.1. Volume +//! +//! Read a volume "vDIGITS(\.DIGITS)?" +//! +//! ##### 2.1.1.1.2. BPM +//! +//! Read a BPM "bDIGITS" +//! +//! ##### 2.1.1.1.3. Instrument +//! +//! Read an instrument "ipiano"/"iviolin". +//! +//! #### 2.1.1.2. Silence +//! +//! Read a silence "rDIGITS\.*" +//! +//! #### 2.1.1.3. Chord +//! +//! Read a chord = tone (/ tone)* +//! +//! ##### 2.1.1.3.1. Octave +//! +//! Read an octave "(oDIGITS)?|\[><\]*" +//! +//! ##### 2.1.1.3.2. Semitone +//! +//! Read a semitone "\[a-g\](\+|\-)*" +//! +//! ##### 2.1.1.3.3. duration +//! +//! Read a duration "DIGITS\.*|pDIGITS(\.DIGITS)?" +//! +//! ### 2.1.2 Repetition specification +//! +//! Read either a [duration](#21133-duration), to specify a cram +//! expression, and/or "*DIGITS" to specify a repetition of DIGITS times. +//! +//! # 3. name +//! +//! Read a name "\[name\]" that has an assigned value + +use super::*; +use std::error::Error; + +pub mod play; + +/// Possibilities of an instrument. +/// +/// Right now only the piano is usable, whereas the violin is but an +/// experiment. +#[derive(Debug, Clone, Copy, Default, Eq, PartialEq)] +pub enum Instrument { + #[default] + /// A piano instrument + Piano, + /// An experimental violin instrument + Violin, + /// Pure Sinus instrument + Sinus, + /// Pure Tangent instrument + Tangent, +} + +impl Display for Instrument { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + Instrument::Piano => write!(f, "piano"), + Instrument::Violin => write!(f, "violin"), + Instrument::Sinus => write!(f, "sinus"), + Instrument::Tangent => write!(f, "tangent"), + } + } +} + +/// An instruction changing some setting. +/// +/// It changes one of the following: +/// +/// 1. the volume +/// +/// 2. the bpm +/// +/// 3. the instrument +#[derive(Debug, Copy, Clone)] +pub enum Instruction { + /// Change volume + VolumeTo(Volume), + /// Change beats per minute + BPMTo(BPM), + /// Change instrument + InstrumentTo(Instrument), +} + +impl From for Instruction { + fn from(volume: Volume) -> Self { + Self::VolumeTo(volume) + } +} + +impl From for Instruction { + fn from(bpm: BPM) -> Self { + Self::BPMTo(bpm) + } +} + +impl From for Instruction { + fn from(instrument: Instrument) -> Self { + Self::InstrumentTo(instrument) + } +} + +impl Display for Instruction { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + Instruction::VolumeTo(v) => write!(f, "volume => {}", **v), + Instruction::BPMTo(bpm) => write!(f, "BPM => {}", **bpm), + Instruction::InstrumentTo(instrument) => write!(f, "Instrument => {instrument}"), + } + } +} + +/// Represents a semitone +#[derive(Debug, Copy, Clone, Eq, PartialEq, Default)] +pub struct SheetSemitone(isize); + +impl SheetSemitone { + fn shift(&mut self, shift: isize) { + self.0 += shift; + } +} + +impl Display for SheetSemitone { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +/// Represents an octave that is either absolute or relative. +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub enum SheetOctave { + /// Absolute octave specification + Absolute(usize), + /// Relative octave specification + Relative(isize), +} + +impl From for SheetOctave { + fn from(abs: usize) -> Self { + Self::Absolute(abs) + } +} + +impl From for SheetOctave { + fn from(rel: isize) -> Self { + Self::Relative(rel) + } +} + +impl Display for SheetOctave { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + Self::Absolute(val) => write!(f, "o{val}"), + Self::Relative(shift) => { + let string = if shift.is_positive() { + str::repeat(">", *shift as usize) + } else { + str::repeat("<", (-*shift) as usize) + }; + + write!(f, "{string}") + } + } + } +} + +impl From<(usize, SheetSemitone)> for Semitones { + fn from((octave, semitone): (usize, SheetSemitone)) -> Self { + let octave_translation = 12 * (octave as isize - 4isize); + + // println!("octave = {octave_translation}"); + // println!("semitone = {}", semitone.0); + + ((semitone.0 + octave_translation - 9) as f64).into() + } +} + +/// Represents a tone and an optional octave. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Default)] +struct SheetTone { + semitone: SheetSemitone, + octave: Option, +} + +impl SheetTone { + fn new(semitone: SheetSemitone, octave: Option) -> Self { + Self { semitone, octave } + } +} + +impl Display for SheetTone { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + let semitone = self.semitone; + + if let Some(octave) = self.octave { + write!(f, "{octave} {semitone}") + } else { + write!(f, "{semitone}") + } + } +} + +impl From for SheetTone +where + T: Into, +{ + fn from(semitone: T) -> Self { + let semitone = semitone.into(); + let octave = None; + + Self { semitone, octave } + } +} + +/// A chord is a vector of tones. +#[derive(Debug, Clone, Default)] +pub struct SheetChord(Vec); + +impl Display for SheetChord { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self.0.len() { + 0 => write!(f, ""), + _ => { + let mut iter = self.0.iter(); + let first_tone = *iter.by_ref().take(1).next().unwrap(); + + write!(f, "{first_tone}")?; + + for tone in iter { + write!(f, " / {tone}")?; + } + + Ok(()) + } + } + } +} + +impl From for SheetChord +where + T: Into, +{ + fn from(arg: T) -> Self { + Self(vec![arg.into()]) + } +} + +impl From> for SheetChord +where + T: Into, +{ + fn from(arg: Vec) -> Self { + Self(arg.into_iter().map(Into::into).collect()) + } +} + +/// A duration for a group. +/// +/// This is essentially a float number, except that it also accepts +/// dots. +#[derive(Debug, Copy, Clone, Default)] +pub struct SheetDuration { + beats: Option, + dots: usize, +} + +impl SheetDuration { + /// Return an optional `Beats`. + pub fn beats(&self) -> Option { + self.beats + } + + /// Return the number of dots. + pub fn dots(&self) -> usize { + self.dots + } +} + +impl Display for SheetDuration { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + if let Some(b) = self.beats() { + write!(f, "{}", *b)?; + } + + let dots_str = str::repeat(".", self.dots); + + write!(f, "{dots_str}") + } +} + +impl From for SheetDuration { + fn from(dots: usize) -> Self { + let beats = Default::default(); + Self { beats, dots } + } +} + +impl> From for SheetDuration { + fn from(arg: T) -> Self { + let beats = arg.into(); + let beats = Some(beats); + let dots = Default::default(); + Self { beats, dots } + } +} + +impl> From<(Option, usize)> for SheetDuration { + fn from((arg, dots): (Option, usize)) -> Self { + let beats = arg.map(|b| b.into()); + + Self { beats, dots } + } +} + +/// A unit of the sheet. +/// +/// It is one of the following: +/// +/// 1. an ordinary chord. +/// +/// 2. a silence. +/// +/// 3. an instruction. +#[derive(Debug, Clone)] +pub enum SheetUnit { + /// A combination of a chord and the number of beats. + Tone(SheetChord, SheetDuration), + /// A silence that lasts a number of beats. + Silence(SheetDuration), + /// An [`Instruction`]. + Instruction(Instruction), +} + +impl Default for SheetUnit { + fn default() -> Self { + // The default is silence + let duration: SheetDuration = Default::default(); + duration.into() + } +} + +impl> From for SheetUnit { + fn from(instruction: T) -> Self { + let instruction = instruction.into(); + + Self::Instruction(instruction) + } +} + +impl From for SheetUnit { + fn from(duration: SheetDuration) -> Self { + Self::Silence(duration) + } +} + +impl From for SheetUnit { + fn from(chord: SheetChord) -> Self { + Self::Tone(chord, Default::default()) + } +} + +impl SheetUnit { + /// Adjusting the duration of the chord. + /// + /// # Warning + /// + /// This function does nothing if called on a unit that is not a + /// tone unit. + pub fn set_duration(&mut self, duration: Beats) { + match self { + Self::Tone(chord, _) => { + *self = Self::Tone(chord.clone(), duration.into()); + } + Self::Silence(_) => { + *self = Self::Silence(duration.into()); + } + _ => (), + } + } +} + +impl Display for SheetUnit { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + Self::Tone(chord, duration) => write!(f, "{chord},{duration}"), + Self::Silence(duration) => write!(f, "r{duration}"), + Self::Instruction(instruction) => write!(f, "{instruction}"), + } + } +} + +/// A sheet group node either specifies the number of repetitions or is +/// a unit. +#[derive(Debug, Clone)] +pub enum SheetGroupNode { + /// The number of repetition and optionally the crammed duration. + /// + /// Note that the number of repetition can be zero. + /// + /// The crammed duration, if present, will be used to adjust the + /// durations in the notes. + Intermediate(usize, Option), + /// A unit + Unit(SheetUnit), +} + +impl SheetGroupNode { + /// Set the repetition number of an intermediate node. + /// + /// # Panic + /// + /// This function panics if called on a node that is not an + /// intermediate node. + pub fn set_repetition(&mut self, repetition: usize) { + match self { + Self::Intermediate(_, duration) => { + *self = Self::Intermediate(repetition, *duration); + } + _ => { + panic!("setting intermediate on a unit: {self:?}"); + } + } + } + + /// Set the crammed duration of an intermediate node. + /// + /// # Panic + /// + /// This function panics if called on a node that is not an + /// intermediate node. + pub fn set_cram(&mut self, duration: Option) { + match self { + Self::Intermediate(repetition, _) => { + *self = Self::Intermediate(*repetition, duration); + } + _ => { + panic!("setting intermediate on a unit: {self:?}"); + } + } + } + + /// Adjusting the durations of a node to fit within a crammed + /// duration. + pub fn set_duration(&mut self, duration: Option) { + match self { + Self::Unit(unit) => { + if let Some(duration) = duration { + unit.set_duration(duration); + } + } + SheetGroupNode::Intermediate(repetition, _) => { + *self = SheetGroupNode::Intermediate(*repetition, duration.map(Into::into)); + } + } + } +} + +/// A sheet group is a tree whose leafs are units and whose +/// intermediate nodes specify the number of repetitions. +#[derive(Debug, Clone, Default)] +pub struct SheetGroup { + nodes: Vec, + edges: Vec>, +} + +impl SheetGroup { + /// Construct a new group. + pub fn new(nodes: Vec, edges: Vec>) -> Self { + Self { nodes, edges } + } + + /// Return the node at position N. + pub fn get_node(&self, n: usize) -> &SheetGroupNode { + self.nodes.get(n).unwrap() + } + + /// Return the children of the node at position N. + pub fn get_children(&self, n: usize) -> &[usize] { + self.edges.get(n).unwrap() + } + + /// Adjust durations for each chord in the group. + fn do_cram(&mut self, start_duration: &mut Beats) { + if self.nodes.is_empty() { + return; + } + + // First calculate the durations of each node, so that we can + // find out their respective proportions later on. + + let mut durations_array: Vec = vec![0f64.into(); self.nodes.len()]; + let mut seen = vec![false; self.nodes.len()]; + + let current_duration = start_duration; + + let mut stack = vec![0]; + + let mut cram_result: Vec<(usize, Beats)> = vec![]; + + while let Some(top) = stack.pop() { + match self.nodes[top] { + SheetGroupNode::Intermediate(repetition, Some(cram)) => { + match repetition { + 0 => { + continue; + } + 1 => { + durations_array[top] = (&cram, *current_duration).into(); + } + _ => { + durations_array[top] = (&cram, *current_duration).into(); + + durations_array[top] = + (*durations_array[top] * repetition as f64).into(); + } + } + + for child in self.edges[top].iter().rev() { + stack.push(*child); + } + } + SheetGroupNode::Intermediate(repetition, _) => { + if repetition == 0 { + continue; + } + + if !seen[top] { + stack.push(top); + + for child in self.edges[top].iter().rev().copied() { + stack.push(child); + } + } else { + let summation: f64 = self.edges[top] + .iter() + .map(|node| *durations_array[*node]) + .sum(); + + match repetition { + 0 => unreachable!(), + 1 => { + durations_array[top] = summation.into(); + } + _ => { + durations_array[top] = summation.into(); + + durations_array[top] = + (*durations_array[top] * repetition as f64).into(); + } + } + } + } + SheetGroupNode::Unit(SheetUnit::Tone(_, duration)) => { + let final_duration: Beats = (&duration, *current_duration).into(); + + durations_array[top] = final_duration; + + *current_duration = final_duration; + + cram_result.push((top, final_duration)); + } + SheetGroupNode::Unit(SheetUnit::Silence(duration)) => { + let final_duration: Beats = (&duration, *current_duration).into(); + + durations_array[top] = final_duration; + + *current_duration = final_duration; + + cram_result.push((top, final_duration)); + } + // Instructions have no duration. + _ => {} + } + + seen[top] = true; + } + + for (child, duration) in cram_result { + self.nodes[child].set_duration(Some(duration)); + } + + // Then do a depth first traversal to "cram" the durations, + // based on their respective proportions. + + let mut stack = vec![( + 0, + matches!(self.nodes[0], SheetGroupNode::Intermediate(_, Some(_))), + )]; + + while let Some((top, top_cram_p)) = stack.pop() { + if let SheetGroupNode::Intermediate(repetition, cram) = self.nodes[top] { + if repetition == 0 { + continue; + } + + let new_cram_p = top_cram_p || cram.is_some(); + + let top_duration = *durations_array[top]; + + let total_duration: f64 = self.edges[top] + .iter() + .copied() + .map(|child| *durations_array[child]) + .sum(); + + if new_cram_p { + let mut cram_result: Vec<(usize, Beats)> = vec![]; + + // println!("top = {top}"); + + for child in self.edges[top].iter().copied() { + if matches!( + self.nodes[child], + SheetGroupNode::Unit(SheetUnit::Instruction(_)) + ) { + continue; + } + // println!("child = {child}"); + + let child_duration = *durations_array[child]; + + let mut crammed_duration: Beats = + ((child_duration / total_duration) * top_duration).into(); + + if let SheetGroupNode::Intermediate(repetition, _) = self.nodes[child] { + match repetition { + 0 => { + continue; + } + 1 => { + // nothing to do + } + _ => { + crammed_duration = + (*crammed_duration / repetition as f64).into(); + } + } + } + + cram_result.push((child, crammed_duration)); + } + + for (child, crammed_duration) in cram_result { + self.nodes[child].set_duration(Some(crammed_duration)); + // durations_array[child] = crammed_duration; + } + } + + stack.append( + &mut self.edges[top] + .iter() + .copied() + .filter_map(|child| match self.nodes[child] { + SheetGroupNode::Intermediate(repetition, _) if repetition != 0 => { + Some((child, new_cram_p)) + } + _ => None, + }) + .collect(), + ); + } + } + } +} + +/// A sheet block is a sequence of groups. +/// +/// A block is delimited by curly braces. +#[derive(Debug, Clone, Default)] +pub struct SheetBlock { + groups: Vec, +} + +impl SheetBlock { + /// A plain constructor. + pub fn new(groups: Vec) -> Self { + Self { groups } + } +} + +impl Deref for SheetBlock { + type Target = [SheetGroup]; + + fn deref(&self) -> &Self::Target { + &self.groups + } +} + +/// A sheet consists of a vector of blocks. +/// +/// If a sheet does not have any blocks, a block is automatically +/// inferred to contain the entire sheet. +/// +/// If a sheet has multiple blocks, every block will be played at the +/// same time. +#[derive(Debug, Clone, Default)] +pub struct Sheet { + blocks: Vec, +} + +impl Sheet { + /// A plain constructor. + pub fn new(blocks: Vec) -> Self { + Self { blocks } + } +} + +impl Deref for Sheet { + type Target = [SheetBlock]; + + fn deref(&self) -> &Self::Target { + &self.blocks + } +} + +/// A parsing error. +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum SheetParseError { + /// Empty sheet is not allowed. + EmptySheet, + /// Encounters an invalid character in a comment. + InvalidComment(usize), + /// Expecting a closing block. + HangingBlock(usize), + /// Invalid block. + InvalidBlock(usize), + /// Expecting a closing group. + HangingGroup(usize), + /// Invalid group. + InvalidGroup(usize), + /// Invalid character + InvalidChar(usize), + /// Invalid instruction + InvalidInstruction(usize), + /// Invalid instrument instruction + InvalidInstrument(usize), + /// Invalid volume instruction + InvalidVolume(usize), + /// Invalid bpm instruction + InvalidBPM(usize), + /// Invalid tone + InvalidTone(usize), + /// Invalid octave + InvalidOctave(usize), + /// Invalid duration + InvalidDuration(usize), + /// Invalid repetition + InvalidRepetition(usize), +} + +impl Display for SheetParseError { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + SheetParseError::EmptySheet => write!(f, "found an empty sheet"), + SheetParseError::InvalidComment(pos) => write!(f, "invalid comment at {pos}"), + SheetParseError::HangingBlock(pos) => write!(f, "expecting closing block at {pos}"), + SheetParseError::InvalidBlock(pos) => write!(f, "invalid block at {pos}"), + SheetParseError::HangingGroup(pos) => write!(f, "expecting closing group at {pos}"), + SheetParseError::InvalidGroup(pos) => write!(f, "invalid group at {pos}"), + SheetParseError::InvalidChar(pos) => write!(f, "invalid character at {pos}"), + SheetParseError::InvalidInstruction(pos) => write!(f, "invalid instruction at {pos}"), + SheetParseError::InvalidInstrument(pos) => write!(f, "invalid instrument at {pos}"), + SheetParseError::InvalidVolume(pos) => write!(f, "invalid volume at {pos}"), + SheetParseError::InvalidBPM(pos) => write!(f, "invalid BPM at {pos}"), + SheetParseError::InvalidTone(pos) => write!(f, "invalid tone at {pos}"), + SheetParseError::InvalidOctave(pos) => write!(f, "invalid octave at {pos}"), + SheetParseError::InvalidDuration(pos) => write!(f, "invalid duration at {pos}"), + SheetParseError::InvalidRepetition(pos) => write!(f, "invalid repetition at {pos}"), + } + } +} + +impl Error for SheetParseError {} + +impl SheetParseError { + /// Translate the positions in the error by a certain amount. + pub fn shift_pos(&self, shift_len: usize) -> Self { + match self { + SheetParseError::EmptySheet => self.clone(), + SheetParseError::InvalidComment(pos) => Self::InvalidComment(pos + shift_len), + SheetParseError::HangingBlock(pos) => Self::HangingBlock(pos + shift_len), + SheetParseError::InvalidBlock(pos) => Self::InvalidBlock(pos + shift_len), + SheetParseError::HangingGroup(pos) => Self::HangingGroup(pos + shift_len), + SheetParseError::InvalidGroup(pos) => Self::InvalidGroup(pos + shift_len), + SheetParseError::InvalidChar(pos) => Self::InvalidChar(pos + shift_len), + SheetParseError::InvalidInstruction(pos) => Self::InvalidInstruction(pos + shift_len), + SheetParseError::InvalidInstrument(pos) => Self::InvalidInstrument(pos + shift_len), + SheetParseError::InvalidVolume(pos) => Self::InvalidVolume(pos + shift_len), + SheetParseError::InvalidBPM(pos) => Self::InvalidBPM(pos + shift_len), + SheetParseError::InvalidTone(pos) => Self::InvalidTone(pos + shift_len), + SheetParseError::InvalidOctave(pos) => Self::InvalidOctave(pos + shift_len), + SheetParseError::InvalidDuration(pos) => Self::InvalidDuration(pos + shift_len), + SheetParseError::InvalidRepetition(pos) => Self::InvalidRepetition(pos + shift_len), + } + } +} + +fn skip_by_fn(s: &str, f: fn(char) -> bool) -> usize { + for (index, c) in s.chars().enumerate() { + if !f(c) { + return index; + } + } + + s.len() +} + +fn is_wsp(c: char) -> bool { + c == ' ' || c == '\t' +} + +fn is_wsp_or_newline(c: char) -> bool { + is_wsp(c) || c == '\n' || c == '\r' +} + +fn is_v_char(c: char) -> bool { + ('!'..='~').contains(&c) +} + +fn is_wsp_or_v_char(c: char) -> bool { + is_wsp(c) || is_v_char(c) +} + +// fn is_alpha(c: char) -> bool { +// ('A'..='Z').contains(&c) || ('a'..='z').contains(&c) +// } + +fn is_decimal_digit(c: char) -> bool { + ('0'..='9').contains(&c) +} + +// fn is_binary_digit(c: char) -> bool { +// c == '0' || c == '1' +// } + +// fn is_hexadecimal_digit(c: char) -> bool { +// is_decimal_digit(c) || ('A'..='F').contains(&c) || ('a'..='f').contains(&c) +// } + +// fn is_alpha_or_digit_or_hyphen(c: char) -> bool { +// is_alpha(c) || is_decimal_digit(c) || c == '-' +// } + +// #[allow(unused)] +// fn is_char_not_quote(c: char) -> bool { +// (' '..='~').contains(&c) && c != '"' +// } + +/// Skip comments and newlines. +fn skip_c_nl(s: &str) -> Result, SheetParseError> { + if s.is_empty() { + return Ok(Some(0)); + } + + let s_len = s.len(); + + match s.chars().next().unwrap() { + ';' => { + let after_wsp_and_v_char = skip_by_fn(&s[1..], is_wsp_or_v_char); + + if 1 + after_wsp_and_v_char >= s_len { + Ok(Some(s_len)) + } else { + match s.chars().nth(1 + after_wsp_and_v_char).unwrap() { + '\n' | '\r' => Ok(Some(after_wsp_and_v_char + 2)), + _ => Err(SheetParseError::InvalidComment(1 + after_wsp_and_v_char)), + } + } + } + '\n' | '\r' => Ok(Some(1)), + _ => Ok(None), + } +} + +/// Skip whitespaces, newlines, and comments. +fn skip_wsp_and_newline_and_comments(s: &str) -> Result { + let mut s = s; + let mut continue_skipping = true; + + let mut index = 0; + + while continue_skipping { + continue_skipping = false; + + let skip_wsp = skip_by_fn(s, is_wsp_or_newline); + + if skip_wsp > 0 { + continue_skipping = true; + + index += skip_wsp; + + if skip_wsp >= s.len() { + return Ok(index); + } + + s = &s[skip_wsp..]; + } + + // skip comments + match skip_c_nl(s).map_err(|e| e.shift_pos(index))? { + Some(skipped_size) if skipped_size > 0 => { + continue_skipping = true; + + index += skipped_size; + + if skipped_size >= s.len() { + return Ok(index); + } + + s = &s[skipped_size..]; + } + _ => {} + } + } + + Ok(index) +} + +/// Skip white spaces, tabs, newlines, and comments altogether. +macro_rules! skip_ws { + ($s:ident, $index:ident, $fallback:expr) => { + let skip_result = skip_wsp_and_newline_and_comments($s).map_err(|e| e.shift_pos($index))?; + + $index += skip_result; + + if skip_result >= $s.len() { + $fallback + } else { + $s = &$s[skip_result..]; + } + }; +} + +#[derive(Debug, Copy, Clone, Default)] +struct CramRepetition(Option, Option); + +impl CramRepetition { + fn repetition(&self) -> usize { + self.1.unwrap_or(1usize) + } + + fn cram(&self) -> Option { + self.0 + } + + fn set_cram(&mut self, cram: Option) { + self.0 = cram; + } + + fn set_repetition(&mut self, repetition: Option) { + self.1 = repetition; + } +} + +fn parse_cram_repetition(s: &str) -> Result<(usize, CramRepetition), SheetParseError> { + if s.is_empty() { + return Ok((0, Default::default())); + } + + let mut s = s; + + let mut index = 0; + + let mut result: CramRepetition = Default::default(); + + // The index is zero, so we do not need to translate the error. + let (duration_len, duration) = parse_one_duration(s)?; + + if duration_len != 0 { + index += duration_len; + + result.set_cram(Some(duration)); + + if duration_len >= s.len() { + return Ok((index, result)); + } + + s = &s[duration_len..]; + + skip_ws!(s, index, { + return Ok((index, result)); + }); + } + + if s.starts_with('*') { + index += 1; + s = &s[1..]; + + skip_ws!(s, index, { + return Err(SheetParseError::InvalidRepetition(index)); + }); + + let digits_len = skip_by_fn(s, is_decimal_digit); + + if digits_len != 0 { + index += digits_len; + + let num: usize = s[..digits_len].parse().unwrap(); + + result.set_repetition(Some(num)); + } + } + + Ok((index, result)) +} + +fn parse_one_instruction(s: &str) -> Result<(usize, SheetUnit), SheetParseError> { + if s.is_empty() { + return Err(SheetParseError::InvalidInstruction(0)); + } + + let mut s = s; + let mut index = 0; + + if s.starts_with('i') { + index += 1; + s = &s[1..]; + + if s.starts_with("piano") { + index += 5; + + Ok((index, Instrument::Piano.into())) + } else if s.starts_with("violin") { + index += 6; + + Ok((index, Instrument::Violin.into())) + } else if s.starts_with("sinus") { + index += 5; + + Ok((index, Instrument::Sinus.into())) + } else if s.starts_with("tangent") { + index += 7; + + Ok((index, Instrument::Tangent.into())) + } else { + Err(SheetParseError::InvalidInstrument(index)) + } + } else if s.starts_with('b') { + index += 1; + s = &s[1..]; + + let digits_len = skip_by_fn(s, is_decimal_digit); + + if digits_len == 0 { + return Err(SheetParseError::InvalidBPM(index)); + } + + // This should never fail because the string is supposed to + // contain only decimal digits. + let num: usize = (s[..digits_len]).parse().unwrap(); + + if num == 0 { + return Err(SheetParseError::InvalidBPM(index)); + } + + index += digits_len; + + let bpm: BPM = (num as f64).into(); + + Ok((index, bpm.into())) + } else if s.starts_with('v') { + index += 1; + s = &s[1..]; + + let digits_len = skip_by_fn(s, is_decimal_digit); + + let num: usize = if digits_len == 0 { + 0 + } else { + // This should never fail because the string is supposed + // to contain only decimal digits. + (s[..digits_len]).parse().unwrap() + }; + + let mut frac: usize = 0; + + index += digits_len; + s = &s[digits_len..]; + + if s.starts_with('.') { + index += 1; + s = &s[1..]; + + let frac_digits_len = skip_by_fn(s, is_decimal_digit); + + if frac_digits_len != 0 { + index += frac_digits_len; + + frac = (s[..frac_digits_len]).parse().unwrap(); + } else if digits_len == 0 { + return Err(SheetParseError::InvalidVolume(index)); + } + } + + // This should never fail + let volume: f64 = format!("{num}.{frac}").parse().unwrap(); + + let volume: Volume = volume.into(); + + Ok((index, volume.into())) + } else { + Err(SheetParseError::InvalidInstruction(index)) + } +} + +// This never fails, since a silence "r" cannot determine that the +// following is invalid. +fn parse_one_silence(s: &str) -> (usize, SheetUnit) { + if s.is_empty() { + return (0, Default::default()); + } + + let mut s = s; + let mut index = 0; + + let mut beats: Option = None; + + let digits_len = skip_by_fn(s, is_decimal_digit); + + if digits_len != 0 { + // this will not fail + let num: usize = s[..digits_len].parse().unwrap(); + + let beats_beats: Beats = (1f64 / num as f64).into(); + + beats = Some(beats_beats); + + if digits_len >= s.len() { + let duration: SheetDuration = beats_beats.into(); + return (index + digits_len, duration.into()); + } + + index += digits_len; + s = &s[digits_len..]; + } + + let dots_len = skip_by_fn(s, |c| c == '.'); + + index += dots_len; + + let duration: SheetDuration = (beats, dots_len).into(); + + (index, duration.into()) +} + +fn parse_one_duration(s: &str) -> Result<(usize, SheetDuration), SheetParseError> { + if s.is_empty() { + return Ok((0, Default::default())); + } + + let mut s = s; + + let mut index = 0; + + if s.starts_with('p') { + // a precise specification + + index += 1; + s = &s[1..]; + + let digits_len = skip_by_fn(s, is_decimal_digit); + + if digits_len == 0 { + return Err(SheetParseError::InvalidDuration(index)); + } + + index += digits_len; + + let num: usize = s[..digits_len].parse().unwrap(); + + if digits_len >= s.len() { + return Ok((index, (num as f64).into())); + } + + s = &s[digits_len..]; + + let mut frac: usize = 0; + + if s.starts_with('.') { + index += 1; + s = &s[1..]; + + let frac_len = skip_by_fn(s, is_decimal_digit); + + index += frac_len; + + if frac_len != 0 { + frac = s[..frac_len].parse().unwrap(); + } + } + + let duration_f64: f64 = format!("{num}.{frac}").parse().unwrap(); + + Ok((index, duration_f64.into())) + } else { + let digits_len = skip_by_fn(s, is_decimal_digit); + + let mut duration_f64 = None; + + let mut dots_len = 0; + + if digits_len != 0 { + index += digits_len; + + let num: usize = s[..digits_len].parse().unwrap(); + + duration_f64 = Some(1f64 / num as f64); + + if digits_len < s.len() { + s = &s[digits_len..]; + } + } + + if s.starts_with('.') { + dots_len = skip_by_fn(s, |c| c == '.'); + + index += dots_len; + } + + Ok((index, (duration_f64, dots_len).into())) + } +} + +/// Parse a chord. +/// +/// # Panic +/// +/// It assumes that the string starts with a valid letter. If the +/// assumption is violated, this function panics. +fn parse_one_chord(s: &str) -> Result<(usize, SheetUnit), SheetParseError> { + if s.is_empty() { + return Err(SheetParseError::InvalidTone(0)); + } + + let mut s = s; + let mut index = 0; + + let mut tones: Vec = Vec::new(); + + let mut sheet_duration = SheetDuration::default(); + + loop { + let mut octave: Option = None; + + if s.starts_with('o') { + index += 1; + s = &s[1..]; + + let digits_len = skip_by_fn(s, is_decimal_digit); + + if digits_len == 0 { + return Err(SheetParseError::InvalidOctave(index)); + } else if digits_len >= s.len() { + return Err(SheetParseError::InvalidTone(index + digits_len)); + } + + let num: usize = s[..digits_len].parse().unwrap(); + octave = Some(num.into()); + + index += digits_len; + s = &s[digits_len..]; + } else if s.starts_with('<') { + let less_len = skip_by_fn(s, |c| c == '<'); + + if less_len >= s.len() { + return Err(SheetParseError::InvalidTone(index + less_len)); + } + + octave = Some((-(less_len as isize)).into()); + + index += less_len; + s = &s[less_len..]; + } else if s.starts_with('>') { + let greater_len = skip_by_fn(s, |c| c == '>'); + + if greater_len >= s.len() { + return Err(SheetParseError::InvalidTone(index + greater_len)); + } + + octave = Some((greater_len as isize).into()); + + index += greater_len; + s = &s[greater_len..]; + } + + skip_ws!(s, index, { + return Err(SheetParseError::InvalidTone(index)); + }); + + let first_char = s.chars().next().unwrap(); + + let mut tone = match first_char { + 'c' | 'C' => SheetSemitone(0isize), + 'd' | 'D' => SheetSemitone(2isize), + 'e' | 'E' => SheetSemitone(4isize), + 'f' | 'F' => SheetSemitone(5isize), + 'g' | 'G' => SheetSemitone(7isize), + 'a' | 'A' => SheetSemitone(9isize), + 'b' | 'B' => SheetSemitone(11isize), + _ => { + return Err(SheetParseError::InvalidTone(index)); + } + }; + + index += 1; + + if s.len() == 1 { + tones.push(SheetTone::new(tone, octave)); + + break; + } + + s = &s[1..]; + + if s.starts_with('+') { + let plus_len = skip_by_fn(s, |c| c == '+'); + tone.shift(plus_len as isize); + + index += plus_len; + + if plus_len >= s.len() { + tones.push(SheetTone::new(tone, octave)); + + let chord: SheetChord = tones.into(); + + return Ok((index, chord.into())); + } + + s = &s[plus_len..]; + } else if s.starts_with('-') { + let minus_len = skip_by_fn(s, |c| c == '-'); + tone.shift(-(minus_len as isize)); + + index += minus_len; + + if minus_len >= s.len() { + tones.push(SheetTone::new(tone, octave)); + + let chord: SheetChord = tones.into(); + + return Ok((index, chord.into())); + } + + s = &s[minus_len..]; + } + + tones.push(SheetTone::new(tone, octave)); + + skip_ws!(s, index, { + let chord: SheetChord = tones.into(); + + return Ok((index, chord.into())); + }); + + let (duration_len, duration) = parse_one_duration(s).map_err(|e| e.shift_pos(index))?; + + if duration_len != 0 { + index += duration_len; + + sheet_duration = duration; + + break; + } + + // If there is a forward slash then continue else just break + + if !s.starts_with('/') { + break; + } + + index += 1; + s = &s[1..]; + + skip_ws!(s, index, { + return Err(SheetParseError::InvalidTone(index)); + }); + } + + let chord: SheetChord = tones.into(); + + let unit = SheetUnit::Tone(chord, sheet_duration); + + Ok((index, unit)) +} + +fn parse_one_unit(s: &str) -> Result<(usize, SheetUnit), SheetParseError> { + if s.is_empty() { + return Ok((0, Default::default())); + } + + let mut s = s; + + let mut index = 0; + + let mut peek_iter = s.chars().peekable(); + let first_char = peek_iter.peek().unwrap(); + + match first_char { + 'i' => { + s = &s[1..]; + index += 1; + + parse_one_instruction(s) + .map(|(len, unit)| (len + index, unit)) + .map_err(|e| e.shift_pos(index)) + } + 'r' => { + s = &s[1..]; + index += 1; + + let (len, unit) = parse_one_silence(s); + + Ok((index + len, unit)) + } + 'o' | '<' | '>' | 'a' | 'b' | 'c' | 'd' | 'e' | 'f' | 'g' => parse_one_chord(s), + _ => Ok((0, Default::default())), + } +} + +fn parse_one_group(s: &str) -> Result<(usize, SheetGroup), SheetParseError> { + // read units + + let mut s = s; + + let mut explicit_group = true; + + let mut index = 0; + + let mut result = SheetGroup::default(); + + result.nodes.push(SheetGroupNode::Intermediate(1, None)); + result.edges.push(Vec::new()); + + let mut peek_iter = s.chars().peekable(); + let first_char = peek_iter.peek(); + + match first_char { + Some('(') => { + // an explicit block + + let mut stack = vec![0]; + let mut current_parent = 0; + + index += 1; + s = &s[1..]; + + skip_ws!(s, index, { + dbg!(); + return Err(SheetParseError::HangingGroup(index)); + }); + + loop { + let (parsed_length, parsed_unit) = + parse_one_unit(s).map_err(|e| e.shift_pos(index))?; + + if parsed_length == 0 { + let mut peekable = s.chars().peekable(); + + let first_char = peekable.peek().unwrap(); + + match first_char { + '(' => { + result.nodes.push(SheetGroupNode::Intermediate(1, None)); + result.edges.push(Vec::new()); + + let result_len = result.nodes.len(); + + result.edges[current_parent].push(result_len - 1); + + stack.push(current_parent); + current_parent = result_len - 1; + + index += 1; + s = &s[1..]; + + skip_ws!(s, index, { + return Err(SheetParseError::HangingGroup(index)); + }); + + continue; + } + ')' => { + if stack.len() <= 1 { + break; + } + + index += 1; + s = &s[1..]; + + skip_ws!(s, index, { + return Err(SheetParseError::HangingGroup(index)); + }); + + let (parsed_length, parsed_cram_repetition) = + parse_cram_repetition(s).map_err(|e| e.shift_pos(index))?; + + if parsed_length != 0 { + index += parsed_length; + s = &s[parsed_length..]; + + result.nodes[current_parent] + .set_repetition(parsed_cram_repetition.repetition()); + + result.nodes[current_parent] + .set_cram(parsed_cram_repetition.cram()); + + skip_ws!(s, index, { + return Err(SheetParseError::HangingGroup(index)); + }); + } + + current_parent = stack.pop().unwrap(); + + continue; + } + _ => { + break; + } + } + } + + index += parsed_length; + s = &s[parsed_length..]; + + let leaf_node = SheetGroupNode::Unit(parsed_unit); + + let nodes_len = result.nodes.len(); + + result.nodes.push(leaf_node); + result.edges.push(Vec::new()); + + result.edges[current_parent].push(nodes_len); + + skip_ws!(s, index, { + dbg!(); + return Err(SheetParseError::HangingGroup(index)); + }); + } + + if s.starts_with(')') { + index += 1; + s = &s[1..]; + + skip_ws!(s, index, { + return Ok((index, result)); + }); + } else { + dbg!(); + return Err(SheetParseError::HangingGroup(index)); + } + } + + Some(')') => { + dbg!(); + return Err(SheetParseError::HangingGroup(index)); + } + Some(_) => { + // an implicit group: we read a unit only + + explicit_group = false; + + let (parsed_length, parsed_unit) = parse_one_unit(s).map_err(|e| e.shift_pos(index))?; + + if parsed_length != 0 { + index += parsed_length; + + let leaf_node = SheetGroupNode::Unit(parsed_unit); + + let nodes_len = result.nodes.len(); + + result.nodes.push(leaf_node); + result.edges.push(Vec::new()); + + result.edges[0].push(nodes_len); + + if parsed_length >= s.len() { + return Ok((index, result)); + } + + s = &s[parsed_length..]; + + skip_ws!(s, index, { + return Ok((index, result)); + }); + } + } + None => { + // empty group + } + } + + if explicit_group && index == 1 { + dbg!(); + return Err(SheetParseError::HangingGroup(index)); + } // else if !explicit_group && index == 0 { + // dbg!(); + // return Err(SheetParseError::InvalidGroup(index)); + // } + + let (parsed_length, parsed_cram_repetition) = + parse_cram_repetition(s).map_err(|e| e.shift_pos(index))?; + + if parsed_length != 0 { + index += parsed_length; + s = &s[parsed_length..]; + + result.nodes[0].set_repetition(parsed_cram_repetition.repetition()); + result.nodes[0].set_cram(parsed_cram_repetition.cram()); + } + + let skip_result = skip_wsp_and_newline_and_comments(s).map_err(|e| e.shift_pos(index))?; + + index += skip_result; + + // let mut peek_iter = s.chars().peekable(); + // let first_char = peek_iter.peek(); + + // if explicit_group && first_char.copied() != Some(')') { + // return Err(SheetParseError::HangingGroup(index)); + // } + + // if explicit_group { + // index += 1; + // } + + Ok((index, result)) +} + +fn parse_one_block(s: &str) -> Result<(usize, SheetBlock), SheetParseError> { + // read groups + + let mut s = s; + + // let mut explicit_block = true; + + let mut index = 0; + + let mut result = SheetBlock::default(); + + let mut peek_iter = s.chars().peekable(); + let first_char = peek_iter.peek(); + + match first_char { + Some('{') => { + // an explicit block + + index += 1; + s = &s[1..]; + + skip_ws!(s, index, { + return Err(SheetParseError::HangingBlock(index)); + }); + + loop { + let (parsed_length, parsed_group) = + parse_one_group(s).map_err(|e| e.shift_pos(index))?; + + if parsed_length == 0 { + break; + } + + index += parsed_length; + s = &s[parsed_length..]; + + result.groups.push(parsed_group); + } + + skip_ws!(s, index, { + return Err(SheetParseError::HangingBlock(index)); + }); + + if s.starts_with('}') { + index += 1; + } else { + return Err(SheetParseError::HangingBlock(index)); + } + } + Some('}') => { + dbg!(); + return Err(SheetParseError::HangingBlock(index)); + } + Some(_) => { + // an implicit block: we read till the end + + // explicit_block = false; + + loop { + let (parsed_length, parsed_group) = + parse_one_group(s).map_err(|e| e.shift_pos(index))?; + + if parsed_length == 0 { + break; + } + + index += parsed_length; + s = &s[parsed_length..]; + + result.groups.push(parsed_group); + } + } + None => { + // empty block + } + } + + // let mut peek_iter = s.chars().peekable(); + // let first_char = peek_iter.peek(); + + // if explicit_block && first_char.copied() != Some('}') { + // return Err(SheetParseError::HangingBlock(index)); + // } else if !explicit_block && !s.is_empty() { + // return Err(SheetParseError::InvalidBlock(index)); + // } + + // if explicit_block { + // index += 1; + // } + + Ok((index, result)) +} + +impl std::str::FromStr for Sheet { + type Err = SheetParseError; + + fn from_str(s: &str) -> Result { + if s.is_empty() { + return Err(SheetParseError::EmptySheet); + } + + let mut s = s; + + let mut index: usize = 0; + + let mut result = Sheet::default(); + + loop { + skip_ws!(s, index, break); + + // s is guaranteed to be non-empty at this point + + // look ahead one character to determine the situation + if s.starts_with('\"') { + // an assignment expression + todo!(); + // first skip whitespaces again + } else { + // a block + + let (block_len, block) = parse_one_block(s).map_err(|e| e.shift_pos(index))?; + + index += block_len; + + result.blocks.push(block); + + if block_len >= s.len() { + break; + } + + s = &s[block_len..]; + } + } + + Ok(result) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_sheet() -> Result<(), Box> { + let s: &str = + &String::from("{ (o4 a+ / < b- 4... o3 c+ ( < e-- 4 r8 ) 1 * 2 ) 4... * 12 > d. }"); + + println!("input = {s}"); + + let sheet: Sheet = s.parse().map_err(|e: SheetParseError| e.to_string())?; + + println!("sheet = {sheet:?}"); + + Ok(()) + } + + #[test] + fn test_group_do_cram() -> Result<(), String> { + let s: &str = &String::from("(o4 a+ / < b- 4... o3 c+) 4... * 12"); + + println!("input = {s}"); + + let (index, mut group) = parse_one_group(s).map_err(|e| e.to_string())?; + + println!("{group:?}"); + + assert_eq!(index, s.len()); + + group.do_cram(&mut Beats::default()); + + println!("{group:?}"); + + match group.nodes[1] { + SheetGroupNode::Unit(SheetUnit::Tone(_, duration)) => { + assert_eq!(*duration.beats.unwrap(), 0.234375f64); + } + _ => { + panic!("wrong node"); + } + } + + match group.nodes[2] { + SheetGroupNode::Unit(SheetUnit::Tone(_, duration)) => { + assert_eq!(*duration.beats.unwrap(), 0.234375f64); + } + _ => { + panic!("wrong node"); + } + } + + Ok(()) + } + + #[test] + fn test_parse_group() -> Result<(), String> { + let s: &str = &String::from("(o4 a+ / < b- 4... o3 c+) 4... * 12"); + + println!("input = {s}"); + + let (index, unit) = parse_one_group(s).map_err(|e| e.to_string())?; + + println!("{unit:?}"); + + assert_eq!(index, s.len()); + + Ok(()) + } + + #[test] + fn test_parse_chord() -> Result<(), SheetParseError> { + let s: &str = &String::from("o4 a+ / < b- 4..."); + + let (index, unit) = parse_one_chord(s)?; + + assert_eq!(index, s.len()); + assert_eq!(format!("{unit}"), "o4 10 / < 10,0.25..."); + + Ok(()) + } + + #[test] + fn test_skip_wsp_nl_c() -> Result<(), SheetParseError> { + let mut s: &str = + &String::from("\n\n\r\r \t; haha this is a comment\nthis is not a comment"); + + let s_len = s.len(); + + let mut index = 0; + + skip_ws!(s, index, ()); + + assert_eq!(index, 33); + assert_eq!(s_len, 54); + + assert_eq!(s, "this is not a comment"); + + Ok(()) + } +} diff --git a/src/sheet/play.rs b/src/sheet/play.rs new file mode 100644 index 0000000..c2a632d --- /dev/null +++ b/src/sheet/play.rs @@ -0,0 +1,314 @@ +#![warn(missing_docs)] +//! This file implements the interface to turn a parsed representation +//! of a sheet into pulses. + +use super::*; + +#[allow(unused_imports)] +use crate::instruments::{self, Instrument as IInstrument}; + +#[derive(Debug, Clone, Copy)] +/// The environment when generating waves from a sheet. +struct SheetEnv { + volume: Volume, + bpm: BPM, + instrument: Instrument, + duration: Beats, + octave: usize, + rate: Samples, +} + +impl SheetEnv { + fn set_volume(&mut self, volume: Volume) { + self.volume = volume; + } + fn set_bpm(&mut self, bpm: BPM) { + self.bpm = bpm; + } + fn set_instrument(&mut self, instrument: Instrument) { + self.instrument = instrument; + } + fn set_duration(&mut self, duration: Beats) { + self.duration = duration; + } + fn set_octave(&mut self, octave: usize) { + self.octave = octave; + } +} + +impl Default for SheetEnv { + fn default() -> Self { + let volume = Volume::default(); + let bpm = BPM::default(); + let instrument = Instrument::default(); + let duration = Beats::default(); + let octave = 4usize; + let rate = Samples::default(); + + Self { + volume, + bpm, + instrument, + duration, + octave, + rate, + } + } +} + +impl From<(&SheetDuration, Beats)> for Beats { + fn from((duration, env_duration): (&SheetDuration, Beats)) -> Self { + let duration_base: Beats = if let Some(base) = duration.beats { + base + } else { + env_duration + }; + + let mut final_duration = duration_base; + + for i in (0..duration.dots).map(|n| n + 1) { + let power = 2usize.pow(i as u32) as f64; + + final_duration = (*final_duration + *duration_base / power).into(); + } + + final_duration + } +} + +impl From<(&SheetUnit, &mut SheetEnv)> for Wave { + fn from((unit, env): (&SheetUnit, &mut SheetEnv)) -> Self { + let mut result = Vec::new(); + + // Unfortunately our trait is wrongly designed and that is not + // dynamically dispatchable, so we need separate variables for + // each instrument. + let mut piano = instruments::PianoFromC::default(); + let mut violin = instruments::Violin::default(); + let mut sinus = instruments::Sinus::default(); + let mut tangent = instruments::Tangent::default(); + + match unit { + SheetUnit::Tone(chord, duration) => { + let mut local_waves = Vec::new(); + + for tone in chord.0.iter() { + let octave = if let Some(abs_or_rel) = tone.octave { + match abs_or_rel { + SheetOctave::Absolute(absolute_octave) => absolute_octave, + SheetOctave::Relative(relative_octave) => { + let result: isize = relative_octave + env.octave as isize; + + assert!(!result.is_negative()); + + result as usize + } + } + } else { + env.octave + }; + + env.set_octave(octave); + + let semitone: Semitones = (octave, tone.semitone).into(); + + let final_duration: Beats = (duration, env.duration).into(); + + env.set_duration(final_duration); + + let wave = match env.instrument { + Instrument::Piano => { + piano.play(semitone, env.rate, env.bpm, final_duration, env.volume) + } + Instrument::Violin => { + violin.play(semitone, env.rate, env.bpm, final_duration, env.volume) + } + Instrument::Sinus => { + sinus.play(semitone, env.rate, env.bpm, final_duration, env.volume) + } + Instrument::Tangent => { + tangent.play(semitone, env.rate, env.bpm, final_duration, env.volume) + } + }; + + local_waves.push(wave); + } + + let local_result = mix_waves_exact(local_waves); + + result.append(&mut local_result.to_vec()); + } + SheetUnit::Silence(duration) => { + let duration_per_beat: Seconds = env.bpm.into(); + + let final_duration: Beats = (duration, env.duration).into(); + + env.set_duration(final_duration); + + let duration: Seconds = (*final_duration * *duration_per_beat).into(); + let nb_samples = (*duration * *env.rate).floor() as usize; + + result.append(&mut vec![0f64.into(); nb_samples]); + } + SheetUnit::Instruction(instruction) => match instruction { + Instruction::VolumeTo(volume) => { + env.set_volume(*volume); + } + Instruction::BPMTo(bpm) => { + env.set_bpm(*bpm); + } + Instruction::InstrumentTo(instrument) => { + env.set_instrument(*instrument); + } + }, + } + + Wave::new(result) + } +} + +impl From<(&mut SheetGroup, &mut SheetEnv)> for Wave { + fn from((group, env): (&mut SheetGroup, &mut SheetEnv)) -> Self { + group.do_cram(&mut env.duration); + + // if group.nodes.len() == 19 { + // println!("group={group:?}"); + // } + + // After we crammed the expressions, we are sure that all + // tones have the correct durations, so we can safely ignore + // all cram expressions afterwards. + + let mut stack = vec![0usize]; + let mut seen = vec![false; group.nodes.len()]; + let mut stack_args: Vec = vec![]; + + while let Some(top) = stack.pop() { + match &group.nodes[top] { + SheetGroupNode::Intermediate(repetition, _) if *repetition != 0 => { + if !seen[top] { + stack.push(top); + + stack.append( + &mut group.edges[top] + .iter() + .copied() + .rev() + .filter(|child| match group.nodes[*child] { + SheetGroupNode::Intermediate(child_repetition, _) + if child_repetition != 0 => + { + true + } + SheetGroupNode::Unit(_) => true, + _ => false, + }) + .collect(), + ); + } else { + match repetition { + 0 => { + unreachable!() + } + // 1 => { + // for _ in 0..group.edges[top] + // .iter() + // .filter(|child| match group.nodes[**child] { + // SheetGroupNode::Intermediate(child_repetition, _) + // if child_repetition != 0 => + // { + // true + // } + // SheetGroupNode::Unit(_) => true, + // _ => false, + // }) + // .count() + // { + // result.append(&mut stack_args.pop().unwrap().to_vec()); + // } + // } + _ => { + let mut local_stack_args = vec![]; + let mut local_results = vec![]; + let mut final_local_result = vec![]; + + for _ in 0..group.edges[top] + .iter() + .filter(|child| match group.nodes[**child] { + SheetGroupNode::Intermediate(child_repetition, _) + if child_repetition != 0 => + { + true + } + SheetGroupNode::Unit(_) => true, + _ => false, + }) + .count() + { + local_stack_args.push(stack_args.pop().unwrap()); + } + + for local_wave in local_stack_args.into_iter().rev() { + local_results.append(&mut local_wave.to_vec()); + } + + for _ in 0..(repetition - 1) { + final_local_result.append(&mut local_results.clone()); + } + + final_local_result.append(&mut local_results); + + stack_args.push(Wave::new(final_local_result)); + } + } + } + } + SheetGroupNode::Unit(unit) => { + stack_args.push((unit, &mut *env).into()); + } + _ => { + continue; + } + } + + seen[top] = true; + } + + let result = stack_args + .iter() + .flat_map(|vec| vec.iter()) + .copied() + .collect(); + + Wave::new(result) + } +} + +// Note: The rest are but ordinary house-keeping codes to glue +// everything together. + +impl From<&mut SheetBlock> for Wave { + fn from(block: &mut SheetBlock) -> Self { + let mut result = Vec::new(); + let mut env = SheetEnv::default(); + + for group in block.groups.iter_mut() { + let wave: Wave = (group, &mut env).into(); + + result.append(&mut wave.to_vec()); + } + + Self::new(result) + } +} + +impl From<&mut Sheet> for Wave { + fn from(sheet: &mut Sheet) -> Self { + mix_waves_exact(sheet.blocks.iter_mut().map(Into::::into).collect()) + } +} + +#[cfg(test)] +mod tests { + // use super::*; +} diff --git a/tests/sine.rs b/tests/sine.rs new file mode 100644 index 0000000..89f866b --- /dev/null +++ b/tests/sine.rs @@ -0,0 +1,175 @@ +#[allow(unused_imports)] +use rumu::{ + instruments::{Instrument, PianoStackoverflow, PianoVideo, PianoWOver, Sinus, Tangent, Violin}, + output::{Output, PlainOutput}, + sheet::Sheet, + Hertz, Samples, Semitones, Wave, BPM, +}; + +#[cfg(feature = "ffmpeg")] +#[allow(unused_imports)] +use rumu::output::ffmpeg_output::{AACOutput, MP3Output, OpusOutput}; + +#[test] +fn test_sine() -> Result<(), Box> { + let tones = [-9, -7, -5, -4, -2, 0, 2, 3, 2, 0, -2, -4, -5, -7, -9]; + let mut sine_instrument: Sinus = Default::default(); + + let rate: Samples = Default::default(); + + let wave: Vec<_> = tones + .into_iter() + .map(|tone| { + sine_instrument + .play(tone as f64, rate, 100f64, 1f64, 1f64) + .to_vec() + }) + .collect(); + + let wave = Wave::new(wave.into_iter().flatten().collect()); + + let plain_output = PlainOutput::default(); + + plain_output.save(wave, rate, "test.bin")?; + + Ok(()) +} + +#[test] +fn test_tangent() -> Result<(), Box> { + let tones = [-9, -7, -5, -4, -2, 0, 2, 3, 2, 0, -2, -4, -5, -7, -9]; + let mut tangent_instrument: Tangent = Default::default(); + + let rate: Samples = Default::default(); + + let wave: Vec<_> = tones + .into_iter() + .map(|tone| { + tangent_instrument + .play(tone as f64, rate, 100f64, 1f64, 1f64) + .to_vec() + }) + .collect(); + + let wave = Wave::new(wave.into_iter().flatten().collect()); + + let plain_output = PlainOutput::default(); + + plain_output.save(wave, rate, "test.bin")?; + + Ok(()) +} + +#[test] +fn test_piano_wover() -> Result<(), Box> { + let tones = [-9, -7, -5, -4, -2, 0, 2, 3, 2, 0, -2, -4, -5, -7, -9]; + let mut tangent_instrument: PianoWOver = Default::default(); + + let rate: Samples = Default::default(); + + let wave: Vec<_> = tones + .into_iter() + .map(|tone| { + tangent_instrument + .play(tone as f64, rate, 100f64, 1f64, 2f64) + .to_vec() + }) + .collect(); + + let wave = Wave::new(wave.into_iter().flatten().collect()); + + let plain_output = PlainOutput::default(); + + plain_output.save(wave, rate, "test.bin")?; + + Ok(()) +} + +#[test] +#[cfg(feature = "ffmpeg")] +fn test_ffmpeg_piano_video() -> Result<(), Box> { + let tones = [-9, -7, -5, -4, -2, 0, 2, 3, 2, 0, -2, -4, -5, -7, -9]; + let mut piano_wover_instrument: PianoVideo = Default::default(); + + let rate: Samples = 48000f64.into(); + + let wave = tones.into_iter().map(|tone| { + piano_wover_instrument + .play(tone as f64, rate, 120f64, 1f64, 2f64) + .to_vec() + }); + + let wave = Wave::new(wave.flatten().collect()); + + let opus_output = OpusOutput::default(); + + opus_output.save(wave, rate, "test.opus")?; + + Ok(()) +} + +#[test] +#[cfg(feature = "ffmpeg")] +fn test_ffmpeg_piano_stackoverflow() -> Result<(), Box> { + let tones = [-9, -7, -5, -4, -2, 0, 2, 3, 2, 0, -2, -4, -5, -7, -9]; + let mut piano_wover_instrument: PianoStackoverflow = Default::default(); + + let rate: Samples = 48000f64.into(); + + let wave = tones.into_iter().map(|tone| { + piano_wover_instrument + .play(tone as f64, rate, 120f64, 1f64, 1f64) + .to_vec() + }); + + let wave = Wave::new(wave.flatten().collect()); + + let opus_output = OpusOutput::default(); + + opus_output.save(wave, rate, "test.opus")?; + + Ok(()) +} + +#[test] +#[cfg(feature = "ffmpeg")] +fn test_ffmpeg_violin() -> Result<(), Box> { + let tones = [-9, -7, -5, -4, -2, 0, 2, 3, 2, 0, -2, -4, -5, -7, -9]; + let mut piano_wover_instrument: Violin = Default::default(); + + let rate: Samples = 48000f64.into(); + + let wave = tones.into_iter().map(|tone| { + piano_wover_instrument + .play(tone as f64, rate, 180f64, 1f64, 0.5f64) + .to_vec() + }); + + let wave = Wave::new(wave.flatten().collect()); + + let opus_output = OpusOutput::default(); + + opus_output.save(wave, rate, "test.opus")?; + + Ok(()) +} + +#[test] +#[cfg(feature = "ffmpeg")] +fn test_ffmpeg_piano_from_sheet() -> Result<(), Box> { + let source = std::fs::read_to_string("songs/Ievan Polkka.rumu")?; + + let mut sheet: Sheet = source.parse()?; + + println!("sheet = {sheet:?}"); + + let wave: Wave = (&mut sheet).into(); + + let rate = Samples::default(); + + let opus_output = OpusOutput::default(); + + opus_output.save(wave, rate, "test.opus")?; + + Ok(()) +} -- cgit v1.2.3-18-g5258