//! Instrument mode types: MIDI note mapping, ADSR envelope, and key zone configuration. /// How the instrument maps MIDI notes to samples. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum InstrumentMode { /// One sample pitch-shifted across the entire keyboard. Chromatic, /// Multiple samples assigned to specific key ranges. MultiSample, } /// ADSR envelope parameters. #[derive(Debug, Clone, Copy)] pub struct AdsrEnvelope { /// Attack time in seconds. pub attack: f32, /// Decay time in seconds. pub decay: f32, /// Sustain level (0.0 to 1.0). pub sustain: f32, /// Release time in seconds. pub release: f32, } impl Default for AdsrEnvelope { fn default() -> Self { Self { attack: 0.005, decay: 0.1, sustain: 1.0, release: 0.05, } } } /// What velocity controls. #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] pub enum VelocityTarget { #[default] Volume, } /// A key zone mapping a sample to a MIDI note range. #[derive(Debug, Clone)] pub struct KeyZone { /// Content-addressed hash of the sample. pub sample_hash: String, /// Display name of the sample. pub name: String, /// MIDI note number that plays the sample at original pitch. pub root_note: u8, /// Lowest MIDI note in the zone (inclusive). pub low_note: u8, /// Highest MIDI note in the zone (inclusive). pub high_note: u8, /// Minimum velocity to trigger this zone (0.0 to 1.0). pub vel_low: f32, /// Maximum velocity to trigger this zone (0.0 to 1.0). pub vel_high: f32, } /// Top-level instrument configuration. #[derive(Debug, Clone)] pub struct InstrumentConfig { pub mode: InstrumentMode, pub envelope: AdsrEnvelope, pub velocity_target: VelocityTarget, pub max_voices: usize, } impl Default for InstrumentConfig { fn default() -> Self { Self { mode: InstrumentMode::Chromatic, envelope: AdsrEnvelope::default(), velocity_target: VelocityTarget::default(), max_voices: 8, } } } /// Parse a musical key string like "A minor" or "F# major" into a MIDI root note (octave 3). /// /// Returns the note number for the pitch class at octave 3 (C3 = 48). /// Supports sharp (#) and flat (b) accidentals. Case-insensitive on the pitch class. pub fn key_to_root_note(key: &str) -> Option { let key = key.trim(); if key.is_empty() { return None; } let mut chars = key.chars(); let letter = chars.next()?.to_ascii_uppercase(); let base = match letter { 'C' => 0, 'D' => 2, 'E' => 4, 'F' => 5, 'G' => 7, 'A' => 9, 'B' => 11, _ => return None, }; // Check for accidental: next char after the letter let rest = &key[letter.len_utf8()..]; let accidental = if rest.starts_with('#') { 1i8 } else if rest.starts_with('b') { -1 } else { 0 }; // Octave 3: C3 = 48 let note = 48 + base as i8 + accidental; if note < 0 { None } else { Some(note as u8) } } /// Convert a MIDI note number to a note name string like "C3" or "A#4". pub fn note_name(note: u8) -> String { const NAMES: [&str; 12] = [ "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B", ]; let octave = (note / 12) as i8 - 1; // MIDI 0 = C-1, 48 = C3, 60 = C4 let pitch = (note % 12) as usize; format!("{}{}", NAMES[pitch], octave) } #[cfg(test)] mod tests { use super::*; #[test] fn default_envelope() { let env = AdsrEnvelope::default(); assert!((env.attack - 0.005).abs() < f32::EPSILON); assert!((env.decay - 0.1).abs() < f32::EPSILON); assert!((env.sustain - 1.0).abs() < f32::EPSILON); assert!((env.release - 0.05).abs() < f32::EPSILON); } #[test] fn default_config() { let cfg = InstrumentConfig::default(); assert_eq!(cfg.mode, InstrumentMode::Chromatic); assert_eq!(cfg.velocity_target, VelocityTarget::Volume); assert_eq!(cfg.max_voices, 8); } #[test] fn key_to_root_note_natural_keys() { assert_eq!(key_to_root_note("C major"), Some(48)); // C3 assert_eq!(key_to_root_note("D minor"), Some(50)); // D3 assert_eq!(key_to_root_note("E minor"), Some(52)); // E3 assert_eq!(key_to_root_note("F major"), Some(53)); // F3 assert_eq!(key_to_root_note("G major"), Some(55)); // G3 assert_eq!(key_to_root_note("A minor"), Some(57)); // A3 assert_eq!(key_to_root_note("B minor"), Some(59)); // B3 } #[test] fn key_to_root_note_sharps() { assert_eq!(key_to_root_note("C# minor"), Some(49)); assert_eq!(key_to_root_note("D# minor"), Some(51)); assert_eq!(key_to_root_note("F# minor"), Some(54)); assert_eq!(key_to_root_note("G# minor"), Some(56)); assert_eq!(key_to_root_note("A# minor"), Some(58)); } #[test] fn key_to_root_note_flats() { assert_eq!(key_to_root_note("Db major"), Some(49)); assert_eq!(key_to_root_note("Eb minor"), Some(51)); assert_eq!(key_to_root_note("Gb major"), Some(54)); assert_eq!(key_to_root_note("Ab minor"), Some(56)); assert_eq!(key_to_root_note("Bb major"), Some(58)); } #[test] fn key_to_root_note_invalid() { assert_eq!(key_to_root_note(""), None); assert_eq!(key_to_root_note("X major"), None); assert_eq!(key_to_root_note(" "), None); } #[test] fn note_name_standard_values() { assert_eq!(note_name(60), "C4"); // Middle C assert_eq!(note_name(69), "A4"); // A440 assert_eq!(note_name(48), "C3"); assert_eq!(note_name(0), "C-1"); assert_eq!(note_name(127), "G9"); } #[test] fn note_name_all_pitch_classes() { let names: Vec = (48..60).map(note_name).collect(); assert_eq!( names, vec!["C3", "C#3", "D3", "D#3", "E3", "F3", "F#3", "G3", "G#3", "A3", "A#3", "B3"] ); } #[test] fn note_name_round_trip() { // key_to_root_note("A minor") = 57 = A3 let note = key_to_root_note("A minor").unwrap(); assert_eq!(note_name(note), "A3"); let note = key_to_root_note("C major").unwrap(); assert_eq!(note_name(note), "C3"); } }