Several fixes and new example

This commit is contained in:
2025-06-23 23:26:42 -06:00
parent 1434b0d0ed
commit e695c67363
11 changed files with 1341 additions and 92 deletions

View File

@ -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`

View File

@ -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",

697
examples/enchanted.json Normal file
View File

@ -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
}
}

View File

@ -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))?;
}

View File

@ -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(),
})
}

View File

@ -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(),
}
}
}

View File

@ -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<EffectConfig>,
}
/// 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);

View File

@ -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<dyn AudioEffect>;
}
/// Low-pass filter effect
@ -132,6 +135,10 @@ impl AudioEffect for LowPassFilter {
_ => {}
}
}
fn clone_box(&self) -> Box<dyn AudioEffect> {
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<dyn AudioEffect> {
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<dyn AudioEffect> {
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<dyn AudioEffect> {
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<dyn AudioEffect> {
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<dyn AudioEffect> {
Box::new(self.clone())
}
}
/// Effects chain that can combine multiple effects
#[derive(Debug)]
pub struct EffectsChain {
effects: Vec<Box<dyn AudioEffect>>,
}
@ -735,6 +767,10 @@ impl AudioEffect for EffectsChain {
// For now, we'll just ignore it
let _ = (param, value);
}
fn clone_box(&self) -> Box<dyn AudioEffect> {
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<dyn AudioEffect> {
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<dyn AudioEffect> {
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<dyn AudioEffect> {
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());
}
}

View File

@ -96,6 +96,7 @@ fn show_info(scales: bool, instruments: bool, all: bool) -> Result<(), Box<dyn s
println!(" sawtooth - Sawtooth wave (bright, buzzy)");
println!(" triangle - Triangle wave (mellow, soft)");
println!(" noise - White noise (for percussion, textures)");
println!(" piano - Natural piano sound with harmonics");
println!();
}

View File

@ -4,7 +4,12 @@
//! handle timing, and coordinate multiple tracks and patterns.
use crate::bpm_to_samples_per_beat;
use crate::core::{Composition, InstrumentType};
use crate::config::EffectConfig;
use crate::core::Composition;
use crate::effects::{
AudioEffect, BitCrusher, Chorus, Delay, Distortion, EffectsChain, HighPassFilter,
LowPassFilter, Reverb, TapeSaturation, VinylCrackle,
};
use crate::patterns::MelodicPattern;
use crate::synthesis::{PolySynth, Waveform};
use std::collections::VecDeque;
@ -56,17 +61,193 @@ struct TrackState {
pub volume: f32,
pub is_muted: bool,
pub current_notes: Vec<u8>, // 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<EffectsChain, String> {
let mut effects_chain = EffectsChain::new();
for effect_config in effect_configs {
let effect: Box<dyn AudioEffect> = 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;
}
}

View File

@ -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