aboutsummaryrefslogtreecommitdiffstats
path: root/beeps.rs
diff options
context:
space:
mode:
Diffstat (limited to 'beeps.rs')
-rw-r--r--beeps.rs135
1 files changed, 135 insertions, 0 deletions
diff --git a/beeps.rs b/beeps.rs
new file mode 100644
index 0000000..f3c3ac8
--- /dev/null
+++ b/beeps.rs
@@ -0,0 +1,135 @@
+// Copyright (C) 2024 Tomasz Kramkowski <tomasz@kramkow.ski>
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+use std::{
+ env,
+ f64::consts,
+ io::{self, Write},
+ process,
+};
+type Float = f64;
+
+const RATE: f64 = 8000.0;
+
+#[derive(Debug, Copy, Clone)]
+struct Beep {
+ frequency: Float,
+ volume: Float,
+}
+
+impl Beep {
+ fn new(frequency: Float, volume: Float) -> Self {
+ Self { frequency, volume }
+ }
+
+ fn sample_at(&self, time: Float) -> Float {
+ Float::sin(time * consts::TAU * self.frequency) * self.volume
+ }
+}
+
+#[derive(Copy, Clone)]
+struct Beat<'a> {
+ beep: Beep,
+ start: Float,
+ duration: Float,
+ interval: Float,
+ mask: &'a dyn Fn(Float) -> bool,
+}
+
+impl<'a> Beat<'a> {
+ fn new(
+ beep: Beep,
+ start: Float,
+ duration: Float,
+ interval: Option<Float>,
+ mask: Option<&'a dyn Fn(Float) -> bool>,
+ ) -> Self {
+ Self {
+ beep,
+ start,
+ duration,
+ interval: interval.unwrap_or(Float::INFINITY),
+ mask: mask.unwrap_or(&|_| true),
+ }
+ }
+
+ fn rel_time(&self, time: Float) -> Float {
+ (time - self.start) % self.interval
+ }
+
+ fn plays_now(&self, time: Float) -> bool {
+ (self.mask)(time) && time > self.start && self.rel_time(time) < self.duration
+ }
+
+ fn sample_at(&self, time: Float) -> Float {
+ self.beep.sample_at(self.rel_time(time))
+ }
+}
+
+fn sample_at(beats: &[Beat<'_>], time: Float) -> Float {
+ let mut sample = 0.0;
+
+ for beat in beats {
+ if beat.plays_now(time) {
+ sample += beat.sample_at(time)
+ }
+ }
+
+ sample
+}
+
+fn main() {
+ let mut args = env::args();
+ // This is bugged, argv[0] is empty if you exec with an empty or no argv
+ let argv0 = args.next().expect("No argv0");
+ let (Some(duration), None) = (args.next(), args.next()) else {
+ eprintln!("Usage: {argv0} duration");
+ process::exit(1);
+ };
+ let duration: Float = duration.parse().expect("Invalid duration");
+ let mut stdout = io::stdout();
+
+ let endstop = |s| s < duration - 1.0;
+
+ let beats = [
+ Beat::new(Beep::new(500.0, 0.5), 0.0, 0.1, Some(30.0), Some(&endstop)),
+ Beat::new(Beep::new(500.0, 0.5), 14.9, 0.1, Some(30.0), Some(&endstop)),
+ Beat::new(Beep::new(600.0, 0.5), 15.0, 0.1, Some(30.0), Some(&endstop)),
+ Beat::new(Beep::new(600.0, 0.5), -0.1, 0.1, Some(30.0), Some(&endstop)),
+ Beat::new(
+ Beep::new(300.0, 0.3),
+ 1.45,
+ 0.1,
+ Some(3.0),
+ Some(&|s| (s / 15.0) as u64 % 2 == 0),
+ ),
+ Beat::new(
+ Beep::new(1000.0, 0.4),
+ 299.95,
+ 0.05,
+ Some(300.0),
+ Some(&endstop),
+ ),
+ Beat::new(
+ Beep::new(1000.0, 0.4),
+ 300.05,
+ 0.05,
+ Some(300.0),
+ Some(&endstop),
+ ),
+ Beat::new(Beep::new(400.0, 0.5), duration + 0.0, 0.4, None, None),
+ Beat::new(Beep::new(500.0, 0.5), duration + 0.45, 0.4, None, None),
+ Beat::new(Beep::new(600.0, 0.5), duration + 0.9, 0.4, None, None),
+ ];
+
+ let start = -0.1;
+ let end = duration + 1.4;
+
+ for sample_index in 0..((RATE * (end - start)) as u64) {
+ let time = start + sample_index as Float / RATE;
+ let sample = sample_at(&beats, time);
+ stdout
+ .write_all(&[((sample + 1.0) / 2.0 * 256.0).floor().clamp(0.0, 255.0) as u8])
+ .expect("Could not write sample");
+ }
+}