summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJSDurand <mmemmew@gmail.com>2022-08-24 23:54:13 +0800
committerJSDurand <mmemmew@gmail.com>2022-08-24 23:54:13 +0800
commite954df3f896bd18494cd27d77b26bbb2005de8a7 (patch)
tree7c81b759e41665ffd3fcd77f2f1036b59c03bc79
First commit
Now the project is in a somewhat complete state, ready for future enhancements.
-rw-r--r--.cargo/config8
-rw-r--r--.gitignore4
-rw-r--r--Cargo.toml13
-rwxr-xr-xplay.sh3
-rw-r--r--songs/Ievan Polkka.rumu324
-rw-r--r--songs/Wellerman.rumu85
-rw-r--r--songs/experiment.rumu66
-rw-r--r--songs/katyusha.rumu44
-rw-r--r--songs/take me hand.rumu33
-rw-r--r--songs/test.rumu10
-rw-r--r--src/instruments.rs485
-rw-r--r--src/lib.rs279
-rw-r--r--src/main.rs214
-rw-r--r--src/output/ffmpeg_output.rs417
-rw-r--r--src/output/mod.rs68
-rw-r--r--src/sheet/mod.rs1905
-rw-r--r--src/sheet/play.rs314
-rw-r--r--tests/sine.rs175
18 files changed, 4447 insertions, 0 deletions
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:
+; <https://musescore.com/user/11638191/scores/6726144>
+
+; 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 / <b / d) * 4
+
+ (o4 a / > 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: <https://www.youtube.com/watch?v=wN83dn07TUw>.
+
+
+{
+ 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](<https://en.wikipedia.org/wiki/Envelope_(music)>).
+ fn play<T, S, U, V, R>(&mut self, note: T, rate: U, bpm: S, duration: V, volume: R) -> Wave
+ where
+ T: Into<Semitones>,
+ S: Into<BPM>,
+ U: Into<Samples>,
+ V: Into<Beats>,
+ R: Into<Volume>;
+}
+
+// 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<T, S, U, V, R>(&mut self, note: T, rate: U, bpm: S, duration: V, volume: R) -> Wave
+ where
+ T: Into<Semitones>,
+ S: Into<BPM>,
+ U: Into<Samples>,
+ V: Into<Beats>,
+ R: Into<Volume>,
+ {
+ 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<Pulse> = 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<T, S, U, V, R>(&mut self, note: T, rate: U, bpm: S, duration: V, volume: R) -> Wave
+ where
+ T: Into<Semitones>,
+ S: Into<BPM>,
+ U: Into<Samples>,
+ V: Into<Beats>,
+ R: Into<Volume>,
+ {
+ 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<Pulse> = 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<T, S, U, V, R>(&mut self, note: T, rate: U, bpm: S, duration: V, volume: R) -> Wave
+ where
+ T: Into<Semitones>,
+ S: Into<BPM>,
+ U: Into<Samples>,
+ V: Into<Beats>,
+ R: Into<Volume>,
+ {
+ 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<Pulse> = 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<T, S, U, V, R>(&mut self, note: T, rate: U, bpm: S, duration: V, volume: R) -> Wave
+ where
+ T: Into<Semitones>,
+ S: Into<BPM>,
+ U: Into<Samples>,
+ V: Into<Beats>,
+ R: Into<Volume>,
+ {
+ 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<Pulse> = 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<T, S, U, V, R>(&mut self, note: T, rate: U, bpm: S, duration: V, volume: R) -> Wave
+ where
+ T: Into<Semitones>,
+ S: Into<BPM>,
+ U: Into<Samples>,
+ V: Into<Beats>,
+ R: Into<Volume>,
+ {
+ 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<Pulse> = 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<T, S, U, V, R>(&mut self, note: T, rate: U, bpm: S, duration: V, volume: R) -> Wave
+ where
+ T: Into<Semitones>,
+ S: Into<BPM>,
+ U: Into<Samples>,
+ V: Into<Beats>,
+ R: Into<Volume>,
+ {
+ 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<Pulse> = 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<T, S, U, V, R>(&mut self, note: T, rate: U, bpm: S, duration: V, volume: R) -> Wave
+ where
+ T: Into<Semitones>,
+ S: Into<BPM>,
+ U: Into<Samples>,
+ V: Into<Beats>,
+ R: Into<Volume>,
+ {
+ 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<Pulse> = 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<f64> 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<Pulse>,
+}
+
+impl Wave {
+ /// Construct a new wave
+ pub fn new(pulses: Vec<Pulse>) -> Self {
+ Self { pulses }
+ }
+
+ /// Return the vector contained inside it
+ pub fn to_vec(self) -> Vec<Pulse> {
+ self.pulses
+ }
+}
+
+#[allow(dead_code)]
+fn mix_waves_no_divide(waves: Vec<Wave>) -> 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>) -> 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>) -> 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<BPM> 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<Semitones> 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<PathBuf> {
+ 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<dyn std::error::Error>> {
+ 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<FFError> 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<Muxer<File>, 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<u8> = 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<u8> = 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<u8> = 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<io::Error> 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<u8> = 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<Volume> for Instruction {
+ fn from(volume: Volume) -> Self {
+ Self::VolumeTo(volume)
+ }
+}
+
+impl From<BPM> for Instruction {
+ fn from(bpm: BPM) -> Self {
+ Self::BPMTo(bpm)
+ }
+}
+
+impl From<Instrument> 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<usize> for SheetOctave {
+ fn from(abs: usize) -> Self {
+ Self::Absolute(abs)
+ }
+}
+
+impl From<isize> 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<SheetOctave>,
+}
+
+impl SheetTone {
+ fn new(semitone: SheetSemitone, octave: Option<SheetOctave>) -> 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<T> From<T> for SheetTone
+where
+ T: Into<SheetSemitone>,
+{
+ 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<SheetTone>);
+
+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<T> From<T> for SheetChord
+where
+ T: Into<SheetTone>,
+{
+ fn from(arg: T) -> Self {
+ Self(vec![arg.into()])
+ }
+}
+
+impl<T> From<Vec<T>> for SheetChord
+where
+ T: Into<SheetTone>,
+{
+ fn from(arg: Vec<T>) -> 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<Beats>,
+ dots: usize,
+}
+
+impl SheetDuration {
+ /// Return an optional `Beats`.
+ pub fn beats(&self) -> Option<Beats> {
+ 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<usize> for SheetDuration {
+ fn from(dots: usize) -> Self {
+ let beats = Default::default();
+ Self { beats, dots }
+ }
+}
+
+impl<T: Into<Beats>> From<T> for SheetDuration {
+ fn from(arg: T) -> Self {
+ let beats = arg.into();
+ let beats = Some(beats);
+ let dots = Default::default();
+ Self { beats, dots }
+ }
+}
+
+impl<T: Into<Beats>> From<(Option<T>, usize)> for SheetDuration {
+ fn from((arg, dots): (Option<T>, 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<T: Into<Instruction>> From<T> for SheetUnit {
+ fn from(instruction: T) -> Self {
+ let instruction = instruction.into();
+
+ Self::Instruction(instruction)
+ }
+}
+
+impl From<SheetDuration> for SheetUnit {
+ fn from(duration: SheetDuration) -> Self {
+ Self::Silence(duration)
+ }
+}
+
+impl From<SheetChord> 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<SheetDuration>),
+ /// 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<SheetDuration>) {
+ 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<Beats>) {
+ 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<SheetGroupNode>,
+ edges: Vec<Vec<usize>>,
+}
+
+impl SheetGroup {
+ /// Construct a new group.
+ pub fn new(nodes: Vec<SheetGroupNode>, edges: Vec<Vec<usize>>) -> 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<Beats> = 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<SheetGroup>,
+}
+
+impl SheetBlock {
+ /// A plain constructor.
+ pub fn new(groups: Vec<SheetGroup>) -> 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<SheetBlock>,
+}
+
+impl Sheet {
+ /// A plain constructor.
+ pub fn new(blocks: Vec<SheetBlock>) -> 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<Option<usize>, 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<usize, SheetParseError> {
+ 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<SheetDuration>, Option<usize>);
+
+impl CramRepetition {
+ fn repetition(&self) -> usize {
+ self.1.unwrap_or(1usize)
+ }
+
+ fn cram(&self) -> Option<SheetDuration> {
+ self.0
+ }
+
+ fn set_cram(&mut self, cram: Option<SheetDuration>) {
+ self.0 = cram;
+ }
+
+ fn set_repetition(&mut self, repetition: Option<usize>) {
+ 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<Beats> = 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<SheetTone> = Vec::new();
+
+ let mut sheet_duration = SheetDuration::default();
+
+ loop {
+ let mut octave: Option<SheetOctave> = 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<Self, Self::Err> {
+ 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<dyn std::error::Error>> {
+ 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<Wave> = 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::<Wave>::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<dyn std::error::Error>> {
+ 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<dyn std::error::Error>> {
+ 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<dyn std::error::Error>> {
+ 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<dyn std::error::Error>> {
+ 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<dyn std::error::Error>> {
+ 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<dyn std::error::Error>> {
+ 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<dyn std::error::Error>> {
+ 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(())
+}