mvp (minimum vibrational production)
This commit is contained in:
commit
3aab7d93a3
4 changed files with 123 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/target
|
||||
16
Cargo.lock
generated
Normal file
16
Cargo.lock
generated
Normal file
|
|
@ -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",
|
||||
]
|
||||
7
Cargo.toml
Normal file
7
Cargo.toml
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
[package]
|
||||
name = "rpip7"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
hound = "3.5.1"
|
||||
99
src/main.rs
Normal file
99
src/main.rs
Normal file
|
|
@ -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<u16>,
|
||||
}
|
||||
|
||||
impl Note {
|
||||
fn rest() -> Self {
|
||||
Self {
|
||||
freq: 0,
|
||||
beats: NonZero::<u16>::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<Self::Item> {
|
||||
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::<u16>::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();
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue