touches on popgen

This commit is contained in:
David Westgate 2024-12-11 03:30:58 -08:00
parent 21d85e1ee4
commit f8eaf6c753
4 changed files with 45 additions and 26 deletions

View File

@ -9,6 +9,7 @@ path = "src/popgen.rs"
[dependencies] [dependencies]
clap = { version = "4.5.23", features = ["derive"] } clap = { version = "4.5.23", features = ["derive"] }
hound = "3.5.1"
rand = "0.8.5" rand = "0.8.5"
regex = "1.11.1" regex = "1.11.1"
rodio = "0.20.1" rodio = "0.20.1"

BIN
code/popgen/out0.wav Normal file

Binary file not shown.

BIN
code/popgen/out1.wav Normal file

Binary file not shown.

View File

@ -4,9 +4,10 @@
// https://github.com/pdx-cs-sound/popgen/blob/main/popgen.py // https://github.com/pdx-cs-sound/popgen/blob/main/popgen.py
use clap::Parser; use clap::Parser;
use hound;
use rand::Rng; use rand::Rng;
use regex::Regex; use regex::Regex;
use rodio::{OutputStream, buffer::SamplesBuffer, Sink}; use rodio::{buffer::SamplesBuffer, OutputStream, Sink};
use std::{f32::consts::PI, vec}; use std::{f32::consts::PI, vec};
const C5: &str = "C[5]"; const C5: &str = "C[5]";
@ -93,11 +94,10 @@ fn chord_to_note_offset(posn: i8) -> i8 {
floor_div(posn, 3) * 7 + (major_chord as i8) - 1 floor_div(posn, 3) * 7 + (major_chord as i8) - 1
} }
// Choose four random notes (one measure) with proper chord offset from the base note
fn pick_notes(chord_root: u8, position: &mut i8, rng: &mut rand::prelude::ThreadRng) -> [i8; 4] { fn pick_notes(chord_root: u8, position: &mut i8, rng: &mut rand::prelude::ThreadRng) -> [i8; 4] {
let mut notes: [i8; 4] = [0; 4]; //Vec would work too, but less memory efficient let mut notes: [i8; 4] = [0; 4]; //Vec would work too, but less memory efficient
let mut p: i8 = *position; let mut p: i8 = *position;
for note in &mut notes { for note in &mut notes {
let chord_note_offset: i8 = chord_to_note_offset(p); let chord_note_offset: i8 = chord_to_note_offset(p);
let chord_note: i8 = note_to_key_offset((chord_root as i8) + chord_note_offset); let chord_note: i8 = note_to_key_offset((chord_root as i8) + chord_note_offset);
@ -130,6 +130,7 @@ fn parse_note(note_str: &str, regex: Regex) -> u32 {
let mut it: std::str::Chars<'_> = chars.into_iter(); let mut it: std::str::Chars<'_> = chars.into_iter();
let base_note: char = it.next().unwrap(); let base_note: char = it.next().unwrap();
let mut flat: bool = false; let mut flat: bool = false;
// Logic to Handle all valid cases, like C, C[4], Db, Db[6], etc.
match it.next() { match it.next() {
Some(c) => { Some(c) => {
if c == 'b' { if c == 'b' {
@ -155,6 +156,7 @@ fn parse_note(note_str: &str, regex: Regex) -> u32 {
None => {} None => {}
} }
// Different string format for flat vs not
let index: usize = match flat { let index: usize = match flat {
true => NAMES.iter().position(|&n| n == format!("{}b", base_note)), true => NAMES.iter().position(|&n| n == format!("{}b", base_note)),
false => NAMES.iter().position(|&n| n == base_note.to_string()), false => NAMES.iter().position(|&n| n == base_note.to_string()),
@ -186,27 +188,35 @@ struct Args {
#[arg(short, long, default_value_t = -3)] #[arg(short, long, default_value_t = -3)]
gain: i8, gain: i8,
#[arg(short, long, default_value_t = NO_OUTPUT.to_string())] #[arg(short, long, default_value_t = NO_OUTPUT.to_string())] // Ideally there is a better way than this
output: String, output: String,
} }
// Rodio example https://github.com/RustAudio/rodio/blob/master/examples/basic.rs // Rodio example https://github.com/RustAudio/rodio/blob/master/examples/basic.rs
fn play(samples: Vec<f32>) { fn play(samples: Vec<f32>, samplerate: u16, gain: i8) {
// TODO: apply gain
let (_stream, stream_handle) = OutputStream::try_default().unwrap(); let (_stream, stream_handle) = OutputStream::try_default().unwrap();
let source = SamplesBuffer::new(1,48_000, samples); let source: SamplesBuffer<f32> = SamplesBuffer::new(1, samplerate as u32, samples);
let sink: Sink = Sink::try_new(&stream_handle).unwrap(); //sink makes it easy to keep the program asleep so the whole song plays
let sink = Sink::try_new(&stream_handle).unwrap();
sink.append(source); sink.append(source);
sink.sleep_until_end(); sink.sleep_until_end();
}
fn save(samples: Vec<f32>, samplerate: u16, output: &str, gain: i8) {
// TODO: apply gain
let spec: hound::WavSpec = hound::WavSpec {
channels: 1,
sample_rate: samplerate as u32,
bits_per_sample: 32,
sample_format: hound::SampleFormat::Float,
};
let mut writer = hound::WavWriter::create(output, spec).unwrap();
for s in samples {
writer.write_sample(s).unwrap();
}
} }
fn main() { fn main() {
println!("Hello, pop!");
let args: Args = Args::parse(); let args: Args = Args::parse();
let note_name_regexp: Regex = Regex::new(REGEX_BASE).unwrap(); let note_name_regexp: Regex = Regex::new(REGEX_BASE).unwrap();
let mut rng: rand::prelude::ThreadRng = rand::thread_rng(); let mut rng: rand::prelude::ThreadRng = rand::thread_rng();
@ -235,35 +245,43 @@ fn main() {
let mut sound: Vec<f32> = vec![]; let mut sound: Vec<f32> = vec![];
for chord_root in CHORD_LOOP { for chord_root in CHORD_LOOP {
let notes = pick_notes(chord_root - 1, &mut position, &mut rng); let notes: [i8; 4] = pick_notes(chord_root - 1, &mut position, &mut rng);
let mut melody: Vec<Vec<f32>> = vec![]; let mut melody: Vec<Vec<f32>> = vec![];
for note in notes { for note in notes {
let melody_note: Vec<f32> = make_note(note +melody_root_i8 , None, beat_samples, samplerate); let melody_note: Vec<f32> =
make_note(note + melody_root_i8, None, beat_samples, samplerate);
melody.push(melody_note); melody.push(melody_note);
} }
let bass_note: i8 = note_to_key_offset((chord_root - 1) as i8); let bass_note: i8 = note_to_key_offset((chord_root - 1) as i8);
let bass: Vec<f32> = make_note(bass_note + bass_root, Some(4), beat_samples, samplerate); let bass: Vec<f32> = make_note(bass_note + bass_root, Some(4), beat_samples, samplerate);
let bass_gain = 1 - melody_gain; let bass_gain: i8 = 1 - melody_gain;
// Scale the waveforms // Unlike pythons array arithmetic, here we must unravel and mutate our data structures to apply the gains in the correct place.
let flattened_melody: Vec<f32> = melody.into_iter().flatten().collect(); let flattened_melody: Vec<f32> = melody.into_iter().flatten().collect();
let scaled_melody: Vec<f32> = flattened_melody.iter().map(|&x| x * melody_gain as f32).collect();
let scaled_melody: Vec<f32> = flattened_melody
.iter()
.map(|&x| x * melody_gain as f32)
.collect();
let scaled_bass: Vec<f32> = bass.iter().map(|&x| x * bass_gain as f32).collect(); let scaled_bass: Vec<f32> = bass.iter().map(|&x| x * bass_gain as f32).collect();
let paired_notes: Vec<(f32, f32)> = scaled_melody.iter().zip(scaled_bass.iter()).map(|(&m, &b)| (m, b)).collect(); let paired_notes: Vec<(f32, f32)> = scaled_melody
.iter()
.zip(scaled_bass.iter())
.map(|(&m, &b)| (m, b))
.collect();
// Finally, combine back together
let combined_waveform: Vec<f32> = paired_notes.iter().map(|(m, b)| m + b).collect(); let combined_waveform: Vec<f32> = paired_notes.iter().map(|(m, b)| m + b).collect();
sound.extend(combined_waveform); sound.extend(combined_waveform);
} }
if args.output.eq(NO_OUTPUT) { if args.output.eq(NO_OUTPUT) {
play(sound); play(sound, samplerate, args.gain);
} } else {
else { save(sound, samplerate, &args.output, args.gain);
todo!()
} }
} }