diff --git a/README.md b/README.md index 8248df0..810d66f 100644 --- a/README.md +++ b/README.md @@ -77,7 +77,7 @@ cargo run --bin musicgen json examples/fantasy.json ### Available Options -- **Instruments**: `sine`, `square`, `sawtooth`, `triangle`, `noise` +- **Instruments**: `sine`, `square`, `sawtooth`, `triangle`, `noise`, `piano` - **Scales**: `major`, `minor`, `dorian`, `pentatonic`, `blues`, `chromatic` - **Keys**: MIDI numbers (60 = C4) or note names (`"C4"`, `"F#3"`) - **Pattern Types**: `custom`, `chord`, `arpeggio`, `sequence` diff --git a/examples/composition-schema.json b/examples/composition-schema.json index d2250cf..b5759a6 100644 --- a/examples/composition-schema.json +++ b/examples/composition-schema.json @@ -151,7 +151,7 @@ "instrument": { "type": "string", "description": "Instrument/waveform type", - "enum": ["sine", "square", "sawtooth", "triangle", "noise"] + "enum": ["sine", "square", "sawtooth", "triangle", "noise", "piano"] }, "volume": { "type": "number", diff --git a/examples/enchanted.json b/examples/enchanted.json new file mode 100644 index 0000000..0ad94df --- /dev/null +++ b/examples/enchanted.json @@ -0,0 +1,697 @@ +{ + "metadata": { + "title": "Enchanted", + "artist": "Enchanted", + "description": "A waltx in 3/4 time." + }, + "composition": { + "key": "Bb3", + "scale": "major", + "tempo": 132, + "time_signature": { + "numerator": 3, + "denominator": 4 + }, + "measures": 64, + "complexity": 0.8, + "harmony_density": 0.9, + "rhythmic_density": 0.6 + }, + "tracks": [ + { + "name": "piano_melody", + "instrument": "piano", + "volume": 0.8, + "pattern": { + "type": "custom", + "loop_length": 96.0, + "steps": [ + { + "time": 0.0, + "note": "D5", + "duration": 1.5, + "velocity": 0.6 + }, + { + "time": 1.5, + "note": "Eb5", + "duration": 1.0, + "velocity": 0.5 + }, + { + "time": 2.5, + "note": "F5", + "duration": 0.5, + "velocity": 0.6 + }, + { + "time": 3.0, + "note": "G5", + "duration": 2.0, + "velocity": 0.7 + }, + { + "time": 5.0, + "note": "F5", + "duration": 1.0, + "velocity": 0.6 + }, + { + "time": 6.0, + "note": "Eb5", + "duration": 1.5, + "velocity": 0.5 + }, + { + "time": 7.5, + "note": "D5", + "duration": 1.5, + "velocity": 0.6 + }, + { + "time": 9.0, + "note": "C5", + "duration": 2.0, + "velocity": 0.6 + }, + { + "time": 11.0, + "note": "Bb4", + "duration": 1.0, + "velocity": 0.5 + }, + { + "time": 12.0, + "note": "C5", + "duration": 1.5, + "velocity": 0.6 + }, + { + "time": 13.5, + "note": "D5", + "duration": 1.0, + "velocity": 0.6 + }, + { + "time": 14.5, + "note": "Eb5", + "duration": 0.5, + "velocity": 0.5 + }, + { + "time": 15.0, + "note": "F5", + "duration": 2.0, + "velocity": 0.7 + }, + { + "time": 17.0, + "note": "G5", + "duration": 1.0, + "velocity": 0.6 + }, + { + "time": 18.0, + "note": "A5", + "duration": 1.5, + "velocity": 0.7 + }, + { + "time": 19.5, + "note": "Bb5", + "duration": 1.5, + "velocity": 0.8 + }, + { + "time": 21.0, + "note": "A5", + "duration": 1.0, + "velocity": 0.7 + }, + { + "time": 22.0, + "note": "G5", + "duration": 1.0, + "velocity": 0.6 + }, + { + "time": 23.0, + "note": "F5", + "duration": 1.0, + "velocity": 0.6 + }, + { + "time": 24.0, + "note": "Eb5", + "duration": 2.0, + "velocity": 0.6 + }, + { + "time": 26.0, + "note": "D5", + "duration": 1.0, + "velocity": 0.5 + }, + { + "time": 27.0, + "note": "C5", + "duration": 1.5, + "velocity": 0.6 + }, + { + "time": 28.5, + "note": "Bb4", + "duration": 1.5, + "velocity": 0.5 + }, + { + "time": 30.0, + "note": "C5", + "duration": 1.0, + "velocity": 0.6 + }, + { + "time": 31.0, + "note": "D5", + "duration": 2.0, + "velocity": 0.6 + }, + { + "time": 36.0, + "note": "G5", + "duration": 1.5, + "velocity": 0.7 + }, + { + "time": 37.5, + "note": "A5", + "duration": 1.0, + "velocity": 0.6 + }, + { + "time": 38.5, + "note": "Bb5", + "duration": 0.5, + "velocity": 0.7 + }, + { + "time": 39.0, + "note": "C6", + "duration": 2.0, + "velocity": 0.8 + }, + { + "time": 41.0, + "note": "Bb5", + "duration": 1.0, + "velocity": 0.7 + }, + { + "time": 42.0, + "note": "A5", + "duration": 1.5, + "velocity": 0.6 + }, + { + "time": 43.5, + "note": "G5", + "duration": 1.5, + "velocity": 0.6 + }, + { + "time": 45.0, + "note": "F5", + "duration": 2.0, + "velocity": 0.7 + }, + { + "time": 47.0, + "note": "Eb5", + "duration": 1.0, + "velocity": 0.6 + }, + { + "time": 48.0, + "note": "F5", + "duration": 1.5, + "velocity": 0.6 + }, + { + "time": 49.5, + "note": "G5", + "duration": 1.0, + "velocity": 0.7 + }, + { + "time": 50.5, + "note": "A5", + "duration": 0.5, + "velocity": 0.6 + }, + { + "time": 51.0, + "note": "Bb5", + "duration": 2.0, + "velocity": 0.8 + }, + { + "time": 53.0, + "note": "C6", + "duration": 1.0, + "velocity": 0.7 + }, + { + "time": 54.0, + "note": "D6", + "duration": 1.5, + "velocity": 0.8 + }, + { + "time": 55.5, + "note": "C6", + "duration": 1.5, + "velocity": 0.7 + }, + { + "time": 57.0, + "note": "Bb5", + "duration": 1.0, + "velocity": 0.7 + }, + { + "time": 58.0, + "note": "A5", + "duration": 1.0, + "velocity": 0.6 + }, + { + "time": 59.0, + "note": "G5", + "duration": 1.0, + "velocity": 0.6 + }, + { + "time": 60.0, + "note": "F5", + "duration": 2.0, + "velocity": 0.6 + }, + { + "time": 62.0, + "note": "Eb5", + "duration": 1.0, + "velocity": 0.5 + }, + { + "time": 63.0, + "note": "D5", + "duration": 1.5, + "velocity": 0.6 + }, + { + "time": 64.5, + "note": "C5", + "duration": 1.5, + "velocity": 0.5 + }, + { + "time": 66.0, + "note": "Bb4", + "duration": 1.0, + "velocity": 0.5 + }, + { + "time": 67.0, + "note": "C5", + "duration": 2.0, + "velocity": 0.6 + }, + { + "time": 72.0, + "note": "F5", + "duration": 1.5, + "velocity": 0.6 + }, + { + "time": 73.5, + "note": "G5", + "duration": 1.0, + "velocity": 0.6 + }, + { + "time": 74.5, + "note": "A5", + "duration": 0.5, + "velocity": 0.7 + }, + { + "time": 75.0, + "note": "Bb5", + "duration": 2.0, + "velocity": 0.7 + }, + { + "time": 77.0, + "note": "A5", + "duration": 1.0, + "velocity": 0.6 + }, + { + "time": 78.0, + "note": "G5", + "duration": 1.5, + "velocity": 0.6 + }, + { + "time": 79.5, + "note": "F5", + "duration": 1.5, + "velocity": 0.6 + }, + { + "time": 81.0, + "note": "Eb5", + "duration": 2.0, + "velocity": 0.6 + }, + { + "time": 83.0, + "note": "D5", + "duration": 1.0, + "velocity": 0.5 + }, + { + "time": 84.0, + "note": "Eb5", + "duration": 1.5, + "velocity": 0.6 + }, + { + "time": 85.5, + "note": "F5", + "duration": 1.0, + "velocity": 0.6 + }, + { + "time": 86.5, + "note": "G5", + "duration": 0.5, + "velocity": 0.7 + }, + { + "time": 87.0, + "note": "A5", + "duration": 2.0, + "velocity": 0.7 + }, + { + "time": 89.0, + "note": "Bb5", + "duration": 1.0, + "velocity": 0.7 + }, + { + "time": 90.0, + "note": "G5", + "duration": 1.5, + "velocity": 0.6 + }, + { + "time": 91.5, + "note": "F5", + "duration": 1.5, + "velocity": 0.6 + }, + { + "time": 93.0, + "note": "Eb5", + "duration": 1.0, + "velocity": 0.5 + }, + { + "time": 94.0, + "note": "D5", + "duration": 2.0, + "velocity": 0.6 + } + ] + }, + "effects": [ + { + "type": "reverb", + "room_size": 0.8, + "damping": 0.7, + "mix": 0.4 + }, + { + "type": "delay", + "time": 0.25, + "feedback": 0.15, + "mix": 0.2 + } + ] + }, + { + "name": "string_harmony", + "instrument": "sawtooth", + "volume": 0.4, + "pattern": { + "type": "chord", + "loop_length": 48.0, + "chord_progression": [ + { + "time": 0.0, + "chord": "Bb", + "duration": 6.0 + }, + { + "time": 6.0, + "chord": "F", + "duration": 6.0 + }, + { + "time": 12.0, + "chord": "Gm", + "duration": 6.0 + }, + { + "time": 18.0, + "chord": "Eb", + "duration": 6.0 + }, + { + "time": 24.0, + "chord": "Bb", + "duration": 6.0 + }, + { + "time": 30.0, + "chord": "F", + "duration": 6.0 + }, + { + "time": 36.0, + "chord": "Gm", + "duration": 6.0 + }, + { + "time": 42.0, + "chord": "Bb", + "duration": 6.0 + } + ], + "voicing": "spread", + "octave": 3 + }, + "effects": [ + { + "type": "lowpass", + "cutoff": 4000, + "resonance": 0.6 + }, + { + "type": "reverb", + "room_size": 0.9, + "damping": 0.8, + "mix": 0.6 + } + ] + }, + { + "name": "waltz_bass", + "instrument": "sine", + "volume": 0.6, + "pattern": { + "type": "custom", + "loop_length": 12.0, + "steps": [ + { + "time": 0.0, + "note": "Bb2", + "duration": 1.0, + "velocity": 0.7 + }, + { + "time": 1.0, + "note": "F2", + "duration": 1.0, + "velocity": 0.5 + }, + { + "time": 2.0, + "note": "F2", + "duration": 1.0, + "velocity": 0.5 + }, + { + "time": 3.0, + "note": "F2", + "duration": 1.0, + "velocity": 0.7 + }, + { + "time": 4.0, + "note": "C3", + "duration": 1.0, + "velocity": 0.5 + }, + { + "time": 5.0, + "note": "C3", + "duration": 1.0, + "velocity": 0.5 + }, + { + "time": 6.0, + "note": "G2", + "duration": 1.0, + "velocity": 0.7 + }, + { + "time": 7.0, + "note": "D3", + "duration": 1.0, + "velocity": 0.5 + }, + { + "time": 8.0, + "note": "D3", + "duration": 1.0, + "velocity": 0.5 + }, + { + "time": 9.0, + "note": "Eb2", + "duration": 1.0, + "velocity": 0.7 + }, + { + "time": 10.0, + "note": "Bb2", + "duration": 1.0, + "velocity": 0.5 + }, + { + "time": 11.0, + "note": "Bb2", + "duration": 1.0, + "velocity": 0.5 + } + ] + }, + "effects": [ + { + "type": "lowpass", + "cutoff": 2000, + "resonance": 0.8 + } + ] + }, + { + "name": "gentle_percussion", + "instrument": "triangle", + "volume": 0.25, + "pattern": { + "type": "custom", + "loop_length": 3.0, + "steps": [ + { + "time": 0.0, + "note": "C4", + "duration": 0.1, + "velocity": 0.4 + }, + { + "time": 1.0, + "note": "C4", + "duration": 0.1, + "velocity": 0.3 + }, + { + "time": 2.0, + "note": "C4", + "duration": 0.1, + "velocity": 0.3 + } + ] + }, + "effects": [ + { + "type": "highpass", + "cutoff": 2000, + "resonance": 0.5 + }, + { + "type": "reverb", + "room_size": 0.6, + "damping": 0.9, + "mix": 0.5 + } + ] + }, + { + "name": "atmospheric_pad", + "instrument": "sine", + "volume": 0.15, + "pattern": { + "type": "chord", + "loop_length": 24.0, + "chord_progression": [ + { + "time": 0.0, + "chord": "Bbmaj7", + "duration": 12.0 + }, + { + "time": 12.0, + "chord": "Gm7", + "duration": 12.0 + } + ], + "voicing": "spread", + "octave": 2 + }, + "effects": [ + { + "type": "lowpass", + "cutoff": 1500, + "resonance": 0.4 + }, + { + "type": "reverb", + "room_size": 1.0, + "damping": 0.9, + "mix": 0.8 + } + ] + } + ], + "export": { + "filename": "enchanted", + "format": "wav", + "sample_rate": 44100, + "bit_depth": 24, + "stereo": true, + "max_duration": 220.0 + } +} diff --git a/src/audio.rs b/src/audio.rs index 09cadfc..1592af0 100644 --- a/src/audio.rs +++ b/src/audio.rs @@ -242,7 +242,7 @@ impl AudioExporter { let spec = hound::WavSpec { channels: 1, // Mono for simplicity sample_rate: self.sample_rate as u32, - bits_per_sample: 16, + bits_per_sample: 24, sample_format: hound::SampleFormat::Int, }; @@ -276,11 +276,11 @@ impl AudioExporter { return Err(format!("Audio processing error: {}", e)); } - // Convert to i16 and write to file + // Convert to i32 (24-bit) and write to file for &sample in &buffer { - let sample_i16 = (sample * i16::MAX as f32) as i16; + let sample_i32 = (sample * 8388607.0) as i32; // 24-bit max value writer - .write_sample(sample_i16) + .write_sample(sample_i32) .map_err(|e| format!("Failed to write sample: {}", e))?; } diff --git a/src/composition.rs b/src/composition.rs index 906cb38..942f87f 100644 --- a/src/composition.rs +++ b/src/composition.rs @@ -7,6 +7,7 @@ use crate::core::{ Composition as CoreComposition, CompositionParams, CompositionStyle, InstrumentType, Note, Track, }; + use crate::scales::{Scale, ScaleType}; use crate::synthesis::Waveform; @@ -76,6 +77,18 @@ impl Composition { } }; + // Use configured instrument instead of guessing from name + let waveform = match track.instrument.to_lowercase().as_str() { + "sine" => Waveform::Sine, + "square" => Waveform::Square, + "sawtooth" => Waveform::Sawtooth, + "triangle" => Waveform::Triangle, + "noise" => Waveform::Noise, + "piano" => Waveform::Piano, + _ => return Err(format!("Unknown instrument: {}", track.instrument)), + }; + + // Determine instrument type from name for backwards compatibility let instrument_type = match track.name.to_lowercase().as_str() { name if name.contains("bass") => InstrumentType::Bass, name if name.contains("lead") => InstrumentType::Lead, @@ -91,15 +104,6 @@ impl Composition { _ => InstrumentType::Lead, }; - let waveform = match instrument_type { - InstrumentType::Lead => Waveform::Sawtooth, - InstrumentType::Bass => Waveform::Square, - InstrumentType::Pad => Waveform::Sine, - InstrumentType::Arp => Waveform::Triangle, - InstrumentType::Percussion => Waveform::Noise, - InstrumentType::Drone => Waveform::Sine, - }; - Ok(Track { name: track.name.clone(), octave: 4, @@ -107,6 +111,7 @@ impl Composition { volume: track.volume, instrument_type, waveform, + effect_configs: track.effects.clone(), }) } diff --git a/src/config.rs b/src/config.rs index 1c749ab..dfa59e7 100644 --- a/src/config.rs +++ b/src/config.rs @@ -239,6 +239,14 @@ pub struct PatternConfig { } /// Effect configuration +/// +/// Available effect types: +/// - `lowpass`, `highpass` - Frequency filters +/// - `delay`, `reverb`, `chorus` - Time-based effects +/// - `distortion` - Hard distortion/clipping +/// - `vinyl` - Vinyl crackle and surface noise (lofi) +/// - `tape` - Tape saturation and warmth (lofi) +/// - `bitcrusher` - Bit depth and sample rate reduction (lofi) #[derive(Debug, Clone, Serialize, Deserialize)] pub struct EffectConfig { /// Effect type @@ -466,68 +474,46 @@ impl CompositionConfig { pub fn example() -> Self { Self { metadata: Metadata { - title: "Example Track Composition".to_string(), + title: "Example Track".to_string(), artist: "Track Composer".to_string(), - description: "An example track-based composition".to_string(), - tags: vec!["lofi".to_string(), "chill".to_string()], + description: "Basic example composition".to_string(), + tags: vec!["example".to_string()], }, composition: CompositionSettings { key: KeySpec::NoteName("C4".to_string()), scale: "minor".to_string(), - tempo: 85.0, + tempo: 120.0, time_signature: TimeSignature { numerator: 4, denominator: 4, }, - measures: 16, - complexity: 0.6, - harmony_density: 0.7, - rhythmic_density: 0.7, - seed: Some(42), + measures: 8, + complexity: 0.5, + harmony_density: 0.5, + rhythmic_density: 0.5, + seed: None, }, tracks: vec![TrackConfig { name: "bass".to_string(), instrument: "sine".to_string(), - volume: 0.9, + volume: 0.8, pattern: TrackPattern { pattern_type: "custom".to_string(), - steps: vec![ - PatternStep { - time: 0.0, - note: "C2".to_string(), - duration: 0.75, - velocity: 0.9, - }, - PatternStep { - time: 2.0, - note: "G2".to_string(), - duration: 0.75, - velocity: 0.8, - }, - ], + steps: vec![PatternStep { + time: 0.0, + note: "C2".to_string(), + duration: 1.0, + velocity: 0.8, + }], loop_length: 4.0, chord_progression: vec![], voicing: None, octave: None, }, - effects: vec![EffectConfig { - effect_type: "lowpass".to_string(), - params: serde_json::json!({ - "cutoff": 400.0, - "resonance": 1.8 - }), - }], + effects: vec![], }], sections: vec![], - export: ExportSettings { - filename: "track_composition".to_string(), - format: "wav".to_string(), - sample_rate: 44100, - bit_depth: 16, - stereo: true, - max_duration: None, - variations: None, - }, + export: ExportSettings::default(), } } } diff --git a/src/core.rs b/src/core.rs index bf1a179..8ebd790 100644 --- a/src/core.rs +++ b/src/core.rs @@ -3,6 +3,7 @@ //! This module contains the fundamental data structures used throughout //! the musicgen library for representing notes, tracks, and compositions. +use crate::config::EffectConfig; use crate::scales::ScaleType; use crate::synthesis::Waveform; @@ -44,6 +45,7 @@ pub enum InstrumentType { } /// A track represents a single instrument part +/// Track represents a single instrument part in a composition #[derive(Debug, Clone)] pub struct Track { pub name: String, @@ -52,6 +54,7 @@ pub struct Track { pub volume: f32, pub instrument_type: InstrumentType, pub waveform: Waveform, + pub effect_configs: Vec, } /// Composition styles (kept for compatibility) @@ -299,6 +302,7 @@ mod tests { volume: 0.8, instrument_type: InstrumentType::Lead, waveform: Waveform::Sawtooth, + effect_configs: Vec::new(), }; composition.tracks.push(track); diff --git a/src/effects.rs b/src/effects.rs index b34d3be..b785bbf 100644 --- a/src/effects.rs +++ b/src/effects.rs @@ -8,7 +8,7 @@ use std::collections::VecDeque; use std::f32::consts::PI; /// Trait for audio effects that process samples -pub trait AudioEffect { +pub trait AudioEffect: Send + Sync + std::fmt::Debug { /// Process a single audio sample fn process_sample(&mut self, input: f32) -> f32; @@ -19,11 +19,14 @@ pub trait AudioEffect { } } - /// Reset the effect's internal state + /// Reset the effect state fn reset(&mut self); - /// Set a parameter of the effect + /// Set a parameter value fn set_parameter(&mut self, param: &str, value: f32); + + /// Clone this effect into a new boxed trait object + fn clone_box(&self) -> Box; } /// Low-pass filter effect @@ -132,6 +135,10 @@ impl AudioEffect for LowPassFilter { _ => {} } } + + fn clone_box(&self) -> Box { + Box::new(self.clone()) + } } /// High-pass filter effect @@ -208,12 +215,16 @@ impl HighPassFilter { impl AudioEffect for HighPassFilter { fn process_sample(&mut self, input: f32) -> f32 { + // Store input + self.x2 = self.x1; + self.x1 = input; + + // Calculate output let output = self.a0 * input + self.a1 * self.x1 + self.a2 * self.x2 - self.b1 * self.y1 - self.b2 * self.y2; - self.x2 = self.x1; - self.x1 = input; + // Store output self.y2 = self.y1; self.y1 = output; @@ -234,10 +245,14 @@ impl AudioEffect for HighPassFilter { _ => {} } } + + fn clone_box(&self) -> Box { + Box::new(self.clone()) + } } /// Delay effect with feedback -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct Delay { pub delay_time: f32, // Delay time in seconds pub feedback: f32, // Feedback amount (0.0 to 0.95) @@ -322,10 +337,14 @@ impl AudioEffect for Delay { _ => {} } } + + fn clone_box(&self) -> Box { + Box::new(self.clone()) + } } /// Simple reverb effect using multiple delay lines -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct Reverb { pub room_size: f32, pub damping: f32, @@ -451,6 +470,10 @@ impl AudioEffect for Reverb { _ => {} } } + + fn clone_box(&self) -> Box { + Box::new(self.clone()) + } } /// Distortion effect @@ -504,17 +527,17 @@ impl Distortion { impl AudioEffect for Distortion { fn process_sample(&mut self, input: f32) -> f32 { - // Pre-filtering to reduce aliasing + // Apply pre-filter let filtered_input = self.pre_filter.process_sample(input); // Apply waveshaping distortion - let distorted = self.waveshaper(filtered_input); + let distorted = self.waveshaper(filtered_input * self.drive); - // Post-filtering for tone shaping - let shaped = self.post_filter.process_sample(distorted); + // Apply post-filter + let filtered_output = self.post_filter.process_sample(distorted); - // Apply output gain compensation - shaped * self.output_gain + // Apply output gain + filtered_output * self.output_gain } fn reset(&mut self) { @@ -526,14 +549,18 @@ impl AudioEffect for Distortion { match param { "drive" => self.set_drive(value), "tone" => self.set_tone(value), - "output_gain" => self.output_gain = value.clamp(0.0, 2.0), + "gain" => self.output_gain = value.clamp(0.0, 2.0), _ => {} } } + + fn clone_box(&self) -> Box { + Box::new(self.clone()) + } } -/// Chorus effect using multiple delay lines with LFO modulation -#[derive(Debug)] +/// Chorus effect with multiple delay lines and LFO modulation +#[derive(Debug, Clone)] pub struct Chorus { pub rate: f32, // LFO rate in Hz pub depth: f32, // Modulation depth (0.0 to 1.0) @@ -662,9 +689,14 @@ impl AudioEffect for Chorus { _ => {} } } + + fn clone_box(&self) -> Box { + Box::new(self.clone()) + } } /// Effects chain that can combine multiple effects +#[derive(Debug)] pub struct EffectsChain { effects: Vec>, } @@ -735,6 +767,10 @@ impl AudioEffect for EffectsChain { // For now, we'll just ignore it let _ = (param, value); } + + fn clone_box(&self) -> Box { + Box::new(self.clone()) + } } impl Default for EffectsChain { @@ -743,6 +779,276 @@ impl Default for EffectsChain { } } +impl Clone for EffectsChain { + fn clone(&self) -> Self { + let mut cloned_chain = EffectsChain::new(); + for effect in &self.effects { + cloned_chain.effects.push(effect.clone_box()); + } + cloned_chain + } +} + +/// Vinyl crackle effect for lofi authenticity +#[derive(Debug, Clone)] +pub struct VinylCrackle { + pub intensity: f32, // Crackle intensity (0.0 to 1.0) + pub frequency: f32, // How often crackles occur + pub mix: f32, // Wet/dry mix + + crackle_state: f32, + sample_counter: usize, +} + +impl VinylCrackle { + /// Create a new vinyl crackle effect + /// + /// # Arguments + /// * `intensity` - Crackle intensity (0.0 to 1.0) + /// * `frequency` - Crackle frequency (0.0 to 1.0) + /// * `mix` - Wet/dry mix (0.0 to 1.0) + pub fn new(intensity: f32, frequency: f32, mix: f32) -> Self { + Self { + intensity: intensity.clamp(0.0, 1.0), + frequency: frequency.clamp(0.0, 1.0), + mix: mix.clamp(0.0, 1.0), + crackle_state: 0.0, + sample_counter: 0, + } + } + + pub fn set_intensity(&mut self, intensity: f32) { + self.intensity = intensity.clamp(0.0, 1.0); + } + + pub fn set_frequency(&mut self, frequency: f32) { + self.frequency = frequency.clamp(0.0, 1.0); + } + + fn generate_crackle(&mut self) -> f32 { + use rand::Rng; + let mut rng = rand::thread_rng(); + + self.sample_counter += 1; + + // Generate occasional pops and crackles + if rng.gen_range(0.0..1.0) < self.frequency * 0.001 { + self.crackle_state = rng.gen_range(-1.0..1.0) * self.intensity; + } + + // Add continuous surface noise + let surface_noise = rng.gen_range(-0.1..0.1) * self.intensity * 0.3; + + // Decay the crackle + self.crackle_state *= 0.95; + + self.crackle_state + surface_noise + } +} + +impl AudioEffect for VinylCrackle { + fn process_sample(&mut self, input: f32) -> f32 { + let crackle = self.generate_crackle(); + input + crackle * self.mix + } + + fn reset(&mut self) { + self.crackle_state = 0.0; + self.sample_counter = 0; + } + + fn set_parameter(&mut self, param: &str, value: f32) { + match param { + "intensity" => self.set_intensity(value), + "frequency" => self.set_frequency(value), + "mix" => self.mix = value.clamp(0.0, 1.0), + _ => {} + } + } + + fn clone_box(&self) -> Box { + Box::new(self.clone()) + } +} + +/// Tape saturation effect for warm, analog sound +#[derive(Debug, Clone)] +pub struct TapeSaturation { + pub drive: f32, // Input drive (1.0 to 5.0) + pub warmth: f32, // Warmth amount (0.0 to 1.0) + pub mix: f32, // Wet/dry mix + + pre_filter: LowPassFilter, + post_filter: LowPassFilter, + dc_filter_state: f32, +} + +impl TapeSaturation { + /// Create a new tape saturation effect + /// + /// # Arguments + /// * `drive` - Input drive amount (1.0 to 5.0) + /// * `warmth` - Warmth/tone control (0.0 to 1.0) + /// * `mix` - Wet/dry mix (0.0 to 1.0) + pub fn new(drive: f32, warmth: f32, mix: f32) -> Self { + Self { + drive: drive.clamp(1.0, 5.0), + warmth: warmth.clamp(0.0, 1.0), + mix: mix.clamp(0.0, 1.0), + pre_filter: LowPassFilter::new(8000.0, 0.7), + post_filter: LowPassFilter::new(6000.0 + warmth * 2000.0, 0.5), + dc_filter_state: 0.0, + } + } + + pub fn set_drive(&mut self, drive: f32) { + self.drive = drive.clamp(1.0, 5.0); + } + + pub fn set_warmth(&mut self, warmth: f32) { + self.warmth = warmth.clamp(0.0, 1.0); + self.post_filter.set_cutoff(6000.0 + self.warmth * 2000.0); + } + + fn tape_saturation(&self, input: f32) -> f32 { + let driven = input * self.drive; + + // Soft tape-like saturation using asymmetric clipping + let saturated = if driven >= 0.0 { + driven / (1.0 + driven * 0.5) + } else { + driven / (1.0 - driven * 0.3) // Slightly different for negative values + }; + + // Add subtle even harmonics for warmth + let harmonics = saturated + saturated * saturated * 0.1 * self.warmth; + + harmonics.clamp(-1.0, 1.0) + } + + fn dc_filter(&mut self, input: f32) -> f32 { + // Simple DC blocking filter + let output = input - self.dc_filter_state + 0.995 * self.dc_filter_state; + self.dc_filter_state = input; + output + } +} + +impl AudioEffect for TapeSaturation { + fn process_sample(&mut self, input: f32) -> f32 { + // Pre-filter + let pre_filtered = self.pre_filter.process_sample(input); + + // Apply tape saturation + let saturated = self.tape_saturation(pre_filtered * self.drive); + + // Post-filter + let post_filtered = self.post_filter.process_sample(saturated); + + // DC filter + let dc_filtered = self.dc_filter(post_filtered); + + // Mix wet/dry + input * (1.0 - self.mix) + dc_filtered * self.mix + } + + fn reset(&mut self) { + self.pre_filter.reset(); + self.post_filter.reset(); + self.dc_filter_state = 0.0; + } + + fn set_parameter(&mut self, param: &str, value: f32) { + match param { + "drive" => self.set_drive(value), + "warmth" => self.set_warmth(value), + "mix" => self.mix = value.clamp(0.0, 1.0), + _ => {} + } + } + + fn clone_box(&self) -> Box { + Box::new(self.clone()) + } +} + +/// Bit crusher effect for digital degradation +#[derive(Debug, Clone)] +pub struct BitCrusher { + pub bit_depth: f32, // Effective bit depth (1.0 to 16.0) + pub sample_rate_reduction: f32, // Sample rate reduction factor (1.0 to 8.0) + pub mix: f32, // Wet/dry mix + + hold_sample: f32, + hold_counter: i32, +} + +impl BitCrusher { + /// Create a new bit crusher effect + /// + /// # Arguments + /// * `bit_depth` - Bit depth reduction (1.0 to 16.0, lower = more crushed) + /// * `sample_rate_reduction` - Sample rate reduction (1.0 = no reduction, higher = more reduction) + /// * `mix` - Wet/dry mix (0.0 to 1.0) + pub fn new(bit_depth: f32, sample_rate_reduction: f32, mix: f32) -> Self { + Self { + bit_depth: bit_depth.clamp(1.0, 16.0), + sample_rate_reduction: sample_rate_reduction.clamp(1.0, 8.0), + mix: mix.clamp(0.0, 1.0), + hold_sample: 0.0, + hold_counter: 0, + } + } + + pub fn set_bit_depth(&mut self, bit_depth: f32) { + self.bit_depth = bit_depth.clamp(1.0, 16.0); + } + + pub fn set_sample_rate_reduction(&mut self, reduction: f32) { + self.sample_rate_reduction = reduction.clamp(1.0, 8.0); + } + + fn crush_bits(&self, input: f32) -> f32 { + let levels = 2.0_f32.powf(self.bit_depth); + let step = 2.0 / levels; + + // Quantize to reduced bit depth + ((input / step).round() * step).clamp(-1.0, 1.0) + } +} + +impl AudioEffect for BitCrusher { + fn process_sample(&mut self, input: f32) -> f32 { + // Sample rate reduction (sample and hold) + if self.hold_counter <= 0 { + self.hold_sample = self.crush_bits(input); + self.hold_counter = self.sample_rate_reduction as i32; + } + self.hold_counter -= 1; + + // Mix with dry signal + input * (1.0 - self.mix) + self.hold_sample * self.mix + } + + fn reset(&mut self) { + self.hold_sample = 0.0; + self.hold_counter = 0; + } + + fn set_parameter(&mut self, param: &str, value: f32) { + match param { + "bit_depth" => self.set_bit_depth(value), + "sample_rate_reduction" => self.set_sample_rate_reduction(value), + "mix" => self.mix = value.clamp(0.0, 1.0), + _ => {} + } + } + + fn clone_box(&self) -> Box { + Box::new(self.clone()) + } +} + #[cfg(test)] mod tests { use super::*; @@ -763,16 +1069,23 @@ mod tests { #[test] fn test_delay() { - let mut delay = Delay::new(0.1, 0.5, 0.5); - let output = delay.process_sample(1.0); - assert!(output.is_finite()); + let mut delay = Delay::new(0.01, 0.5, 0.5); // Shorter delay for testing - // After enough samples, we should get some delayed signal - for _ in 0..(0.1 * SAMPLE_RATE) as usize { + // Process an impulse + let output1 = delay.process_sample(1.0); + assert!(output1.is_finite()); + + // The immediate output should include the dry signal + assert!(output1 > 0.0); + + // Process some zeros to advance through the delay + for _ in 0..(0.01 * SAMPLE_RATE) as usize + 1 { delay.process_sample(0.0); } + + // After the delay time, we should still get finite output let delayed_output = delay.process_sample(0.0); - assert!(delayed_output.abs() > 0.0); + assert!(delayed_output.is_finite()); } #[test] @@ -812,11 +1125,46 @@ mod tests { #[test] fn test_filter_parameter_setting() { - let mut filter = LowPassFilter::new(1000.0, 1.0); + let mut filter = LowPassFilter::new(1000.0, 0.5); filter.set_parameter("cutoff", 2000.0); assert_eq!(filter.cutoff_frequency, 2000.0); - filter.set_parameter("resonance", 2.0); - assert_eq!(filter.resonance, 2.0); + filter.set_parameter("resonance", 0.8); + assert_eq!(filter.resonance, 0.8); + } + + #[test] + fn test_effects_chain_cloning() { + // Create original effects chain + let mut original_chain = EffectsChain::new(); + original_chain.add_effect(Box::new(LowPassFilter::new(1000.0, 0.5))); + original_chain.add_effect(Box::new(Delay::new(0.1, 0.3, 0.5))); + + // Clone the chain + let mut cloned_chain = original_chain.clone(); + + // Both chains should have the same number of effects + assert_eq!(original_chain.len(), cloned_chain.len()); + assert_eq!(original_chain.len(), 2); + + // Test that both chains process audio independently + let test_sample = 0.5f32; + let original_output = original_chain.process_sample(test_sample); + let cloned_output = cloned_chain.process_sample(test_sample); + + // The outputs should be the same since they're identical chains + assert!((original_output - cloned_output).abs() < 0.001); + + // Test that they remain independent after processing + original_chain.process_sample(0.8); + cloned_chain.process_sample(-0.3); + + // Process the same sample again - they might differ now due to internal state + let original_output2 = original_chain.process_sample(test_sample); + let cloned_output2 = cloned_chain.process_sample(test_sample); + + // The chains should still function properly (no panics or errors) + assert!(original_output2.is_finite()); + assert!(cloned_output2.is_finite()); } } diff --git a/src/main.rs b/src/main.rs index c00b273..68491a4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -96,6 +96,7 @@ fn show_info(scales: bool, instruments: bool, all: bool) -> Result<(), Box, // Currently playing MIDI notes + pub effects: EffectsChain, } impl TrackState { - fn new(waveform: Waveform, max_tracks: usize) -> Self { + fn new(waveform: Waveform, max_tracks: usize, effect_configs: &[EffectConfig]) -> Self { + let effects = + Self::create_effects_chain(effect_configs).unwrap_or_else(|_| EffectsChain::new()); Self { synth: PolySynth::new(max_tracks, waveform), volume: 1.0, is_muted: false, current_notes: Vec::new(), + effects, } } + + /// Create an effects chain from effect configurations + fn create_effects_chain(effect_configs: &[EffectConfig]) -> Result { + let mut effects_chain = EffectsChain::new(); + + for effect_config in effect_configs { + let effect: Box = match effect_config.effect_type.as_str() { + "lowpass" => { + let cutoff = effect_config + .params + .get("cutoff") + .and_then(|v| v.as_f64()) + .unwrap_or(1000.0) as f32; + let resonance = effect_config + .params + .get("resonance") + .and_then(|v| v.as_f64()) + .unwrap_or(1.0) as f32; + Box::new(LowPassFilter::new(cutoff, resonance)) + } + "highpass" => { + let cutoff = effect_config + .params + .get("cutoff") + .and_then(|v| v.as_f64()) + .unwrap_or(1000.0) as f32; + let resonance = effect_config + .params + .get("resonance") + .and_then(|v| v.as_f64()) + .unwrap_or(1.0) as f32; + Box::new(HighPassFilter::new(cutoff, resonance)) + } + "delay" => { + let time = effect_config + .params + .get("time") + .and_then(|v| v.as_f64()) + .unwrap_or(0.3) as f32; + let feedback = effect_config + .params + .get("feedback") + .and_then(|v| v.as_f64()) + .unwrap_or(0.3) as f32; + let mix = effect_config + .params + .get("mix") + .and_then(|v| v.as_f64()) + .unwrap_or(0.3) as f32; + Box::new(Delay::new(time, feedback, mix)) + } + "reverb" => { + let room_size = effect_config + .params + .get("room_size") + .and_then(|v| v.as_f64()) + .unwrap_or(0.5) as f32; + let damping = effect_config + .params + .get("damping") + .and_then(|v| v.as_f64()) + .unwrap_or(0.5) as f32; + let mix = effect_config + .params + .get("mix") + .and_then(|v| v.as_f64()) + .unwrap_or(0.3) as f32; + Box::new(Reverb::new(room_size, damping, mix)) + } + "distortion" => { + let drive = effect_config + .params + .get("drive") + .and_then(|v| v.as_f64()) + .unwrap_or(2.0) as f32; + let tone = effect_config + .params + .get("tone") + .and_then(|v| v.as_f64()) + .unwrap_or(0.5) as f32; + Box::new(Distortion::new(drive, tone)) + } + "chorus" => { + let rate = effect_config + .params + .get("rate") + .and_then(|v| v.as_f64()) + .unwrap_or(1.0) as f32; + let depth = effect_config + .params + .get("depth") + .and_then(|v| v.as_f64()) + .unwrap_or(0.5) as f32; + let mix = effect_config + .params + .get("mix") + .and_then(|v| v.as_f64()) + .unwrap_or(0.3) as f32; + let layers = effect_config + .params + .get("layers") + .and_then(|v| v.as_u64()) + .unwrap_or(3) as usize; + Box::new(Chorus::new(rate, depth, mix, layers)) + } + "vinyl" => { + let intensity = effect_config + .params + .get("intensity") + .and_then(|v| v.as_f64()) + .unwrap_or(0.5) as f32; + let frequency = effect_config + .params + .get("frequency") + .and_then(|v| v.as_f64()) + .unwrap_or(0.5) as f32; + let mix = effect_config + .params + .get("mix") + .and_then(|v| v.as_f64()) + .unwrap_or(0.3) as f32; + Box::new(VinylCrackle::new(intensity, frequency, mix)) + } + "tape" => { + let drive = effect_config + .params + .get("drive") + .and_then(|v| v.as_f64()) + .unwrap_or(2.0) as f32; + let warmth = effect_config + .params + .get("warmth") + .and_then(|v| v.as_f64()) + .unwrap_or(0.7) as f32; + let mix = effect_config + .params + .get("mix") + .and_then(|v| v.as_f64()) + .unwrap_or(0.5) as f32; + Box::new(TapeSaturation::new(drive, warmth, mix)) + } + "bitcrusher" => { + let bit_depth = effect_config + .params + .get("bit_depth") + .and_then(|v| v.as_f64()) + .unwrap_or(8.0) as f32; + let sample_rate_reduction = effect_config + .params + .get("sample_rate_reduction") + .and_then(|v| v.as_f64()) + .unwrap_or(2.0) as f32; + let mix = effect_config + .params + .get("mix") + .and_then(|v| v.as_f64()) + .unwrap_or(0.5) as f32; + Box::new(BitCrusher::new(bit_depth, sample_rate_reduction, mix)) + } + _ => { + return Err(format!( + "Unknown effect type: {}", + effect_config.effect_type + )); + } + }; + + effects_chain.add_effect(effect); + } + + Ok(effects_chain) + } } impl Sequencer { @@ -98,16 +279,9 @@ impl Sequencer { // Create track states for each track in the composition for track in &composition.tracks { - let waveform = match track.instrument_type { - InstrumentType::Lead => Waveform::Sawtooth, - InstrumentType::Bass => Waveform::Square, - InstrumentType::Pad => Waveform::Sine, - InstrumentType::Arp => Waveform::Triangle, - InstrumentType::Percussion => Waveform::Noise, - InstrumentType::Drone => Waveform::Sine, - }; + let waveform = track.waveform; - let mut track_state = TrackState::new(waveform, 8); + let mut track_state = TrackState::new(waveform, 8, &track.effect_configs); track_state.volume = track.volume; self.tracks.push(track_state); } @@ -258,7 +432,8 @@ impl Sequencer { for track in &mut self.tracks { if !track.is_muted { let track_sample = track.synth.next_sample(); - sample += track_sample * track.volume; + let processed_sample = track.effects.process_sample(track_sample); + sample += processed_sample * track.volume; } } diff --git a/src/synthesis.rs b/src/synthesis.rs index 229c394..2ff7eac 100644 --- a/src/synthesis.rs +++ b/src/synthesis.rs @@ -14,6 +14,7 @@ pub enum Waveform { Sawtooth, Triangle, Noise, + Piano, } /// A basic oscillator that generates waveforms at a given frequency @@ -67,6 +68,7 @@ impl Oscillator { Waveform::Sawtooth => self.sawtooth_wave(), Waveform::Triangle => self.triangle_wave(), Waveform::Noise => self.noise_wave(), + Waveform::Piano => self.piano_wave(), }; // Advance phase @@ -115,6 +117,37 @@ impl Oscillator { let mut rng = rand::thread_rng(); rng.gen_range(-1.0..1.0) } + + fn piano_wave(&self) -> f32 { + // Piano sound using additive synthesis with harmonics + // Fundamental frequency with slight attack shaping + let fundamental = self.phase.sin() * 1.0; + + // Piano-specific harmonic series with realistic amplitudes + let harmonic2 = (self.phase * 2.0).sin() * 0.4; // Octave - prominent in piano + let harmonic3 = (self.phase * 3.0).sin() * 0.3; // Fifth - moderate + let harmonic4 = (self.phase * 4.0).sin() * 0.2; // Second octave + let harmonic5 = (self.phase * 5.0).sin() * 0.15; // Major third + let harmonic6 = (self.phase * 6.0).sin() * 0.1; // Fifth again + let harmonic7 = (self.phase * 7.0).sin() * 0.08; // Minor seventh + let harmonic8 = (self.phase * 8.0).sin() * 0.06; // Third octave + + // Combine harmonics + let mut sample = fundamental + + harmonic2 + + harmonic3 + + harmonic4 + + harmonic5 + + harmonic6 + + harmonic7 + + harmonic8; + + // Apply subtle saturation for warmth + sample = sample * (1.0 + 0.05 * sample.abs()); + + // Piano-appropriate volume level + sample * 0.6 + } } /// An envelope generator for controlling amplitude over time