Several fixes and new example
This commit is contained in:
@ -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`
|
||||
|
@ -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
697
examples/enchanted.json
Normal 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
|
||||
}
|
||||
}
|
@ -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))?;
|
||||
}
|
||||
|
||||
|
@ -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(),
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
398
src/effects.rs
398
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<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());
|
||||
}
|
||||
}
|
||||
|
@ -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!();
|
||||
}
|
||||
|
||||
|
199
src/sequencer.rs
199
src/sequencer.rs
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
Reference in New Issue
Block a user