music.rs
14.2 kB · rust · 440 lines
1use crate::audio::{self, Timbre, Wave};2use mrlycore::state;3use std::collections::HashMap;45/// The ways a voice moves from one frame to the next.6#[derive(Clone, Copy, Debug, PartialEq)]7pub enum Movement {8 /// Holds the previous frame's notes.9 Repeat,10 /// Draws fresh notes from the pool.11 Random,12 /// Steps every note one pool index up, wrapping at the top.13 Up,14 /// Steps every note one pool index down, wrapping at the bottom.15 Down,16 /// Plays an empty frame.17 Pause,18}1920/// The chord stackings over a progression degree.21#[derive(Clone, Copy, Debug, PartialEq)]22pub enum ChordType {23 /// Root, third and fifth.24 Triad,25 /// Root, third, fifth and seventh.26 Seventh,27}2829impl ChordType {30 /// Returns the chord's intervals as pool index offsets from the root.31 pub fn intervals(&self) -> &'static [usize] {32 match self {33 ChordType::Triad => &[0, 2, 4],34 ChordType::Seventh => &[0, 2, 4, 6],35 }36 }37}3839/// One melodic line: a pool of midi notes, movements to walk it, and a timbre.40#[derive(Clone, Debug, PartialEq)]41pub struct Voice {42 /// The midi notes the voice draws from.43 pub note_pool: Vec<i64>,44 /// The movements the voice may take between frames.45 pub movements: Vec<Movement>,46 /// The chord stacked on each progression degree, or None to play the whole pool.47 pub chord_type: Option<ChordType>,48 /// The counts of simultaneous notes the voice may draw.49 pub num_notes: Vec<usize>,50 /// The waveform each partial is drawn with.51 pub wave_type: Wave,52 /// The number of harmonics summed from the series.53 pub num_harmonics: usize,54 /// The wave whose recipe weights the harmonics.55 pub harmonic_wave: Wave,56}5758impl Voice {59 /// Builds a voice over a note pool with its movements, the rest at their defaults.60 pub fn new(note_pool: Vec<i64>, movements: Vec<Movement>) -> Voice {61 Voice {62 note_pool,63 movements,64 chord_type: None,65 num_notes: vec![1],66 wave_type: Wave::Sine,67 num_harmonics: 1,68 harmonic_wave: Wave::Triangle,69 }70 }71 /// Returns the voice's timbre.72 pub fn timbre(&self) -> Timbre {73 Timbre::new(self.wave_type, self.harmonic_wave, self.num_harmonics)74 }75}7677impl Default for Voice {78 fn default() -> Voice {79 Voice::new(Vec::new(), Vec::new())80 }81}8283/// Returns a chord letter's diatonic degree, C through B as 0 through 6, or None for a stranger.84///85/// ```86/// assert_eq!(mrlymusic::music::degree('F'), Some(3));87/// ```88pub fn degree(letter: char) -> Option<i64> {89 "CDEFGAB"90 .chars()91 .position(|c| c == letter)92 .map(|i| i as i64)93}9495/// Composes frames of midi notes by walking every voice through a chord progression.96///97/// Each letter earns every voice a bar of bar_length frames, letters beyond C through B98/// are skipped, and the base track loops until count frames are filled. With repeat on,99/// a voice replays its first bar for a letter every time that letter returns.100pub fn compose(101 progression: &str,102 voices: &[Voice],103 bar_length: usize,104 count: usize,105 repeat: bool,106) -> Vec<Vec<i64>> {107 let mut tracks: Vec<Vec<Vec<i64>>> = vec![Vec::new(); voices.len()];108 let mut library: HashMap<(usize, char), Vec<Vec<i64>>> = HashMap::new();109 for letter in progression.chars() {110 let Some(d) = degree(letter) else { continue };111 for (i, voice) in voices.iter().enumerate() {112 let bar = if repeat {113 library114 .entry((i, letter))115 .or_insert_with(|| voice_bar(voice, d as usize, bar_length))116 .clone()117 } else {118 voice_bar(voice, d as usize, bar_length)119 };120 tracks[i].extend(bar);121 }122 }123 let base = concatenate(&tracks);124 if base.is_empty() {125 return Vec::new();126 }127 let mut frames = Vec::with_capacity(count);128 while frames.len() < count {129 for frame in &base {130 if frames.len() == count {131 break;132 }133 frames.push(frame.clone());134 }135 }136 frames137}138139/// Mixes one frame's notes through a timbre into a single buffer, one unit-peak tone each.140pub fn mix(notes: &[i64], timbre: &Timbre, seconds: f32) -> Vec<f32> {141 let count = (seconds * audio::RATE as f32) as usize;142 let mut out = vec![0.0f32; count];143 for &midi in notes {144 let tone = audio::tone(midi, timbre, seconds);145 for (s, t) in out.iter_mut().zip(&tone) {146 *s += t;147 }148 }149 out150}151152/// Renders timed frames through a timbre into one track: each frame is mixed and faded,153/// then the whole track is normalized once to the target peak.154pub fn track(frames: &[(Vec<i64>, f32)], timbre: &Timbre, peak: f32) -> Vec<f32> {155 let mut out = Vec::new();156 for (notes, seconds) in frames {157 let mut frame = mix(notes, timbre, *seconds);158 fade(&mut frame);159 out.extend_from_slice(&frame);160 }161 let top = out.iter().fold(0.0f32, |m, s| m.max(s.abs()));162 if top > 0.0 {163 let k = peak / top;164 for s in out.iter_mut() {165 *s *= k;166 }167 }168 out169}170171fn voice_bar(voice: &Voice, degree: usize, bar_length: usize) -> Vec<Vec<i64>> {172 let pool = chord_pool(voice, degree);173 bar(bar_length, &voice.movements, &pool, &voice.num_notes)174}175176fn chord_pool(voice: &Voice, degree: usize) -> Vec<i64> {177 match voice.chord_type {178 None => voice.note_pool.clone(),179 Some(kind) => chord(&voice.note_pool, degree, kind),180 }181}182183fn chord(pool: &[i64], degree: usize, chord_type: ChordType) -> Vec<i64> {184 let mut notes = Vec::new();185 if pool.is_empty() {186 return notes;187 }188 let root = degree % pool.len();189 for interval in chord_type.intervals() {190 let note = pool[(root + interval) % pool.len()];191 if !notes.contains(¬e) {192 notes.push(note);193 }194 }195 notes196}197198fn bar(count: usize, movements: &[Movement], pool: &[i64], num_notes: &[usize]) -> Vec<Vec<i64>> {199 let mut frames = Vec::with_capacity(count.max(1));200 frames.push(state::sample(pool, state::choice(num_notes)));201 for _ in 1..count {202 let movement = if movements.len() > 1 {203 state::choice(movements)204 } else {205 movements[0]206 };207 let previous = frames.last().unwrap();208 let notes = match movement {209 Movement::Repeat => previous.clone(),210 Movement::Random => state::sample(pool, state::choice(num_notes)),211 Movement::Up => step(pool, previous, 1),212 Movement::Down => step(pool, previous, -1),213 Movement::Pause => Vec::new(),214 };215 frames.push(notes);216 }217 frames218}219220fn step(pool: &[i64], previous: &[i64], by: i64) -> Vec<i64> {221 previous222 .iter()223 .map(|n| {224 let at = pool.iter().position(|p| p == n).unwrap() as i64;225 pool[(at + by).rem_euclid(pool.len() as i64) as usize]226 })227 .collect()228}229230fn concatenate(tracks: &[Vec<Vec<i64>>]) -> Vec<Vec<i64>> {231 let beats = tracks.iter().find(|t| !t.is_empty()).map_or(0, |t| t.len());232 let mut out = Vec::with_capacity(beats);233 for beat in 0..beats {234 let mut frame = Vec::new();235 for track in tracks {236 if !track.is_empty() {237 frame.extend_from_slice(&track[beat]);238 }239 }240 out.push(frame);241 }242 out243}244245fn fade(samples: &mut [f32]) {246 let count = samples.len();247 let ramp = ((audio::FADE * audio::RATE as f32) as usize).min(count / 2);248 for i in 0..ramp {249 let g = i as f32 / ramp as f32;250 samples[i] *= g;251 samples[count - 1 - i] *= g;252 }253}254255#[cfg(test)]256mod tests {257 use super::*;258 fn pool() -> Vec<i64> {259 vec![43, 45, 47, 48, 50, 52, 54]260 }261 fn voices() -> Vec<Voice> {262 let mut bass = Voice::new(pool(), vec![Movement::Repeat, Movement::Up, Movement::Down]);263 bass.chord_type = Some(ChordType::Triad);264 let mut rhythm = Voice::new(265 vec![55, 57, 59, 60, 62, 64, 66],266 vec![Movement::Repeat, Movement::Random],267 );268 rhythm.chord_type = Some(ChordType::Seventh);269 rhythm.num_notes = vec![3, 4];270 let lead = Voice::new(271 vec![67, 69, 71, 72, 74, 76, 78],272 vec![273 Movement::Repeat,274 Movement::Random,275 Movement::Up,276 Movement::Down,277 Movement::Pause,278 ],279 );280 vec![bass, rhythm, lead]281 }282 #[test]283 fn degrees_are_diatonic_not_chromatic() {284 assert_eq!(degree('F'), Some(3));285 assert_eq!(audio::class("F"), Some(5));286 for (i, letter) in "CDEFGAB".chars().enumerate() {287 assert_eq!(degree(letter), Some(i as i64));288 }289 assert_eq!(degree('H'), None);290 assert_eq!(degree('c'), None);291 }292 #[test]293 fn intervals_stack_thirds() {294 assert_eq!(ChordType::Triad.intervals(), [0, 2, 4]);295 assert_eq!(ChordType::Seventh.intervals(), [0, 2, 4, 6]);296 }297 #[test]298 fn chords_wrap_and_dedup() {299 assert_eq!(chord(&pool(), 0, ChordType::Triad), vec![43, 47, 50]);300 assert_eq!(chord(&pool(), 3, ChordType::Triad), vec![48, 52, 43]);301 assert_eq!(302 chord(&[60, 62, 64], 0, ChordType::Seventh),303 vec![60, 64, 62]304 );305 assert_eq!(chord(&[60], 4, ChordType::Seventh), vec![60]);306 assert_eq!(chord(&[], 2, ChordType::Triad), Vec::<i64>::new());307 }308 #[test]309 fn voices_carry_their_defaults() {310 let voice = Voice::new(vec![60], vec![Movement::Repeat]);311 assert_eq!(voice.chord_type, None);312 assert_eq!(voice.num_notes, vec![1]);313 assert_eq!(voice.wave_type, Wave::Sine);314 assert_eq!(voice.num_harmonics, 1);315 assert_eq!(voice.harmonic_wave, Wave::Triangle);316 assert_eq!(voice.timbre(), Timbre::new(Wave::Sine, Wave::Triangle, 1));317 assert_eq!(Voice::default(), Voice::new(Vec::new(), Vec::new()));318 }319 #[test]320 fn movements_wrap_the_pool() {321 let _g = state::guard();322 state::seed(7);323 let p = vec![1, 2, 3];324 for pair in bar(4, &[Movement::Up], &p, &[1]).windows(2) {325 let from = p.iter().position(|n| *n == pair[0][0]).unwrap();326 assert_eq!(pair[1][0], p[(from + 1) % p.len()]);327 }328 for pair in bar(4, &[Movement::Down], &p, &[1]).windows(2) {329 let from = p.iter().position(|n| *n == pair[0][0]).unwrap() as i64;330 assert_eq!(331 pair[1][0],332 p[(from - 1).rem_euclid(p.len() as i64) as usize]333 );334 }335 }336 #[test]337 fn repeat_holds_and_pause_empties() {338 let _g = state::guard();339 state::seed(3);340 let held = bar(4, &[Movement::Repeat], &pool(), &[2]);341 assert_eq!(held[0].len(), 2);342 for frame in &held {343 assert_eq!(frame, &held[0]);344 }345 let paused = bar(3, &[Movement::Pause], &pool(), &[2]);346 assert_eq!(paused[0].len(), 2);347 assert!(paused[1].is_empty());348 assert!(paused[2].is_empty());349 assert!(step(&pool(), &[], 1).is_empty());350 }351 #[test]352 fn random_draws_distinct_pool_notes() {353 let _g = state::guard();354 state::seed(9);355 let p = pool();356 for frame in bar(8, &[Movement::Random], &p, &[3]) {357 assert_eq!(frame.len(), 3);358 for n in &frame {359 assert!(p.contains(n));360 }361 let mut seen = frame.clone();362 seen.sort();363 seen.dedup();364 assert_eq!(seen.len(), 3);365 }366 }367 #[test]368 fn compose_replays_from_a_seed() {369 let _g = state::guard();370 state::seed(11);371 let a = compose("CFCFGFCG", &voices(), 4, 64, false);372 state::seed(11);373 let b = compose("CFCFGFCG", &voices(), 4, 64, false);374 assert_eq!(a, b);375 assert_eq!(a.len(), 64);376 }377 #[test]378 fn compose_loops_the_base_track() {379 let _g = state::guard();380 state::seed(5);381 let frames = compose("C", &voices(), 4, 6, false);382 assert_eq!(frames.len(), 6);383 assert_eq!(frames[4], frames[0]);384 assert_eq!(frames[5], frames[1]);385 }386 #[test]387 fn repeat_reuses_bars_across_letters() {388 let _g = state::guard();389 state::seed(2);390 let frames = compose("CC", &voices(), 4, 8, true);391 assert_eq!(&frames[..4], &frames[4..]);392 }393 #[test]394 fn compose_skips_stranger_letters() {395 let _g = state::guard();396 state::seed(4);397 let frames = compose("C-C", &voices(), 4, 8, true);398 state::seed(4);399 let again = compose("CC", &voices(), 4, 8, true);400 assert_eq!(frames, again);401 }402 #[test]403 fn compose_handles_an_empty_progression() {404 assert!(compose("", &voices(), 4, 64, false).is_empty());405 assert!(compose("C", &[], 4, 64, false).is_empty());406 }407 #[test]408 fn mix_sums_unit_tones() {409 let timbre = Timbre::new(Wave::Sine, Wave::Triangle, 3);410 let one = mix(&[69], &timbre, 0.05);411 assert_eq!(one, audio::tone(69, &timbre, 0.05));412 let two = mix(&[69, 69], &timbre, 0.05);413 let doubled: Vec<f32> = one.iter().map(|s| s * 2.0).collect();414 assert_eq!(two, doubled);415 assert!(mix(&[], &timbre, 0.05).iter().all(|s| *s == 0.0));416 }417 #[test]418 fn track_normalizes_once_globally() {419 let timbre = Timbre::new(Wave::Sine, Wave::Sine, 1);420 let frames = vec![(vec![69, 69, 69], 0.1f32), (vec![69], 0.1f32)];421 let out = track(&frames, &timbre, 0.9);422 let peak = out.iter().fold(0.0f32, |m, s| m.max(s.abs()));423 assert!((peak - 0.9).abs() < 1e-4);424 let quiet = out[out.len() / 2..]425 .iter()426 .fold(0.0f32, |m, s| m.max(s.abs()));427 assert!(quiet < 0.4);428 }429 #[test]430 fn track_fades_each_frame() {431 let timbre = Timbre::new(Wave::Sine, Wave::Sine, 1);432 let frames = vec![(vec![60], 0.1f32), (vec![64], 0.1f32)];433 let out = track(&frames, &timbre, 1.0);434 let half = out.len() / 2;435 assert_eq!(out[0], 0.0);436 assert_eq!(out[half - 1], 0.0);437 assert_eq!(out[half], 0.0);438 assert_eq!(out[out.len() - 1], 0.0);439 }440}