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(&note) {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}