audio.rs

16.1 kB · rust · 488 lines

1use mrlycore::rng::Rng;2use mrlycore::trig;3use mrlycore::{json, Json};45/// The home midi note, G2.6pub const ROOT: i64 = 43;78/// The major scale as semitone offsets from a root.9pub const MAJOR: [i64; 7] = [0, 2, 4, 5, 7, 9, 11];1011/// The twelve pitch class names, C first.12pub const NOTES: [&str; 12] = [13    "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B",14];1516/// The frequency of every midi note in millihertz, note 69 at 440000.17pub const MILLIHERTZ: [i64; 128] = [18    8176, 8662, 9177, 9723, 10301, 10913, 11562, 12250, 12978, 13750, 14568, 15434, 16352, 17324,19    18354, 19445, 20602, 21827, 23125, 24500, 25957, 27500, 29135, 30868, 32703, 34648, 36708,20    38891, 41203, 43654, 46249, 48999, 51913, 55000, 58270, 61735, 65406, 69296, 73416, 77782,21    82407, 87307, 92499, 97999, 103826, 110000, 116541, 123471, 130813, 138591, 146832, 155563,22    164814, 174614, 184997, 195998, 207652, 220000, 233082, 246942, 261626, 277183, 293665, 311127,23    329628, 349228, 369994, 391995, 415305, 440000, 466164, 493883, 523251, 554365, 587330, 622254,24    659255, 698456, 739989, 783991, 830609, 880000, 932328, 987767, 1046502, 1108731, 1174659,25    1244508, 1318510, 1396913, 1479978, 1567982, 1661219, 1760000, 1864655, 1975533, 2093005,26    2217461, 2349318, 2489016, 2637020, 2793826, 2959955, 3135963, 3322438, 3520000, 3729310,27    3951066, 4186009, 4434922, 4698636, 4978032, 5274041, 5587652, 5919911, 6271927, 6644875,28    7040000, 7458620, 7902133, 8372018, 8869844, 9397273, 9956063, 10548082, 11175303, 11839822,29    12543854,30];3132/// The four wave names.33pub const WAVES: [&str; 4] = ["sine", "triangle", "square", "sawtooth"];3435/// The sample rate in hertz.36pub const RATE: usize = 44100;3738/// The render volume as a percentage of full scale.39pub const VOLUME: i64 = 30;4041/// The number of harmonics summed per wave.42pub const VOICES: usize = 16;4344/// The sample count of a single-cycle wavetable.45pub const CYCLE: usize = 1024;4647/// The fade length at each end of a note, in seconds.48pub const FADE: f32 = 1.0 / 64.0;4950const PEAK: f32 = VOLUME as f32 / 100.0;5152const MILLI: f32 = 1000.0;5354/// The four waveform shapes.55#[derive(Clone, Copy, Debug, PartialEq)]56pub enum Wave {57    /// The fundamental alone.58    Sine,59    /// Odd harmonics fading as one over n squared.60    Triangle,61    /// Odd harmonics fading as one over n.62    Square,63    /// All harmonics fading as one over n.64    Sawtooth,65}6667impl Wave {68    /// Returns the wave one of the four names spells, or None for a stranger.69    pub fn parse(name: &str) -> Option<Wave> {70        match name {71            "sine" => Some(Wave::Sine),72            "triangle" => Some(Wave::Triangle),73            "square" => Some(Wave::Square),74            "sawtooth" => Some(Wave::Sawtooth),75            _ => None,76        }77    }78    /// Returns the wave's lowercase name.79    pub fn name(&self) -> &'static str {80        match self {81            Wave::Sine => "sine",82            Wave::Triangle => "triangle",83            Wave::Square => "square",84            Wave::Sawtooth => "sawtooth",85        }86    }87    /// Returns the wave's amplitude at a phase measured in turns, wrapping whole turns away.88    pub fn sample(&self, phase: f32) -> f32 {89        let t = phase - phase.floor();90        match self {91            Wave::Sine => ring(t),92            Wave::Triangle => 2.0 * (2.0 * (t - (t + 0.5).floor())).abs() - 1.0,93            Wave::Square => {94                let s = ring(t);95                if s > 0.0 {96                    1.097                } else if s < 0.0 {98                    -1.099                } else {100                    0.0101                }102            }103            Wave::Sawtooth => 2.0 * (t - (t + 0.5).floor()),104        }105    }106    /// Returns the wave's additive recipe as pairs of harmonic multiple and weight.107    pub fn recipe(&self, voices: usize) -> Vec<(f32, f32)> {108        match self {109            Wave::Sine => vec![(1.0, 1.0)],110            Wave::Square => odds(voices).map(|n| (n, 1.0 / n)).collect(),111            Wave::Triangle => odds(voices).map(|n| (n, 1.0 / (n * n))).collect(),112            Wave::Sawtooth => (1..=voices).map(|i| (i as f32, 1.0 / i as f32)).collect(),113        }114    }115}116117/// One tone to render: a pitch, a shape, and a length.118pub struct Note {119    /// The midi note number.120    pub midi: i64,121    /// The waveform.122    pub wave: Wave,123    /// The duration in seconds.124    pub seconds: f32,125}126127impl Note {128    /// Builds a note from pitch, wave, and duration.129    pub fn new(midi: i64, wave: Wave, seconds: f32) -> Note {130        Note {131            midi,132            wave,133            seconds,134        }135    }136}137138/// A two-axis timbre: the shape each partial is drawn with, and the series that weights them.139#[derive(Clone, Copy, Debug, PartialEq)]140pub struct Timbre {141    /// The waveform each partial is drawn with.142    pub shape: Wave,143    /// The wave whose recipe picks the partials and their weights.144    pub series: Wave,145    /// The number of harmonics summed from the series.146    pub harmonics: usize,147}148149impl Timbre {150    /// Builds a timbre from shape, series, and harmonic count.151    pub fn new(shape: Wave, series: Wave, harmonics: usize) -> Timbre {152        Timbre {153            shape,154            series,155            harmonics,156        }157    }158}159160/// Returns a midi note's frequency in millihertz, clamped to the keyboard.161///162/// ```163/// assert_eq!(mrlymusic::audio::freq(69), 440_000);164/// ```165pub fn freq(midi: i64) -> i64 {166    MILLIHERTZ[midi.clamp(0, 127) as usize]167}168169/// Returns a midi note's name, class then octave, like A4 for 69.170pub fn name(midi: i64) -> String {171    format!(172        "{}{}",173        NOTES[midi.rem_euclid(12) as usize],174        midi.div_euclid(12) - 1175    )176}177178/// Returns the pitch class index of a note name, or None for a stranger.179pub fn class(name: &str) -> Option<i64> {180    NOTES.iter().position(|&n| n == name).map(|i| i as i64)181}182183/// Draws a scale degree from the rng, lifted up to octaves above the root.184pub fn pick(rng: &mut Rng, root: i64, scale: &[i64], octaves: i64) -> i64 {185    let degree = *rng.choice(scale);186    root + 12 * rng.range(0, octaves) + degree187}188189/// Renders a note to float samples, peaking at the volume and faded at both ends.190pub fn render(note: &Note) -> Vec<f32> {191    let mut out = partials(note.midi, Wave::Sine, note.wave, VOICES, note.seconds);192    let peak = out.iter().fold(0.0f32, |m, s| m.max(s.abs()));193    if peak > 0.0 {194        let k = PEAK / peak;195        for s in out.iter_mut() {196            *s *= k;197        }198    }199    let count = out.len();200    let ramp = ((FADE * RATE as f32) as usize).min(count / 2);201    for i in 0..ramp {202        let g = i as f32 / ramp as f32;203        out[i] *= g;204        out[count - 1 - i] *= g;205    }206    out207}208209/// Renders a midi note through a timbre to unit-peak float samples, with no fades.210pub fn tone(midi: i64, timbre: &Timbre, seconds: f32) -> Vec<f32> {211    let mut out = partials(midi, timbre.shape, timbre.series, timbre.harmonics, seconds);212    let peak = out.iter().fold(0.0f32, |m, s| m.max(s.abs()));213    if peak > 0.0 {214        for s in out.iter_mut() {215            *s /= peak;216        }217    }218    out219}220221/// Clamps float samples into 16-bit pcm.222pub fn pcm(samples: &[f32]) -> Vec<i16> {223    samples224        .iter()225        .map(|s| (s.clamp(-1.0, 1.0) * 32767.0) as i16)226        .collect()227}228229/// Builds a unit-peak single-cycle wavetable of the wave at a pitch, muting harmonics above Nyquist.230pub fn cycle(wave: &Wave, hz: f32) -> Vec<f32> {231    let mut out = vec![0.0f32; CYCLE];232    for (mult, weight) in wave.recipe(VOICES) {233        if hz * mult * 2.0 >= RATE as f32 {234            continue;235        }236        for (i, s) in out.iter_mut().enumerate() {237            *s += weight * Wave::Sine.sample(mult * i as f32 / CYCLE as f32);238        }239    }240    let peak = out.iter().fold(0.0f32, |m, s| m.max(s.abs()));241    if peak > 0.0 {242        for s in out.iter_mut() {243            *s /= peak;244        }245    }246    out247}248249/// Returns a named sound cue as a note op, falling back to the blip.250pub fn cue(name: &str) -> Json {251    let (offset, ms, gain) = match name {252        "good" => (31, 140, 30),253        "bad" => (13, 160, 30),254        "win" => (36, 320, 30),255        "lose" => (5, 380, 30),256        _ => (24, 90, 25),257    };258    json!({ "op": "note", "freq": freq(ROOT + offset), "ms": ms, "gain": gain })259}260261fn partials(midi: i64, shape: Wave, series: Wave, harmonics: usize, seconds: f32) -> Vec<f32> {262    let base = freq(midi) as f32 / MILLI;263    let count = (seconds * RATE as f32) as usize;264    let mut out = vec![0.0f32; count];265    for (mult, weight) in series.recipe(harmonics) {266        let pitch = base * mult;267        if pitch * 2.0 >= RATE as f32 {268            continue;269        }270        let step = pitch / RATE as f32;271        let mut phase = 0.0f32;272        for s in out.iter_mut() {273            *s += weight * shape.sample(phase);274            phase += step;275            if phase >= 1.0 {276                phase -= 1.0;277            }278        }279    }280    out281}282283fn odds(voices: usize) -> impl Iterator<Item = f32> {284    (0..voices).map(|i| (2 * i + 1) as f32)285}286287fn ring(t: f32) -> f32 {288    let x = t * trig::N as f32;289    let i = x.floor();290    let frac = x - i;291    let a = trig::SINE[(i as usize) % trig::N];292    let b = trig::SINE[(i as usize + 1) % trig::N];293    a + (b - a) * frac294}295296#[cfg(test)]297mod tests {298    use super::*;299300    #[test]301    fn parse_roundtrips_the_names() {302        for name in WAVES {303            assert_eq!(Wave::parse(name).unwrap().name(), name);304        }305        assert_eq!(Wave::parse("noise"), None);306    }307    #[test]308    fn samples_hit_the_landmarks() {309        assert!(Wave::Sine.sample(0.0).abs() < 1e-6);310        assert!((Wave::Sine.sample(0.25) - 1.0).abs() < 1e-4);311        assert!((Wave::Sine.sample(0.75) + 1.0).abs() < 1e-4);312        assert_eq!(Wave::Triangle.sample(0.0), -1.0);313        assert_eq!(Wave::Triangle.sample(0.5), 1.0);314        assert_eq!(Wave::Square.sample(0.25), 1.0);315        assert_eq!(Wave::Square.sample(0.75), -1.0);316        assert_eq!(Wave::Sawtooth.sample(0.25), 0.5);317        assert_eq!(Wave::Sawtooth.sample(0.75), -0.5);318    }319    #[test]320    fn samples_wrap_whole_turns() {321        for wave in [Wave::Sine, Wave::Triangle, Wave::Square, Wave::Sawtooth] {322            assert_eq!(wave.sample(0.25), wave.sample(3.25));323        }324    }325    #[test]326    fn recipes_carry_the_classic_weights() {327        assert_eq!(Wave::Sine.recipe(8), vec![(1.0, 1.0)]);328        assert_eq!(329            Wave::Square.recipe(3),330            vec![(1.0, 1.0), (3.0, 1.0 / 3.0), (5.0, 0.2)]331        );332        assert_eq!(333            Wave::Triangle.recipe(3),334            vec![(1.0, 1.0), (3.0, 1.0 / 9.0), (5.0, 1.0 / 25.0)]335        );336        assert_eq!(337            Wave::Sawtooth.recipe(3),338            vec![(1.0, 1.0), (2.0, 0.5), (3.0, 1.0 / 3.0)]339        );340    }341    #[test]342    fn freq_lands_the_tuning_fork() {343        assert_eq!(freq(69), 440_000);344        assert_eq!(freq(57), 220_000);345        assert_eq!(freq(81), 880_000);346        assert_eq!(freq(67), 391_995);347        assert_eq!(freq(43), 97_999);348    }349    #[test]350    fn freq_clamps_outside_the_keyboard() {351        assert_eq!(freq(-4), MILLIHERTZ[0]);352        assert_eq!(freq(900), MILLIHERTZ[127]);353    }354    #[test]355    fn octaves_double_the_millihertz() {356        for midi in 0..116 {357            let low = freq(midi);358            let high = freq(midi + 12);359            assert!((high - 2 * low).abs() <= 1, "midi {midi}: {low} {high}");360        }361    }362    #[test]363    fn names_roundtrip_the_classes() {364        assert_eq!(name(43), "G2");365        assert_eq!(name(60), "C4");366        assert_eq!(name(69), "A4");367        assert_eq!(class("C"), Some(0));368        assert_eq!(class("G"), Some(7));369        assert_eq!(class("H"), None);370        for (i, n) in NOTES.iter().enumerate() {371            assert_eq!(class(n), Some(i as i64));372        }373    }374    #[test]375    fn pick_is_seeded_and_in_range() {376        let mut a = Rng::new(7);377        let mut b = Rng::new(7);378        for _ in 0..32 {379            let x = pick(&mut a, ROOT, &MAJOR, 1);380            assert_eq!(x, pick(&mut b, ROOT, &MAJOR, 1));381            assert!((ROOT..=ROOT + 12 + 11).contains(&x));382            assert!(MAJOR.contains(&((x - ROOT) % 12)));383        }384    }385    #[test]386    fn render_fills_the_duration() {387        let note = Note::new(69, Wave::Sine, 0.15);388        assert_eq!(render(&note).len(), (0.15 * RATE as f32) as usize);389    }390    #[test]391    fn render_peaks_at_the_volume() {392        for wave in [Wave::Sine, Wave::Triangle, Wave::Square, Wave::Sawtooth] {393            let samples = render(&Note::new(69, wave, 0.15));394            let peak = samples.iter().fold(0.0f32, |m, s| m.max(s.abs()));395            assert!((peak - PEAK).abs() < 1e-4, "{} {peak}", wave.name());396        }397    }398    #[test]399    fn render_fades_the_endpoints() {400        let samples = render(&Note::new(69, Wave::Square, 0.15));401        assert_eq!(samples[0], 0.0);402        assert_eq!(samples[samples.len() - 1], 0.0);403        let ramp = (FADE * RATE as f32) as usize;404        assert!(samples[..ramp].iter().all(|s| s.abs() <= PEAK));405    }406    #[test]407    fn tone_peaks_at_unity() {408        for wave in [Wave::Sine, Wave::Triangle, Wave::Square, Wave::Sawtooth] {409            let timbre = Timbre::new(Wave::Sine, wave, VOICES);410            let samples = tone(69, &timbre, 0.15);411            assert_eq!(samples.len(), (0.15 * RATE as f32) as usize);412            let peak = samples.iter().fold(0.0f32, |m, s| m.max(s.abs()));413            assert!((peak - 1.0).abs() < 1e-4, "{} {peak}", wave.name());414        }415    }416    #[test]417    fn tone_matches_render_inside_the_fades() {418        let rendered = render(&Note::new(69, Wave::Square, 0.15));419        let timbre = Timbre::new(Wave::Sine, Wave::Square, VOICES);420        let toned = tone(69, &timbre, 0.15);421        let ramp = (FADE * RATE as f32) as usize;422        for i in ramp..rendered.len() - ramp {423            assert!((rendered[i] - toned[i] * PEAK).abs() < 1e-4);424        }425    }426    #[test]427    fn tone_separates_the_axes() {428        let pure = tone(69, &Timbre::new(Wave::Sine, Wave::Sine, 1), 0.1);429        let bent = tone(69, &Timbre::new(Wave::Triangle, Wave::Sine, 1), 0.1);430        let rich = tone(69, &Timbre::new(Wave::Sine, Wave::Triangle, VOICES), 0.1);431        assert_ne!(pure, bent);432        assert_ne!(pure, rich);433        assert_ne!(bent, rich);434    }435    #[test]436    fn tone_thins_to_sine_near_nyquist() {437        assert_eq!(438            tone(127, &Timbre::new(Wave::Sine, Wave::Square, VOICES), 0.05),439            tone(127, &Timbre::new(Wave::Sine, Wave::Sine, 1), 0.05)440        );441    }442    #[test]443    fn pcm_clamps_to_i16() {444        assert_eq!(pcm(&[2.0, -2.0, 0.0, 1.0]), vec![32767, -32767, 0, 32767]);445    }446    #[test]447    fn cycle_peaks_at_unity() {448        for wave in [Wave::Sine, Wave::Triangle, Wave::Square, Wave::Sawtooth] {449            let table = cycle(&wave, 440.0);450            assert_eq!(table.len(), CYCLE);451            let peak = table.iter().fold(0.0f32, |m, s| m.max(s.abs()));452            assert!((peak - 1.0).abs() < 1e-4, "{} {peak}", wave.name());453        }454    }455    #[test]456    fn cycle_mutes_above_nyquist() {457        assert!(cycle(&Wave::Sine, 23000.0).iter().all(|s| *s == 0.0));458    }459    #[test]460    fn cycle_thins_to_sine_near_nyquist() {461        assert_eq!(cycle(&Wave::Square, 8000.0), cycle(&Wave::Sine, 8000.0));462    }463    #[test]464    fn cues_are_notes_without_wave() {465        for name in ["blip", "good", "bad", "win", "lose"] {466            let sound = cue(name);467            assert_eq!(sound["op"], "note");468            assert!(sound["freq"].as_i64().unwrap() > 0);469            assert!(sound["ms"].as_i64().unwrap() >= 90);470            assert!(sound["gain"].as_i64().unwrap() > 0);471            assert!(sound.get("wave").is_none());472        }473    }474    #[test]475    fn cues_land_their_offsets() {476        assert_eq!(cue("blip")["freq"], json!(391_995));477        assert_eq!(cue("good")["freq"], json!(freq(ROOT + 31)));478        assert_eq!(cue("bad")["freq"], json!(freq(ROOT + 13)));479        assert_eq!(cue("win")["freq"], json!(freq(ROOT + 36)));480        assert_eq!(cue("lose")["freq"], json!(freq(ROOT + 5)));481        assert_eq!(cue("mystery"), cue("blip"));482    }483    #[test]484    fn gains_are_centi_percent() {485        assert_eq!(cue("blip")["gain"], json!(25));486        assert_eq!(cue("win")["gain"], json!(30));487    }488}