From 3aab7d93a3c1555a003ed01ab452a979de997b5c Mon Sep 17 00:00:00 2001 From: mehbark Date: Fri, 10 Oct 2025 23:54:46 -0400 Subject: [PATCH] mvp (minimum vibrational production) --- .gitignore | 1 + Cargo.lock | 16 +++++++++ Cargo.toml | 7 ++++ src/main.rs | 99 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 123 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 src/main.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..44e5fad --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,16 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "hound" +version = "3.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62adaabb884c94955b19907d60019f4e145d091c75345379e70d1ee696f7854f" + +[[package]] +name = "rpip7" +version = "0.1.0" +dependencies = [ + "hound", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..2d525fd --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "rpip7" +version = "0.1.0" +edition = "2024" + +[dependencies] +hound = "3.5.1" diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..88c5fdf --- /dev/null +++ b/src/main.rs @@ -0,0 +1,99 @@ +#![allow( + clippy::cast_possible_truncation, + clippy::cast_sign_loss, + clippy::cast_possible_wrap, + clippy::cast_precision_loss +)] + +use std::{ + env, + f32::consts::PI, + io::{self, Read}, + num::NonZero, +}; + +#[derive(Debug)] +struct Note { + freq: u16, + beats: NonZero, +} + +impl Note { + fn rest() -> Self { + Self { + freq: 0, + beats: NonZero::::MIN, + } + } +} + +struct Parser<'a> { + src: &'a [u8], +} + +impl<'a> Parser<'a> { + fn new(src: &'a [u8]) -> Self { + Self { src } + } +} + +impl Iterator for Parser<'_> { + type Item = Note; + + fn next(&mut self) -> Option { + let first = *self.src.first()?; + self.src = &self.src[1..]; + + if first == b' ' { + return Some(Note::rest()); + } + + let Some(n) = b"1234567drmfsltDRMSFLT".iter().position(|b| *b == first) else { + // skip invalid byte + return self.next(); + }; + let a4_pos = 12.; + let n = n as f32 - a4_pos; + let freq = 440. * 2.0f32.powf(n / 12.); + let freq = freq.floor() as u16; + let beats = self.src.iter().take_while(|b| **b == b'-').count(); + let beats = NonZero::::new(beats as u16 + 1).unwrap(); + + Some(Note { freq, beats }) + } +} + +fn main() { + let beats_per_minute: f32 = env::args() + .nth(1) + .expect("give me bpm") + .parse() + .expect("bpm is a number please"); + let beats_per_second = beats_per_minute / 60.; + + let output_path = env::args().nth(2).expect("give me a path"); + + let mut src = String::new(); + io::stdin().read_to_string(&mut src).unwrap(); + + let spec = hound::WavSpec { + channels: 1, + sample_rate: 44100, + bits_per_sample: 16, + sample_format: hound::SampleFormat::Int, + }; + + let mut writer = hound::WavWriter::create(output_path, spec).unwrap(); + + for note in Parser::new(src.as_bytes()) { + let freq = f32::from(note.freq); + let amplitude = f32::from(i16::MAX); + + let samples = (spec.sample_rate * u32::from(note.beats.get())) / beats_per_second as u32; + + for t in (0..samples).map(|x| x as f32 / (spec.sample_rate as f32)) { + let sample = (t * freq * 2.0 * PI).sin(); + writer.write_sample((sample * amplitude) as i16).unwrap(); + } + } +}