diff --git a/code/popgen/Cargo.toml b/code/popgen/Cargo.toml index c642dc6..9227563 100644 --- a/code/popgen/Cargo.toml +++ b/code/popgen/Cargo.toml @@ -9,6 +9,7 @@ path = "src/popgen.rs" [dependencies] clap = { version = "4.5.23", features = ["derive"] } +hound = "3.5.1" rand = "0.8.5" regex = "1.11.1" rodio = "0.20.1" diff --git a/code/popgen/out0.wav b/code/popgen/out0.wav new file mode 100644 index 0000000..570745f Binary files /dev/null and b/code/popgen/out0.wav differ diff --git a/code/popgen/out1.wav b/code/popgen/out1.wav new file mode 100644 index 0000000..3569ff4 Binary files /dev/null and b/code/popgen/out1.wav differ diff --git a/code/popgen/src/popgen.rs b/code/popgen/src/popgen.rs index 1bd4c74..9a27fa2 100644 --- a/code/popgen/src/popgen.rs +++ b/code/popgen/src/popgen.rs @@ -4,9 +4,10 @@ // https://github.com/pdx-cs-sound/popgen/blob/main/popgen.py use clap::Parser; +use hound; use rand::Rng; use regex::Regex; -use rodio::{OutputStream, buffer::SamplesBuffer, Sink}; +use rodio::{buffer::SamplesBuffer, OutputStream, Sink}; use std::{f32::consts::PI, vec}; 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 } +// 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] { let mut notes: [i8; 4] = [0; 4]; //Vec would work too, but less memory efficient - let mut p: i8 = *position; - for note in &mut notes { 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); @@ -130,6 +130,7 @@ fn parse_note(note_str: &str, regex: Regex) -> u32 { let mut it: std::str::Chars<'_> = chars.into_iter(); let base_note: char = it.next().unwrap(); let mut flat: bool = false; + // Logic to Handle all valid cases, like C, C[4], Db, Db[6], etc. match it.next() { Some(c) => { if c == 'b' { @@ -155,6 +156,7 @@ fn parse_note(note_str: &str, regex: Regex) -> u32 { None => {} } + // Different string format for flat vs not let index: usize = match flat { true => NAMES.iter().position(|&n| n == format!("{}b", base_note)), false => NAMES.iter().position(|&n| n == base_note.to_string()), @@ -186,27 +188,35 @@ struct Args { #[arg(short, long, default_value_t = -3)] 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, } // Rodio example https://github.com/RustAudio/rodio/blob/master/examples/basic.rs -fn play(samples: Vec) { - - let (_stream, stream_handle) = OutputStream::try_default().unwrap(); - let source = SamplesBuffer::new(1,48_000, samples); - - let sink = Sink::try_new(&stream_handle).unwrap(); - +fn play(samples: Vec, samplerate: u16, gain: i8) { + // TODO: apply gain + let (_stream, stream_handle) = OutputStream::try_default().unwrap(); + let source: SamplesBuffer = 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 sink.append(source); - sink.sleep_until_end(); +} - +fn save(samples: Vec, 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() { - println!("Hello, pop!"); let args: Args = Args::parse(); let note_name_regexp: Regex = Regex::new(REGEX_BASE).unwrap(); let mut rng: rand::prelude::ThreadRng = rand::thread_rng(); @@ -235,35 +245,43 @@ fn main() { let mut sound: Vec = vec![]; 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![]; for note in notes { - let melody_note: Vec = make_note(note +melody_root_i8 , None, beat_samples, samplerate); + let melody_note: Vec = + make_note(note + melody_root_i8, None, beat_samples, samplerate); melody.push(melody_note); } let bass_note: i8 = note_to_key_offset((chord_root - 1) as i8); let bass: Vec = make_note(bass_note + bass_root, Some(4), beat_samples, samplerate); - let bass_gain = 1 - melody_gain; - - // Scale the waveforms + let bass_gain: i8 = 1 - melody_gain; + + // 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 = melody.into_iter().flatten().collect(); - let scaled_melody: Vec = flattened_melody.iter().map(|&x| x * melody_gain as f32).collect(); + + let scaled_melody: Vec = flattened_melody + .iter() + .map(|&x| x * melody_gain as f32) + .collect(); let scaled_bass: Vec = 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 = paired_notes.iter().map(|(m, b)| m + b).collect(); sound.extend(combined_waveform); - } if args.output.eq(NO_OUTPUT) { - play(sound); - } - else { - todo!() + play(sound, samplerate, args.gain); + } else { + save(sound, samplerate, &args.output, args.gain); } }