Initial commit
This commit is contained in:
593
src/audio.rs
Normal file
593
src/audio.rs
Normal file
@ -0,0 +1,593 @@
|
||||
//! Audio output and file export module
|
||||
//!
|
||||
//! This module provides functionality for real-time audio playback and
|
||||
//! exporting generated music to audio files.
|
||||
|
||||
use crate::SAMPLE_RATE;
|
||||
use crate::core::Composition;
|
||||
use crate::sequencer::Sequencer;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
/// Audio output configuration
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AudioConfig {
|
||||
pub sample_rate: f32,
|
||||
pub buffer_size: usize,
|
||||
pub channels: usize,
|
||||
}
|
||||
|
||||
impl Default for AudioConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
sample_rate: SAMPLE_RATE,
|
||||
buffer_size: 512,
|
||||
channels: 2, // Stereo
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Real-time audio player using cpal
|
||||
pub struct AudioPlayer {
|
||||
sequencer: Arc<Mutex<Sequencer>>,
|
||||
is_playing: bool,
|
||||
}
|
||||
|
||||
impl AudioPlayer {
|
||||
/// Create a new audio player
|
||||
pub fn new(_config: AudioConfig) -> Result<Self, String> {
|
||||
let sequencer = Arc::new(Mutex::new(Sequencer::new(120.0)));
|
||||
|
||||
Ok(Self {
|
||||
sequencer,
|
||||
is_playing: false,
|
||||
})
|
||||
}
|
||||
|
||||
/// Load a composition into the player
|
||||
pub fn load_composition(&mut self, composition: &Composition) -> Result<(), String> {
|
||||
let mut sequencer = self
|
||||
.sequencer
|
||||
.lock()
|
||||
.map_err(|e| format!("Lock error: {}", e))?;
|
||||
sequencer.load_composition(composition)
|
||||
}
|
||||
|
||||
/// Start real-time audio playback
|
||||
pub fn start_playback(&mut self) -> Result<(), String> {
|
||||
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
|
||||
|
||||
let host = cpal::default_host();
|
||||
let device = host
|
||||
.default_output_device()
|
||||
.ok_or("No output device available")?;
|
||||
|
||||
let supported_config = device
|
||||
.default_output_config()
|
||||
.map_err(|e| format!("Failed to get default config: {}", e))?;
|
||||
|
||||
let sample_format = supported_config.sample_format();
|
||||
let config = supported_config.into();
|
||||
|
||||
let sequencer = Arc::clone(&self.sequencer);
|
||||
|
||||
let stream = match sample_format {
|
||||
cpal::SampleFormat::F32 => {
|
||||
device.build_output_stream(
|
||||
&config,
|
||||
move |data: &mut [f32], _: &cpal::OutputCallbackInfo| {
|
||||
if let Ok(mut seq) = sequencer.lock() {
|
||||
let _ = seq.process_audio(data);
|
||||
} else {
|
||||
// Fill with silence if we can't lock
|
||||
for sample in data.iter_mut() {
|
||||
*sample = 0.0;
|
||||
}
|
||||
}
|
||||
},
|
||||
|err| eprintln!("Audio stream error: {}", err),
|
||||
None,
|
||||
)
|
||||
}
|
||||
cpal::SampleFormat::I16 => {
|
||||
device.build_output_stream(
|
||||
&config,
|
||||
move |data: &mut [i16], _: &cpal::OutputCallbackInfo| {
|
||||
let mut float_buffer = vec![0.0f32; data.len()];
|
||||
|
||||
if let Ok(mut seq) = sequencer.lock() {
|
||||
let _ = seq.process_audio(&mut float_buffer);
|
||||
}
|
||||
|
||||
// Convert f32 to i16
|
||||
for (i, &sample) in float_buffer.iter().enumerate() {
|
||||
data[i] = (sample * i16::MAX as f32) as i16;
|
||||
}
|
||||
},
|
||||
|err| eprintln!("Audio stream error: {}", err),
|
||||
None,
|
||||
)
|
||||
}
|
||||
cpal::SampleFormat::U16 => {
|
||||
device.build_output_stream(
|
||||
&config,
|
||||
move |data: &mut [u16], _: &cpal::OutputCallbackInfo| {
|
||||
let mut float_buffer = vec![0.0f32; data.len()];
|
||||
|
||||
if let Ok(mut seq) = sequencer.lock() {
|
||||
let _ = seq.process_audio(&mut float_buffer);
|
||||
}
|
||||
|
||||
// Convert f32 to u16
|
||||
for (i, &sample) in float_buffer.iter().enumerate() {
|
||||
let sample_u16 = ((sample + 1.0) * 0.5 * u16::MAX as f32) as u16;
|
||||
data[i] = sample_u16;
|
||||
}
|
||||
},
|
||||
|err| eprintln!("Audio stream error: {}", err),
|
||||
None,
|
||||
)
|
||||
}
|
||||
_ => {
|
||||
return Err("Unsupported sample format".to_string());
|
||||
}
|
||||
}
|
||||
.map_err(|e| format!("Failed to build stream: {}", e))?;
|
||||
|
||||
stream
|
||||
.play()
|
||||
.map_err(|e| format!("Failed to play stream: {}", e))?;
|
||||
|
||||
// Start the sequencer
|
||||
if let Ok(mut seq) = self.sequencer.lock() {
|
||||
seq.play();
|
||||
}
|
||||
|
||||
self.is_playing = true;
|
||||
|
||||
// Keep the stream alive (in a real application, you'd want better lifecycle management)
|
||||
std::mem::forget(stream);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Stop playback
|
||||
pub fn stop_playback(&mut self) -> Result<(), String> {
|
||||
if let Ok(mut seq) = self.sequencer.lock() {
|
||||
seq.stop();
|
||||
}
|
||||
self.is_playing = false;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Pause playback
|
||||
pub fn pause_playback(&mut self) -> Result<(), String> {
|
||||
if let Ok(mut seq) = self.sequencer.lock() {
|
||||
seq.pause();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Resume playback
|
||||
pub fn resume_playback(&mut self) -> Result<(), String> {
|
||||
if let Ok(mut seq) = self.sequencer.lock() {
|
||||
seq.play();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Set playback position
|
||||
pub fn set_position(&mut self, position: f32) -> Result<(), String> {
|
||||
if let Ok(mut seq) = self.sequencer.lock() {
|
||||
seq.set_position(position);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Set tempo
|
||||
pub fn set_tempo(&mut self, tempo: f32) -> Result<(), String> {
|
||||
if let Ok(mut seq) = self.sequencer.lock() {
|
||||
seq.set_tempo(tempo);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get sequencer for direct control
|
||||
pub fn get_sequencer(&self) -> Arc<Mutex<Sequencer>> {
|
||||
Arc::clone(&self.sequencer)
|
||||
}
|
||||
}
|
||||
|
||||
/// Audio file exporter
|
||||
pub struct AudioExporter {
|
||||
sample_rate: f32,
|
||||
}
|
||||
|
||||
impl AudioExporter {
|
||||
/// Create a new audio exporter
|
||||
pub fn new(sample_rate: f32) -> Self {
|
||||
Self { sample_rate }
|
||||
}
|
||||
|
||||
/// Ensure output directory exists
|
||||
fn ensure_output_dir() -> Result<(), String> {
|
||||
let output_dir = Path::new("output");
|
||||
if !output_dir.exists() {
|
||||
fs::create_dir_all(output_dir)
|
||||
.map_err(|e| format!("Failed to create output directory: {}", e))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get full path for output file
|
||||
fn get_output_path(filename: &str) -> String {
|
||||
format!("output/{}", filename)
|
||||
}
|
||||
|
||||
/// Export a composition to a WAV file
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `composition` - The composition to export
|
||||
/// * `filename` - Output filename
|
||||
/// * `duration_seconds` - Duration to export in seconds (None for full composition)
|
||||
pub fn export_wav(
|
||||
&self,
|
||||
composition: &Composition,
|
||||
filename: &str,
|
||||
duration_seconds: Option<f32>,
|
||||
) -> Result<(), String> {
|
||||
Self::ensure_output_dir()?;
|
||||
let output_path = Self::get_output_path(filename);
|
||||
let spec = hound::WavSpec {
|
||||
channels: 1, // Mono for simplicity
|
||||
sample_rate: self.sample_rate as u32,
|
||||
bits_per_sample: 16,
|
||||
sample_format: hound::SampleFormat::Int,
|
||||
};
|
||||
|
||||
let mut writer = hound::WavWriter::create(&output_path, spec)
|
||||
.map_err(|e| format!("Failed to create WAV file: {}", e))?;
|
||||
|
||||
// Create a sequencer for rendering
|
||||
let mut sequencer = Sequencer::new(composition.params.tempo);
|
||||
sequencer
|
||||
.load_composition(composition)
|
||||
.map_err(|e| format!("Failed to load composition: {}", e))?;
|
||||
|
||||
sequencer.play();
|
||||
|
||||
// Calculate total samples to render
|
||||
let export_duration = duration_seconds
|
||||
.unwrap_or(composition.total_duration * 60.0 / composition.params.tempo);
|
||||
let total_samples = (export_duration * self.sample_rate) as usize;
|
||||
|
||||
// Render audio in chunks
|
||||
let chunk_size = 1024;
|
||||
let mut buffer = vec![0.0f32; chunk_size];
|
||||
let mut samples_rendered = 0;
|
||||
|
||||
while samples_rendered < total_samples {
|
||||
let samples_to_render = (total_samples - samples_rendered).min(chunk_size);
|
||||
buffer.resize(samples_to_render, 0.0);
|
||||
|
||||
// Process audio
|
||||
if let Err(e) = sequencer.process_audio(&mut buffer) {
|
||||
return Err(format!("Audio processing error: {}", e));
|
||||
}
|
||||
|
||||
// Convert to i16 and write to file
|
||||
for &sample in &buffer {
|
||||
let sample_i16 = (sample * i16::MAX as f32) as i16;
|
||||
writer
|
||||
.write_sample(sample_i16)
|
||||
.map_err(|e| format!("Failed to write sample: {}", e))?;
|
||||
}
|
||||
|
||||
samples_rendered += samples_to_render;
|
||||
|
||||
// Progress indication (optional)
|
||||
if samples_rendered % (self.sample_rate as usize) == 0 {
|
||||
let progress = samples_rendered as f32 / total_samples as f32 * 100.0;
|
||||
println!("Export progress: {:.1}%", progress);
|
||||
}
|
||||
}
|
||||
|
||||
writer
|
||||
.finalize()
|
||||
.map_err(|e| format!("Failed to finalize WAV file: {}", e))?;
|
||||
|
||||
println!("Successfully exported to {}", output_path);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Export a composition to a stereo WAV file with separate channels for different tracks
|
||||
pub fn export_stereo_wav(
|
||||
&self,
|
||||
composition: &Composition,
|
||||
filename: &str,
|
||||
duration_seconds: Option<f32>,
|
||||
) -> Result<(), String> {
|
||||
Self::ensure_output_dir()?;
|
||||
let output_path = Self::get_output_path(filename);
|
||||
let spec = hound::WavSpec {
|
||||
channels: 2, // Stereo
|
||||
sample_rate: self.sample_rate as u32,
|
||||
bits_per_sample: 16,
|
||||
sample_format: hound::SampleFormat::Int,
|
||||
};
|
||||
|
||||
let mut writer = hound::WavWriter::create(&output_path, spec)
|
||||
.map_err(|e| format!("Failed to create WAV file: {}", e))?;
|
||||
|
||||
// Create a sequencer for rendering
|
||||
let mut sequencer = Sequencer::new(composition.params.tempo);
|
||||
sequencer
|
||||
.load_composition(composition)
|
||||
.map_err(|e| format!("Failed to load composition: {}", e))?;
|
||||
|
||||
sequencer.play();
|
||||
|
||||
// Calculate total samples to render
|
||||
let export_duration = duration_seconds
|
||||
.unwrap_or(composition.total_duration * 60.0 / composition.params.tempo);
|
||||
let total_samples = (export_duration * self.sample_rate) as usize;
|
||||
|
||||
// Render audio in chunks
|
||||
let chunk_size = 1024;
|
||||
let mut buffer = vec![0.0f32; chunk_size];
|
||||
let mut samples_rendered = 0;
|
||||
|
||||
while samples_rendered < total_samples {
|
||||
let samples_to_render = (total_samples - samples_rendered).min(chunk_size);
|
||||
buffer.resize(samples_to_render, 0.0);
|
||||
|
||||
// Process audio
|
||||
if let Err(e) = sequencer.process_audio(&mut buffer) {
|
||||
return Err(format!("Audio processing error: {}", e));
|
||||
}
|
||||
|
||||
// Write stereo samples (duplicate mono to both channels)
|
||||
for &sample in &buffer {
|
||||
let sample_i16 = (sample * i16::MAX as f32) as i16;
|
||||
writer
|
||||
.write_sample(sample_i16) // Left channel
|
||||
.map_err(|e| format!("Failed to write left sample: {}", e))?;
|
||||
writer
|
||||
.write_sample(sample_i16) // Right channel
|
||||
.map_err(|e| format!("Failed to write right sample: {}", e))?;
|
||||
}
|
||||
|
||||
samples_rendered += samples_to_render;
|
||||
|
||||
// Progress indication
|
||||
if samples_rendered % (self.sample_rate as usize) == 0 {
|
||||
let progress = samples_rendered as f32 / total_samples as f32 * 100.0;
|
||||
println!("Export progress: {:.1}%", progress);
|
||||
}
|
||||
}
|
||||
|
||||
writer
|
||||
.finalize()
|
||||
.map_err(|e| format!("Failed to finalize WAV file: {}", e))?;
|
||||
|
||||
println!("Successfully exported stereo to {}", output_path);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Export multiple takes of a composition with different parameters
|
||||
pub fn export_variations(
|
||||
&self,
|
||||
base_composition: &Composition,
|
||||
filename_prefix: &str,
|
||||
variations: usize,
|
||||
duration_seconds: Option<f32>,
|
||||
) -> Result<(), String> {
|
||||
Self::ensure_output_dir()?;
|
||||
|
||||
for i in 0..variations {
|
||||
// Create variation by modifying parameters
|
||||
let mut params = base_composition.params.clone();
|
||||
params.complexity = (i as f32 / variations as f32).clamp(0.1, 1.0);
|
||||
params.rhythmic_density = 0.5 + (i as f32 / variations as f32) * 0.4;
|
||||
|
||||
let mut variation = Composition::new(params);
|
||||
variation
|
||||
.generate()
|
||||
.map_err(|e| format!("Failed to generate variation {}: {}", i, e))?;
|
||||
|
||||
let filename = format!("{}_{:02}.wav", filename_prefix, i + 1);
|
||||
self.export_wav(&variation, &filename, duration_seconds)?;
|
||||
}
|
||||
|
||||
println!("Successfully exported {} variations", variations);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Export composition as raw audio data (for further processing)
|
||||
pub fn export_raw_audio(
|
||||
&self,
|
||||
composition: &Composition,
|
||||
duration_seconds: Option<f32>,
|
||||
) -> Result<Vec<f32>, String> {
|
||||
// Create a sequencer for rendering
|
||||
let mut sequencer = Sequencer::new(composition.params.tempo);
|
||||
sequencer
|
||||
.load_composition(composition)
|
||||
.map_err(|e| format!("Failed to load composition: {}", e))?;
|
||||
|
||||
sequencer.play();
|
||||
|
||||
// Calculate total samples to render
|
||||
let export_duration = duration_seconds
|
||||
.unwrap_or(composition.total_duration * 60.0 / composition.params.tempo);
|
||||
let total_samples = (export_duration * self.sample_rate) as usize;
|
||||
|
||||
let mut audio_data = Vec::with_capacity(total_samples);
|
||||
|
||||
// Render audio in chunks
|
||||
let chunk_size = 1024;
|
||||
let mut buffer = vec![0.0f32; chunk_size];
|
||||
let mut samples_rendered = 0;
|
||||
|
||||
while samples_rendered < total_samples {
|
||||
let samples_to_render = (total_samples - samples_rendered).min(chunk_size);
|
||||
buffer.resize(samples_to_render, 0.0);
|
||||
|
||||
// Process audio
|
||||
if let Err(e) = sequencer.process_audio(&mut buffer) {
|
||||
return Err(format!("Audio processing error: {}", e));
|
||||
}
|
||||
|
||||
audio_data.extend_from_slice(&buffer);
|
||||
samples_rendered += samples_to_render;
|
||||
}
|
||||
|
||||
Ok(audio_data)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for AudioExporter {
|
||||
fn default() -> Self {
|
||||
Self::new(SAMPLE_RATE)
|
||||
}
|
||||
}
|
||||
|
||||
/// Simple audio analysis utilities
|
||||
pub struct AudioAnalyzer;
|
||||
|
||||
impl AudioAnalyzer {
|
||||
/// Calculate RMS (Root Mean Square) level of audio data
|
||||
pub fn calculate_rms(audio_data: &[f32]) -> f32 {
|
||||
if audio_data.is_empty() {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
let sum_squares: f32 = audio_data.iter().map(|&x| x * x).sum();
|
||||
(sum_squares / audio_data.len() as f32).sqrt()
|
||||
}
|
||||
|
||||
/// Find peak amplitude in audio data
|
||||
pub fn find_peak(audio_data: &[f32]) -> f32 {
|
||||
audio_data.iter().map(|&x| x.abs()).fold(0.0, f32::max)
|
||||
}
|
||||
|
||||
/// Calculate dynamic range (ratio of peak to RMS)
|
||||
pub fn calculate_dynamic_range(audio_data: &[f32]) -> f32 {
|
||||
let peak = Self::find_peak(audio_data);
|
||||
let rms = Self::calculate_rms(audio_data);
|
||||
|
||||
if rms > 0.0 {
|
||||
20.0 * (peak / rms).log10() // In dB
|
||||
} else {
|
||||
f32::INFINITY
|
||||
}
|
||||
}
|
||||
|
||||
/// Simple frequency analysis using zero-crossing rate
|
||||
pub fn zero_crossing_rate(audio_data: &[f32]) -> f32 {
|
||||
if audio_data.len() < 2 {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
let mut crossings = 0;
|
||||
for i in 1..audio_data.len() {
|
||||
if (audio_data[i] >= 0.0) != (audio_data[i - 1] >= 0.0) {
|
||||
crossings += 1;
|
||||
}
|
||||
}
|
||||
|
||||
crossings as f32 / audio_data.len() as f32
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::core::{CompositionBuilder, CompositionStyle};
|
||||
|
||||
#[test]
|
||||
fn test_audio_config() {
|
||||
let config = AudioConfig::default();
|
||||
assert_eq!(config.sample_rate, SAMPLE_RATE);
|
||||
assert_eq!(config.channels, 2);
|
||||
assert!(config.buffer_size > 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_audio_exporter_creation() {
|
||||
let exporter = AudioExporter::new(44100.0);
|
||||
assert_eq!(exporter.sample_rate, 44100.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_raw_audio_export() {
|
||||
let mut composition = CompositionBuilder::new()
|
||||
.style(CompositionStyle::Electronic)
|
||||
.measures(2)
|
||||
.tempo(120.0)
|
||||
.build();
|
||||
|
||||
let _ = composition.generate();
|
||||
|
||||
let exporter = AudioExporter::new(44100.0);
|
||||
let result = exporter.export_raw_audio(&composition, Some(1.0));
|
||||
|
||||
assert!(result.is_ok());
|
||||
let audio_data = result.unwrap();
|
||||
assert_eq!(audio_data.len(), 44100); // 1 second at 44.1kHz
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_audio_analysis() {
|
||||
// Test with a simple sine wave
|
||||
let sample_rate = 44100.0;
|
||||
let frequency = 440.0;
|
||||
let duration = 1.0;
|
||||
let samples = (sample_rate * duration) as usize;
|
||||
|
||||
let mut audio_data = Vec::with_capacity(samples);
|
||||
for i in 0..samples {
|
||||
let t = i as f32 / sample_rate;
|
||||
let sample = (2.0 * std::f32::consts::PI * frequency * t).sin() * 0.5;
|
||||
audio_data.push(sample);
|
||||
}
|
||||
|
||||
let rms = AudioAnalyzer::calculate_rms(&audio_data);
|
||||
let peak = AudioAnalyzer::find_peak(&audio_data);
|
||||
let zcr = AudioAnalyzer::zero_crossing_rate(&audio_data);
|
||||
|
||||
assert!(rms > 0.0);
|
||||
assert!(peak > rms);
|
||||
assert!(zcr > 0.0);
|
||||
|
||||
// For a sine wave, RMS should be approximately peak / sqrt(2)
|
||||
let expected_rms = 0.5 / (2.0_f32).sqrt();
|
||||
assert!((rms - expected_rms).abs() < 0.01);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dynamic_range_calculation() {
|
||||
let audio_data = vec![0.0, 0.5, -0.3, 0.8, -0.2, 0.1];
|
||||
let dynamic_range = AudioAnalyzer::calculate_dynamic_range(&audio_data);
|
||||
assert!(dynamic_range > 0.0);
|
||||
assert!(dynamic_range.is_finite());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_zero_crossing_rate() {
|
||||
// Test with alternating positive/negative values
|
||||
let audio_data = vec![1.0, -1.0, 1.0, -1.0, 1.0, -1.0];
|
||||
let zcr = AudioAnalyzer::zero_crossing_rate(&audio_data);
|
||||
|
||||
// Should have high zero-crossing rate
|
||||
assert!(zcr > 0.5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_audio_player_creation() {
|
||||
let config = AudioConfig::default();
|
||||
let result = AudioPlayer::new(config);
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
}
|
461
src/composition.rs
Normal file
461
src/composition.rs
Normal file
@ -0,0 +1,461 @@
|
||||
//! Track-based composition module
|
||||
//!
|
||||
//! This module provides direct control over musical tracks and patterns
|
||||
|
||||
use crate::config::{CompositionConfig, TrackConfig, TrackPattern};
|
||||
use crate::core::{
|
||||
Composition as CoreComposition, CompositionParams, CompositionStyle, InstrumentType, Note,
|
||||
Track,
|
||||
};
|
||||
use crate::scales::{Scale, ScaleType};
|
||||
|
||||
/// Track-based composition
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Composition {
|
||||
pub key: u8,
|
||||
pub scale_type: ScaleType,
|
||||
pub tempo: f32,
|
||||
pub time_signature: (u8, u8),
|
||||
pub measures: usize,
|
||||
pub track_configs: Vec<TrackConfig>,
|
||||
pub tracks: Vec<Track>,
|
||||
pub total_duration: f32,
|
||||
}
|
||||
|
||||
impl Composition {
|
||||
/// Create a new track composition from configuration
|
||||
pub fn from_config(config: CompositionConfig) -> Result<Self, String> {
|
||||
let key = config.composition.key.to_midi()?;
|
||||
let scale_type = parse_scale_type(&config.composition.scale)?;
|
||||
|
||||
let total_duration =
|
||||
config.composition.measures as f32 * config.composition.time_signature.numerator as f32;
|
||||
|
||||
Ok(Self {
|
||||
key,
|
||||
scale_type,
|
||||
tempo: config.composition.tempo,
|
||||
time_signature: (
|
||||
config.composition.time_signature.numerator,
|
||||
config.composition.time_signature.denominator,
|
||||
),
|
||||
measures: config.composition.measures,
|
||||
track_configs: config.tracks,
|
||||
tracks: Vec::new(),
|
||||
total_duration,
|
||||
})
|
||||
}
|
||||
|
||||
/// Generate the composition by processing all tracks
|
||||
pub fn generate(&mut self) -> Result<(), String> {
|
||||
self.tracks.clear();
|
||||
|
||||
let scale = Scale::new(self.scale_type, self.key);
|
||||
|
||||
for track_config in &self.track_configs {
|
||||
let track = self.generate_track(track_config, &scale)?;
|
||||
self.tracks.push(track);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Generate a track from a track configuration
|
||||
fn generate_track(&self, track: &TrackConfig, scale: &Scale) -> Result<Track, String> {
|
||||
let notes = match track.pattern.pattern_type.as_str() {
|
||||
"custom" => self.generate_custom_pattern(&track.pattern)?,
|
||||
"chord" => self.generate_chord_pattern(&track.pattern, scale)?,
|
||||
"arpeggio" => self.generate_arpeggio_pattern(&track.pattern, scale)?,
|
||||
"sequence" => self.generate_sequence_pattern(&track.pattern, scale)?,
|
||||
_ => {
|
||||
return Err(format!(
|
||||
"Unknown pattern type: {}",
|
||||
track.pattern.pattern_type
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
let instrument_type = match track.name.to_lowercase().as_str() {
|
||||
name if name.contains("bass") => InstrumentType::Bass,
|
||||
name if name.contains("lead") => InstrumentType::Lead,
|
||||
name if name.contains("pad") => InstrumentType::Pad,
|
||||
name if name.contains("arp") => InstrumentType::Arp,
|
||||
name if name.contains("drum")
|
||||
|| name.contains("kick")
|
||||
|| name.contains("snare")
|
||||
|| name.contains("hat") =>
|
||||
{
|
||||
InstrumentType::Percussion
|
||||
}
|
||||
_ => InstrumentType::Lead,
|
||||
};
|
||||
|
||||
Ok(Track {
|
||||
name: track.name.clone(),
|
||||
octave: 4, // Will be overridden by note specifications
|
||||
notes,
|
||||
volume: track.volume,
|
||||
instrument_type,
|
||||
})
|
||||
}
|
||||
|
||||
/// Generate notes from a custom pattern
|
||||
fn generate_custom_pattern(&self, pattern: &TrackPattern) -> Result<Vec<Note>, String> {
|
||||
let mut notes = Vec::new();
|
||||
let loop_count = (self.total_duration / pattern.loop_length).ceil() as usize;
|
||||
|
||||
for loop_iteration in 0..loop_count {
|
||||
let loop_offset = loop_iteration as f32 * pattern.loop_length;
|
||||
|
||||
for step in &pattern.steps {
|
||||
let absolute_time = loop_offset + step.time;
|
||||
|
||||
// Don't generate notes beyond the composition length
|
||||
if absolute_time >= self.total_duration {
|
||||
break;
|
||||
}
|
||||
|
||||
let midi_note = parse_note(&step.note)?;
|
||||
|
||||
notes.push(Note::new(
|
||||
midi_note,
|
||||
absolute_time,
|
||||
step.duration,
|
||||
step.velocity,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(notes)
|
||||
}
|
||||
|
||||
/// Generate notes from a chord pattern
|
||||
fn generate_chord_pattern(
|
||||
&self,
|
||||
pattern: &TrackPattern,
|
||||
_scale: &Scale,
|
||||
) -> Result<Vec<Note>, String> {
|
||||
let mut notes = Vec::new();
|
||||
let loop_count = (self.total_duration / pattern.loop_length).ceil() as usize;
|
||||
|
||||
for loop_iteration in 0..loop_count {
|
||||
let loop_offset = loop_iteration as f32 * pattern.loop_length;
|
||||
|
||||
for chord_step in &pattern.chord_progression {
|
||||
let absolute_time = loop_offset + chord_step.time;
|
||||
|
||||
if absolute_time >= self.total_duration {
|
||||
break;
|
||||
}
|
||||
|
||||
let chord_notes = parse_chord(&chord_step.chord, pattern.octave.unwrap_or(4))?;
|
||||
|
||||
for &chord_note in &chord_notes {
|
||||
notes.push(Note::new(
|
||||
chord_note,
|
||||
absolute_time,
|
||||
chord_step.duration,
|
||||
0.7, // Default chord velocity
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(notes)
|
||||
}
|
||||
|
||||
/// Generate notes from an arpeggio pattern
|
||||
fn generate_arpeggio_pattern(
|
||||
&self,
|
||||
pattern: &TrackPattern,
|
||||
_scale: &Scale,
|
||||
) -> Result<Vec<Note>, String> {
|
||||
let mut notes = Vec::new();
|
||||
let loop_count = (self.total_duration / pattern.loop_length).ceil() as usize;
|
||||
|
||||
for loop_iteration in 0..loop_count {
|
||||
let loop_offset = loop_iteration as f32 * pattern.loop_length;
|
||||
|
||||
for chord_step in &pattern.chord_progression {
|
||||
let chord_start_time = loop_offset + chord_step.time;
|
||||
|
||||
if chord_start_time >= self.total_duration {
|
||||
break;
|
||||
}
|
||||
|
||||
let chord_notes = parse_chord(&chord_step.chord, pattern.octave.unwrap_or(4))?;
|
||||
let note_duration = chord_step.duration / chord_notes.len() as f32;
|
||||
|
||||
for (i, &chord_note) in chord_notes.iter().enumerate() {
|
||||
let note_time = chord_start_time + (i as f32 * note_duration);
|
||||
|
||||
if note_time >= self.total_duration {
|
||||
break;
|
||||
}
|
||||
|
||||
notes.push(Note::new(
|
||||
chord_note,
|
||||
note_time,
|
||||
note_duration * 0.9, // Slightly staccato
|
||||
0.6,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(notes)
|
||||
}
|
||||
|
||||
/// Generate notes from a sequence pattern (placeholder)
|
||||
fn generate_sequence_pattern(
|
||||
&self,
|
||||
pattern: &TrackPattern,
|
||||
_scale: &Scale,
|
||||
) -> Result<Vec<Note>, String> {
|
||||
// For now, treat sequence patterns like custom patterns
|
||||
// This can be extended to support step sequencer-style patterns
|
||||
self.generate_custom_pattern(pattern)
|
||||
}
|
||||
|
||||
/// Get all notes from all tracks sorted by start time
|
||||
pub fn get_all_notes(&self) -> Vec<Note> {
|
||||
let mut all_notes = Vec::new();
|
||||
|
||||
for track in &self.tracks {
|
||||
all_notes.extend(track.notes.clone());
|
||||
}
|
||||
|
||||
all_notes.sort_by(|a, b| a.start_time.partial_cmp(&b.start_time).unwrap());
|
||||
all_notes
|
||||
}
|
||||
|
||||
/// Get composition statistics
|
||||
pub fn get_stats(&self) -> CompositionStats {
|
||||
let total_notes = self.tracks.iter().map(|t| t.notes.len()).sum();
|
||||
|
||||
CompositionStats {
|
||||
total_notes,
|
||||
total_duration: self.total_duration,
|
||||
track_count: self.tracks.len(),
|
||||
measures: self.measures,
|
||||
tempo: self.tempo,
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert to old Composition format for audio export compatibility
|
||||
pub fn to_composition(&self) -> CoreComposition {
|
||||
let params = CompositionParams {
|
||||
style: CompositionStyle::Electronic, // Default fallback
|
||||
key: self.key,
|
||||
scale_type: self.scale_type,
|
||||
tempo: self.tempo,
|
||||
time_signature: self.time_signature,
|
||||
measures: self.measures,
|
||||
track_count: self.tracks.len().max(1),
|
||||
complexity: 0.6, // Default values since tracks don't use these
|
||||
harmony_density: 0.7,
|
||||
rhythmic_density: 0.7,
|
||||
};
|
||||
|
||||
let mut composition = CoreComposition::new(params);
|
||||
composition.tracks = self.tracks.clone();
|
||||
composition.total_duration = self.total_duration;
|
||||
|
||||
composition
|
||||
}
|
||||
}
|
||||
|
||||
/// Statistics about a track composition
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CompositionStats {
|
||||
pub total_notes: usize,
|
||||
pub total_duration: f32,
|
||||
pub track_count: usize,
|
||||
pub measures: usize,
|
||||
pub tempo: f32,
|
||||
}
|
||||
|
||||
/// Parse a note string into a MIDI note number
|
||||
fn parse_note(note_str: &str) -> Result<u8, String> {
|
||||
// Try parsing as MIDI number first
|
||||
if let Ok(midi_num) = note_str.parse::<u8>() {
|
||||
if midi_num <= 127 {
|
||||
return Ok(midi_num);
|
||||
} else {
|
||||
return Err(format!(
|
||||
"MIDI note number {} out of range (0-127)",
|
||||
midi_num
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// Parse as note name (e.g., "C4", "F#3", "Bb2")
|
||||
parse_note_name(note_str)
|
||||
}
|
||||
|
||||
/// Parse note name to MIDI number
|
||||
fn parse_note_name(name: &str) -> Result<u8, String> {
|
||||
let name = name.trim().to_uppercase();
|
||||
|
||||
// Extract note, accidental, and octave
|
||||
let (note_part, octave_str) = name.split_at(
|
||||
name.find(|c: char| c.is_ascii_digit())
|
||||
.ok_or_else(|| format!("Invalid note name: {} (no digit found)", name))?,
|
||||
);
|
||||
|
||||
let octave: i32 = octave_str
|
||||
.parse()
|
||||
.map_err(|_| format!("Invalid octave: {}", octave_str))?;
|
||||
|
||||
// Base MIDI numbers for C in each octave
|
||||
let base_midi = (octave + 1) * 12;
|
||||
|
||||
// Parse note and accidental
|
||||
let (note_char, accidental) = if note_part.ends_with('#') || note_part.ends_with('♯') {
|
||||
(¬e_part[0..1], 1)
|
||||
} else if note_part.ends_with('b') || note_part.ends_with('♭') || note_part.ends_with('B') {
|
||||
(¬e_part[0..1], -1)
|
||||
} else {
|
||||
(note_part, 0)
|
||||
};
|
||||
|
||||
// Note offsets from C
|
||||
let note_offset = match note_char {
|
||||
"C" => 0,
|
||||
"D" => 2,
|
||||
"E" => 4,
|
||||
"F" => 5,
|
||||
"G" => 7,
|
||||
"A" => 9,
|
||||
"B" => 11,
|
||||
_ => return Err(format!("Invalid note: {}", note_char)),
|
||||
};
|
||||
|
||||
let midi = base_midi + note_offset as i32 + accidental as i32;
|
||||
if midi < 0 || midi > 127 {
|
||||
return Err(format!("MIDI note {} out of range (0-127)", midi));
|
||||
}
|
||||
|
||||
Ok(midi as u8)
|
||||
}
|
||||
|
||||
/// Parse a chord name into MIDI note numbers
|
||||
fn parse_chord(chord_name: &str, octave: u8) -> Result<Vec<u8>, String> {
|
||||
// Simple chord parsing - this can be extended for more complex chords
|
||||
let chord_name = chord_name.trim();
|
||||
|
||||
// Extract root note
|
||||
let root_char = chord_name
|
||||
.chars()
|
||||
.next()
|
||||
.ok_or_else(|| format!("Empty chord name: {}", chord_name))?;
|
||||
|
||||
let mut pos = 1;
|
||||
let mut root_accidental = 0;
|
||||
|
||||
// Check for accidental
|
||||
if let Some(second_char) = chord_name.chars().nth(1) {
|
||||
match second_char {
|
||||
'#' | '♯' => {
|
||||
root_accidental = 1;
|
||||
pos = 2;
|
||||
}
|
||||
'b' | '♭' => {
|
||||
root_accidental = -1;
|
||||
pos = 2;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
// Get root MIDI note
|
||||
let root_offset = match root_char.to_uppercase().next().unwrap() {
|
||||
'C' => 0,
|
||||
'D' => 2,
|
||||
'E' => 4,
|
||||
'F' => 5,
|
||||
'G' => 7,
|
||||
'A' => 9,
|
||||
'B' => 11,
|
||||
_ => return Err(format!("Invalid root note: {}", root_char)),
|
||||
};
|
||||
|
||||
let root_midi = ((octave as i32 + 1) * 12 + root_offset + root_accidental) as u8;
|
||||
|
||||
// Parse chord quality
|
||||
let quality = &chord_name[pos..];
|
||||
|
||||
let intervals = match quality.to_lowercase().as_str() {
|
||||
"" | "maj" | "major" => vec![0, 4, 7], // Major triad
|
||||
"m" | "min" | "minor" => vec![0, 3, 7], // Minor triad
|
||||
"7" | "dom7" => vec![0, 4, 7, 10], // Dominant 7th
|
||||
"maj7" | "M7" => vec![0, 4, 7, 11], // Major 7th
|
||||
"m7" | "min7" => vec![0, 3, 7, 10], // Minor 7th
|
||||
"dim" | "°" => vec![0, 3, 6], // Diminished
|
||||
"aug" | "+" => vec![0, 4, 8], // Augmented
|
||||
"sus2" => vec![0, 2, 7], // Suspended 2nd
|
||||
"sus4" => vec![0, 5, 7], // Suspended 4th
|
||||
_ => vec![0, 4, 7], // Default to major if unknown
|
||||
};
|
||||
|
||||
let mut chord_notes = Vec::new();
|
||||
for &interval in &intervals {
|
||||
let note = root_midi + interval;
|
||||
if note <= 127 {
|
||||
chord_notes.push(note);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(chord_notes)
|
||||
}
|
||||
|
||||
/// Parse scale type from string
|
||||
fn parse_scale_type(scale_str: &str) -> Result<ScaleType, String> {
|
||||
match scale_str.to_lowercase().as_str() {
|
||||
"major" => Ok(ScaleType::Major),
|
||||
"minor" => Ok(ScaleType::Minor),
|
||||
"dorian" => Ok(ScaleType::Dorian),
|
||||
"phrygian" => Ok(ScaleType::Phrygian),
|
||||
"lydian" => Ok(ScaleType::Lydian),
|
||||
"mixolydian" => Ok(ScaleType::Mixolydian),
|
||||
"aeolian" => Ok(ScaleType::Aeolian),
|
||||
"locrian" => Ok(ScaleType::Locrian),
|
||||
"pentatonic" => Ok(ScaleType::Pentatonic),
|
||||
"blues" => Ok(ScaleType::Blues),
|
||||
"chromatic" => Ok(ScaleType::Chromatic),
|
||||
_ => Err(format!("Unknown scale: {}", scale_str)),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_note_parsing() {
|
||||
assert_eq!(parse_note("60").unwrap(), 60);
|
||||
assert_eq!(parse_note("C4").unwrap(), 60);
|
||||
assert_eq!(parse_note("A4").unwrap(), 69);
|
||||
assert_eq!(parse_note("C#4").unwrap(), 61);
|
||||
assert_eq!(parse_note("Bb3").unwrap(), 58);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_chord_parsing() {
|
||||
let c_major = parse_chord("C", 4).unwrap();
|
||||
assert_eq!(c_major, vec![60, 64, 67]); // C4, E4, G4
|
||||
|
||||
let d_minor = parse_chord("Dm", 4).unwrap();
|
||||
assert_eq!(d_minor, vec![62, 65, 69]); // D4, F4, A4
|
||||
|
||||
let g7 = parse_chord("G7", 4).unwrap();
|
||||
assert_eq!(g7, vec![67, 71, 74, 77]); // G4, B4, D5, F5
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_scale_type_parsing() {
|
||||
assert_eq!(parse_scale_type("major").unwrap(), ScaleType::Major);
|
||||
assert_eq!(parse_scale_type("minor").unwrap(), ScaleType::Minor);
|
||||
assert_eq!(parse_scale_type("blues").unwrap(), ScaleType::Blues);
|
||||
}
|
||||
}
|
566
src/config.rs
Normal file
566
src/config.rs
Normal file
@ -0,0 +1,566 @@
|
||||
//! JSON configuration module for composition specifications
|
||||
//!
|
||||
//! This module provides structures and parsing for JSON-based composition definitions,
|
||||
//! allowing users to specify complete compositions in a declarative format.
|
||||
|
||||
use crate::core::{CompositionParams, CompositionStyle};
|
||||
use crate::scales::ScaleType;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
/// Main configuration structure for a composition
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CompositionConfig {
|
||||
/// Metadata about the composition
|
||||
#[serde(default)]
|
||||
pub metadata: Metadata,
|
||||
|
||||
/// Global composition settings
|
||||
pub composition: CompositionSettings,
|
||||
|
||||
/// Track definitions
|
||||
#[serde(default)]
|
||||
pub tracks: Vec<TrackConfig>,
|
||||
|
||||
/// Optional section definitions for structured compositions
|
||||
#[serde(default)]
|
||||
pub sections: Vec<SectionConfig>,
|
||||
|
||||
/// Export settings
|
||||
#[serde(default)]
|
||||
pub export: ExportSettings,
|
||||
}
|
||||
|
||||
/// Metadata about the composition
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct Metadata {
|
||||
#[serde(default)]
|
||||
pub title: String,
|
||||
|
||||
#[serde(default)]
|
||||
pub artist: String,
|
||||
|
||||
#[serde(default)]
|
||||
pub description: String,
|
||||
|
||||
#[serde(default)]
|
||||
pub tags: Vec<String>,
|
||||
}
|
||||
|
||||
/// Core composition settings
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CompositionSettings {
|
||||
/// Musical key (MIDI note number or note name)
|
||||
pub key: KeySpec,
|
||||
|
||||
/// Scale type
|
||||
pub scale: String,
|
||||
|
||||
/// Tempo in BPM
|
||||
pub tempo: f32,
|
||||
|
||||
/// Time signature
|
||||
#[serde(default = "default_time_signature")]
|
||||
pub time_signature: TimeSignature,
|
||||
|
||||
/// Number of measures
|
||||
pub measures: usize,
|
||||
|
||||
/// Complexity (0.0 to 1.0)
|
||||
#[serde(default = "default_complexity")]
|
||||
pub complexity: f32,
|
||||
|
||||
/// Harmony density (0.0 to 1.0)
|
||||
#[serde(default = "default_harmony_density")]
|
||||
pub harmony_density: f32,
|
||||
|
||||
/// Rhythmic density (0.0 to 1.0)
|
||||
#[serde(default = "default_rhythmic_density")]
|
||||
pub rhythmic_density: f32,
|
||||
|
||||
/// Random seed for reproducible generation
|
||||
#[serde(default)]
|
||||
pub seed: Option<u64>,
|
||||
}
|
||||
|
||||
/// Key specification - can be either a MIDI number or note name
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum KeySpec {
|
||||
Midi(u8),
|
||||
NoteName(String),
|
||||
}
|
||||
|
||||
impl KeySpec {
|
||||
/// Convert to MIDI note number
|
||||
pub fn to_midi(&self) -> Result<u8, String> {
|
||||
match self {
|
||||
KeySpec::Midi(n) => Ok(*n),
|
||||
KeySpec::NoteName(name) => parse_note_name(name),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Time signature specification
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct TimeSignature {
|
||||
pub numerator: u8,
|
||||
pub denominator: u8,
|
||||
}
|
||||
|
||||
impl Default for TimeSignature {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
numerator: 4,
|
||||
denominator: 4,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Track configuration for direct control over individual instruments
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct TrackConfig {
|
||||
/// Track name
|
||||
pub name: String,
|
||||
|
||||
/// Instrument type
|
||||
pub instrument: String,
|
||||
|
||||
/// Volume (0.0 to 1.0)
|
||||
#[serde(default = "default_volume")]
|
||||
pub volume: f32,
|
||||
|
||||
/// Pattern definition
|
||||
pub pattern: TrackPattern,
|
||||
|
||||
/// Effects to apply
|
||||
#[serde(default)]
|
||||
pub effects: Vec<EffectConfig>,
|
||||
}
|
||||
|
||||
/// Pattern definition for a track
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct TrackPattern {
|
||||
/// Pattern type
|
||||
#[serde(rename = "type")]
|
||||
pub pattern_type: String,
|
||||
|
||||
/// Pattern steps for custom patterns
|
||||
#[serde(default)]
|
||||
pub steps: Vec<PatternStep>,
|
||||
|
||||
/// Loop length in beats
|
||||
#[serde(default = "default_loop_length")]
|
||||
pub loop_length: f32,
|
||||
|
||||
/// Chord progression for chord patterns
|
||||
#[serde(default)]
|
||||
pub chord_progression: Vec<ChordStep>,
|
||||
|
||||
/// Chord voicing style
|
||||
#[serde(default)]
|
||||
pub voicing: Option<String>,
|
||||
|
||||
/// Octave for chord patterns
|
||||
#[serde(default)]
|
||||
pub octave: Option<u8>,
|
||||
}
|
||||
|
||||
/// Individual pattern step
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PatternStep {
|
||||
/// Time in beats
|
||||
pub time: f32,
|
||||
|
||||
/// Note (MIDI number or note name)
|
||||
pub note: String,
|
||||
|
||||
/// Duration in beats
|
||||
pub duration: f32,
|
||||
|
||||
/// Velocity (0.0 to 1.0)
|
||||
pub velocity: f32,
|
||||
}
|
||||
|
||||
/// Chord progression step
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ChordStep {
|
||||
/// Time in beats
|
||||
pub time: f32,
|
||||
|
||||
/// Chord name
|
||||
pub chord: String,
|
||||
|
||||
/// Duration in beats
|
||||
pub duration: f32,
|
||||
}
|
||||
|
||||
/// Section definition for structured compositions
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SectionConfig {
|
||||
/// Section name (e.g., "intro", "verse", "chorus")
|
||||
pub name: String,
|
||||
|
||||
/// Number of measures in this section
|
||||
pub measures: usize,
|
||||
|
||||
/// Optional tempo change
|
||||
#[serde(default)]
|
||||
pub tempo: Option<f32>,
|
||||
|
||||
/// Optional key change
|
||||
#[serde(default)]
|
||||
pub key: Option<KeySpec>,
|
||||
|
||||
/// Optional scale change
|
||||
#[serde(default)]
|
||||
pub scale: Option<String>,
|
||||
|
||||
/// Complexity override for this section
|
||||
#[serde(default)]
|
||||
pub complexity: Option<f32>,
|
||||
|
||||
/// Number of times to repeat this section
|
||||
#[serde(default = "default_repeat")]
|
||||
pub repeat: usize,
|
||||
}
|
||||
|
||||
/// Pattern configuration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PatternConfig {
|
||||
/// Pattern type
|
||||
#[serde(rename = "type")]
|
||||
pub pattern_type: String,
|
||||
|
||||
/// Pattern-specific parameters
|
||||
#[serde(flatten)]
|
||||
pub params: serde_json::Value,
|
||||
}
|
||||
|
||||
/// Effect configuration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct EffectConfig {
|
||||
/// Effect type
|
||||
#[serde(rename = "type")]
|
||||
pub effect_type: String,
|
||||
|
||||
/// Effect parameters
|
||||
#[serde(flatten)]
|
||||
pub params: serde_json::Value,
|
||||
}
|
||||
|
||||
/// Export settings
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ExportSettings {
|
||||
/// Output filename (without extension)
|
||||
#[serde(default = "default_filename")]
|
||||
pub filename: String,
|
||||
|
||||
/// Export format
|
||||
#[serde(default = "default_format")]
|
||||
pub format: String,
|
||||
|
||||
/// Sample rate
|
||||
#[serde(default = "default_sample_rate")]
|
||||
pub sample_rate: u32,
|
||||
|
||||
/// Bit depth
|
||||
#[serde(default = "default_bit_depth")]
|
||||
pub bit_depth: u16,
|
||||
|
||||
/// Stereo or mono
|
||||
#[serde(default = "default_stereo")]
|
||||
pub stereo: bool,
|
||||
|
||||
/// Maximum duration in seconds (None for full composition)
|
||||
#[serde(default)]
|
||||
pub max_duration: Option<f32>,
|
||||
|
||||
/// Generate variations
|
||||
#[serde(default)]
|
||||
pub variations: Option<VariationSettings>,
|
||||
}
|
||||
|
||||
/// Variation generation settings
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct VariationSettings {
|
||||
/// Number of variations to generate
|
||||
pub count: usize,
|
||||
|
||||
/// Variation parameters
|
||||
#[serde(default)]
|
||||
pub vary_complexity: bool,
|
||||
|
||||
#[serde(default)]
|
||||
pub vary_rhythm: bool,
|
||||
|
||||
#[serde(default)]
|
||||
pub vary_harmony: bool,
|
||||
|
||||
#[serde(default)]
|
||||
pub vary_tempo: bool,
|
||||
}
|
||||
|
||||
impl Default for ExportSettings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
filename: default_filename(),
|
||||
format: default_format(),
|
||||
sample_rate: default_sample_rate(),
|
||||
bit_depth: default_bit_depth(),
|
||||
stereo: default_stereo(),
|
||||
max_duration: None,
|
||||
variations: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Default value functions
|
||||
fn default_complexity() -> f32 {
|
||||
0.6
|
||||
}
|
||||
fn default_harmony_density() -> f32 {
|
||||
0.7
|
||||
}
|
||||
fn default_rhythmic_density() -> f32 {
|
||||
0.8
|
||||
}
|
||||
fn default_volume() -> f32 {
|
||||
1.0
|
||||
}
|
||||
fn default_loop_length() -> f32 {
|
||||
4.0
|
||||
}
|
||||
fn default_repeat() -> usize {
|
||||
1
|
||||
}
|
||||
fn default_filename() -> String {
|
||||
"output".to_string()
|
||||
}
|
||||
fn default_format() -> String {
|
||||
"wav".to_string()
|
||||
}
|
||||
fn default_sample_rate() -> u32 {
|
||||
44100
|
||||
}
|
||||
fn default_bit_depth() -> u16 {
|
||||
16
|
||||
}
|
||||
fn default_stereo() -> bool {
|
||||
false
|
||||
}
|
||||
fn default_time_signature() -> TimeSignature {
|
||||
TimeSignature::default()
|
||||
}
|
||||
|
||||
/// Parse note name to MIDI number
|
||||
fn parse_note_name(name: &str) -> Result<u8, String> {
|
||||
let name = name.trim().to_uppercase();
|
||||
|
||||
// Extract note, accidental, and octave
|
||||
let (note_part, octave_str) = name.split_at(
|
||||
name.find(|c: char| c.is_ascii_digit())
|
||||
.ok_or_else(|| format!("Invalid note name: {}", name))?,
|
||||
);
|
||||
|
||||
let octave: i32 = octave_str
|
||||
.parse()
|
||||
.map_err(|_| format!("Invalid octave: {}", octave_str))?;
|
||||
|
||||
// Base MIDI numbers for C in each octave
|
||||
let base_midi = (octave + 1) * 12;
|
||||
|
||||
// Parse note and accidental
|
||||
let (note_char, accidental) = if note_part.ends_with('#') || note_part.ends_with('♯') {
|
||||
(¬e_part[0..1], 1)
|
||||
} else if note_part.ends_with('b') || note_part.ends_with('♭') || note_part.ends_with('B') {
|
||||
(¬e_part[0..1], -1)
|
||||
} else {
|
||||
(note_part, 0)
|
||||
};
|
||||
|
||||
// Note offsets from C
|
||||
let note_offset = match note_char {
|
||||
"C" => 0,
|
||||
"D" => 2,
|
||||
"E" => 4,
|
||||
"F" => 5,
|
||||
"G" => 7,
|
||||
"A" => 9,
|
||||
"B" => 11,
|
||||
_ => return Err(format!("Invalid note: {}", note_char)),
|
||||
};
|
||||
|
||||
let midi = base_midi + note_offset as i32 + accidental as i32;
|
||||
if midi < 0 || midi > 127 {
|
||||
return Err(format!("MIDI note {} out of range (0-127)", midi));
|
||||
}
|
||||
|
||||
Ok(midi as u8)
|
||||
}
|
||||
|
||||
impl CompositionConfig {
|
||||
/// Load configuration from a JSON file
|
||||
pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self, String> {
|
||||
let content =
|
||||
fs::read_to_string(path).map_err(|e| format!("Failed to read config file: {}", e))?;
|
||||
|
||||
Self::from_json(&content)
|
||||
}
|
||||
|
||||
/// Parse configuration from JSON string
|
||||
pub fn from_json(json: &str) -> Result<Self, String> {
|
||||
serde_json::from_str(json).map_err(|e| format!("Failed to parse JSON: {}", e))
|
||||
}
|
||||
|
||||
/// Save configuration to a JSON file
|
||||
pub fn to_file<P: AsRef<Path>>(&self, path: P) -> Result<(), String> {
|
||||
let json = self.to_json_pretty()?;
|
||||
fs::write(path, json).map_err(|e| format!("Failed to write config file: {}", e))
|
||||
}
|
||||
|
||||
/// Serialize configuration to pretty JSON
|
||||
pub fn to_json_pretty(&self) -> Result<String, String> {
|
||||
serde_json::to_string_pretty(self)
|
||||
.map_err(|e| format!("Failed to serialize to JSON: {}", e))
|
||||
}
|
||||
|
||||
/// Convert to composition parameters (deprecated - use track-based generation instead)
|
||||
pub fn to_composition_params(&self) -> Result<CompositionParams, String> {
|
||||
let scale_type = match self.composition.scale.to_lowercase().as_str() {
|
||||
"major" => ScaleType::Major,
|
||||
"minor" => ScaleType::Minor,
|
||||
"dorian" => ScaleType::Dorian,
|
||||
"phrygian" => ScaleType::Phrygian,
|
||||
"lydian" => ScaleType::Lydian,
|
||||
"mixolydian" => ScaleType::Mixolydian,
|
||||
"aeolian" => ScaleType::Aeolian,
|
||||
"locrian" => ScaleType::Locrian,
|
||||
"pentatonic" => ScaleType::Pentatonic,
|
||||
"blues" => ScaleType::Blues,
|
||||
"chromatic" => ScaleType::Chromatic,
|
||||
_ => return Err(format!("Unknown scale: {}", self.composition.scale)),
|
||||
};
|
||||
|
||||
let key = self.composition.key.to_midi()?;
|
||||
|
||||
Ok(CompositionParams {
|
||||
style: CompositionStyle::Electronic, // Default fallback
|
||||
key,
|
||||
scale_type,
|
||||
tempo: self.composition.tempo,
|
||||
time_signature: (
|
||||
self.composition.time_signature.numerator,
|
||||
self.composition.time_signature.denominator,
|
||||
),
|
||||
measures: self.composition.measures,
|
||||
track_count: self.tracks.len().max(1),
|
||||
complexity: self.composition.complexity,
|
||||
harmony_density: self.composition.harmony_density,
|
||||
rhythmic_density: self.composition.rhythmic_density,
|
||||
})
|
||||
}
|
||||
|
||||
/// Create an example configuration
|
||||
pub fn example() -> Self {
|
||||
Self {
|
||||
metadata: Metadata {
|
||||
title: "Example Track Composition".to_string(),
|
||||
artist: "Track Composer".to_string(),
|
||||
description: "An example track-based composition".to_string(),
|
||||
tags: vec!["lofi".to_string(), "chill".to_string()],
|
||||
},
|
||||
composition: CompositionSettings {
|
||||
key: KeySpec::NoteName("C4".to_string()),
|
||||
scale: "minor".to_string(),
|
||||
tempo: 85.0,
|
||||
time_signature: TimeSignature {
|
||||
numerator: 4,
|
||||
denominator: 4,
|
||||
},
|
||||
measures: 16,
|
||||
complexity: 0.6,
|
||||
harmony_density: 0.7,
|
||||
rhythmic_density: 0.7,
|
||||
seed: Some(42),
|
||||
},
|
||||
tracks: vec![TrackConfig {
|
||||
name: "bass".to_string(),
|
||||
instrument: "sine".to_string(),
|
||||
volume: 0.9,
|
||||
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,
|
||||
},
|
||||
],
|
||||
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
|
||||
}),
|
||||
}],
|
||||
}],
|
||||
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,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_parse_note_names() {
|
||||
assert_eq!(parse_note_name("C4").unwrap(), 60);
|
||||
assert_eq!(parse_note_name("A4").unwrap(), 69);
|
||||
assert_eq!(parse_note_name("C#4").unwrap(), 61);
|
||||
assert_eq!(parse_note_name("Bb3").unwrap(), 58);
|
||||
assert_eq!(parse_note_name("G5").unwrap(), 79);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_config_serialization() {
|
||||
let config = CompositionConfig::example();
|
||||
let json = config.to_json_pretty().unwrap();
|
||||
let parsed = CompositionConfig::from_json(&json).unwrap();
|
||||
|
||||
assert_eq!(parsed.composition.tempo, config.composition.tempo);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_to_composition_params() {
|
||||
let config = CompositionConfig::example();
|
||||
let params = config.to_composition_params().unwrap();
|
||||
|
||||
assert_eq!(params.key, 60); // C4
|
||||
assert_eq!(params.tempo, 85.0);
|
||||
assert_eq!(params.measures, 16);
|
||||
}
|
||||
}
|
307
src/core.rs
Normal file
307
src/core.rs
Normal file
@ -0,0 +1,307 @@
|
||||
//! Core types for musical composition
|
||||
//!
|
||||
//! This module contains the fundamental data structures used throughout
|
||||
//! the musicgen library for representing notes, tracks, and compositions.
|
||||
|
||||
use crate::scales::ScaleType;
|
||||
|
||||
/// Musical note representation
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct Note {
|
||||
pub midi_note: u8,
|
||||
pub start_time: f32,
|
||||
pub duration: f32,
|
||||
pub velocity: f32,
|
||||
}
|
||||
|
||||
impl Note {
|
||||
/// Create a new note
|
||||
pub fn new(midi_note: u8, start_time: f32, duration: f32, velocity: f32) -> Self {
|
||||
Self {
|
||||
midi_note,
|
||||
start_time,
|
||||
duration,
|
||||
velocity,
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert MIDI note to frequency in Hz
|
||||
pub fn frequency(&self) -> f32 {
|
||||
440.0 * 2.0_f32.powf((self.midi_note as f32 - 69.0) / 12.0)
|
||||
}
|
||||
}
|
||||
|
||||
/// Types of instruments for track categorization
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum InstrumentType {
|
||||
Lead,
|
||||
Bass,
|
||||
Pad,
|
||||
Arp,
|
||||
Percussion,
|
||||
Drone,
|
||||
}
|
||||
|
||||
/// A track represents a single instrument part
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Track {
|
||||
pub name: String,
|
||||
pub octave: u8,
|
||||
pub notes: Vec<Note>,
|
||||
pub volume: f32,
|
||||
pub instrument_type: InstrumentType,
|
||||
}
|
||||
|
||||
/// Composition styles (kept for compatibility)
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum CompositionStyle {
|
||||
Classical,
|
||||
Jazz,
|
||||
Electronic,
|
||||
Ambient,
|
||||
Minimalist,
|
||||
Generative,
|
||||
}
|
||||
|
||||
/// Parameters for composition generation
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CompositionParams {
|
||||
pub style: CompositionStyle,
|
||||
pub key: u8,
|
||||
pub scale_type: ScaleType,
|
||||
pub tempo: f32,
|
||||
pub time_signature: (u8, u8),
|
||||
pub measures: usize,
|
||||
pub track_count: usize,
|
||||
pub complexity: f32,
|
||||
pub harmony_density: f32,
|
||||
pub rhythmic_density: f32,
|
||||
}
|
||||
|
||||
impl Default for CompositionParams {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
style: CompositionStyle::Electronic,
|
||||
key: 60, // C4
|
||||
scale_type: ScaleType::Minor,
|
||||
tempo: 120.0,
|
||||
time_signature: (4, 4),
|
||||
measures: 8,
|
||||
track_count: 4,
|
||||
complexity: 0.6,
|
||||
harmony_density: 0.7,
|
||||
rhythmic_density: 0.7,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A complete musical composition
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Composition {
|
||||
pub params: CompositionParams,
|
||||
pub tracks: Vec<Track>,
|
||||
pub chord_progression: Vec<u8>,
|
||||
pub total_duration: f32,
|
||||
}
|
||||
|
||||
impl Composition {
|
||||
/// Create a new composition with the given parameters
|
||||
pub fn new(params: CompositionParams) -> Self {
|
||||
let total_duration =
|
||||
params.measures as f32 * params.time_signature.0 as f32 * (60.0 / params.tempo) * 4.0;
|
||||
|
||||
Self {
|
||||
params,
|
||||
tracks: Vec::new(),
|
||||
chord_progression: Vec::new(),
|
||||
total_duration,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get all notes from all tracks sorted by start time
|
||||
pub fn get_all_notes(&self) -> Vec<Note> {
|
||||
let mut all_notes = Vec::new();
|
||||
|
||||
for track in &self.tracks {
|
||||
all_notes.extend(track.notes.clone());
|
||||
}
|
||||
|
||||
all_notes.sort_by(|a, b| a.start_time.partial_cmp(&b.start_time).unwrap());
|
||||
all_notes
|
||||
}
|
||||
|
||||
/// Generate the composition (placeholder for compatibility)
|
||||
///
|
||||
/// This is a no-op method for compatibility with the old composition system.
|
||||
/// The composition system builds compositions directly from configuration.
|
||||
pub fn generate(&mut self) -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get composition statistics
|
||||
pub fn get_stats(&self) -> CompositionStats {
|
||||
let total_notes = self.tracks.iter().map(|t| t.notes.len()).sum();
|
||||
|
||||
CompositionStats {
|
||||
total_notes,
|
||||
total_duration: self.total_duration,
|
||||
track_count: self.tracks.len(),
|
||||
chord_count: self.chord_progression.len(),
|
||||
measures: self.params.measures,
|
||||
tempo: self.params.tempo,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Statistics about a composition
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CompositionStats {
|
||||
pub total_notes: usize,
|
||||
pub total_duration: f32,
|
||||
pub track_count: usize,
|
||||
pub chord_count: usize,
|
||||
pub measures: usize,
|
||||
pub tempo: f32,
|
||||
}
|
||||
|
||||
/// Builder for creating compositions with a fluent API
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CompositionBuilder {
|
||||
params: CompositionParams,
|
||||
}
|
||||
|
||||
impl CompositionBuilder {
|
||||
/// Create a new composition builder
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
params: CompositionParams::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the composition style
|
||||
pub fn style(mut self, style: CompositionStyle) -> Self {
|
||||
self.params.style = style;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the key
|
||||
pub fn key(mut self, key: u8) -> Self {
|
||||
self.params.key = key;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the scale type
|
||||
pub fn scale(mut self, scale_type: ScaleType) -> Self {
|
||||
self.params.scale_type = scale_type;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the tempo
|
||||
pub fn tempo(mut self, tempo: f32) -> Self {
|
||||
self.params.tempo = tempo;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the number of measures
|
||||
pub fn measures(mut self, measures: usize) -> Self {
|
||||
self.params.measures = measures;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the complexity level (0.0 to 1.0)
|
||||
pub fn complexity(mut self, complexity: f32) -> Self {
|
||||
self.params.complexity = complexity.clamp(0.0, 1.0);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the harmony density (0.0 to 1.0)
|
||||
pub fn harmony_density(mut self, density: f32) -> Self {
|
||||
self.params.harmony_density = density.clamp(0.0, 1.0);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the rhythmic density (0.0 to 1.0)
|
||||
pub fn rhythmic_density(mut self, density: f32) -> Self {
|
||||
self.params.rhythmic_density = density.clamp(0.0, 1.0);
|
||||
self
|
||||
}
|
||||
|
||||
/// Build the composition
|
||||
pub fn build(self) -> Composition {
|
||||
Composition::new(self.params)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for CompositionBuilder {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_note_creation() {
|
||||
let note = Note::new(60, 0.0, 1.0, 0.8);
|
||||
assert_eq!(note.midi_note, 60);
|
||||
assert_eq!(note.start_time, 0.0);
|
||||
assert_eq!(note.duration, 1.0);
|
||||
assert_eq!(note.velocity, 0.8);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_note_frequency() {
|
||||
let note = Note::new(69, 0.0, 1.0, 0.8); // A4
|
||||
assert!((note.frequency() - 440.0).abs() < 0.001);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_composition_creation() {
|
||||
let composition = Composition::new(CompositionParams::default());
|
||||
assert_eq!(composition.tracks.len(), 0);
|
||||
assert!(composition.total_duration > 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_composition_builder() {
|
||||
let composition = CompositionBuilder::new()
|
||||
.style(CompositionStyle::Jazz)
|
||||
.key(67) // G
|
||||
.tempo(140.0)
|
||||
.measures(16)
|
||||
.complexity(0.8)
|
||||
.build();
|
||||
|
||||
assert_eq!(composition.params.style, CompositionStyle::Jazz);
|
||||
assert_eq!(composition.params.key, 67);
|
||||
assert_eq!(composition.params.tempo, 140.0);
|
||||
assert_eq!(composition.params.measures, 16);
|
||||
assert_eq!(composition.params.complexity, 0.8);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_composition_stats() {
|
||||
let mut composition = Composition::new(CompositionParams::default());
|
||||
|
||||
// Add a test track with some notes
|
||||
let track = Track {
|
||||
name: "Test".to_string(),
|
||||
octave: 4,
|
||||
notes: vec![
|
||||
Note::new(60, 0.0, 1.0, 0.8),
|
||||
Note::new(64, 1.0, 1.0, 0.8),
|
||||
Note::new(67, 2.0, 1.0, 0.8),
|
||||
],
|
||||
volume: 0.8,
|
||||
instrument_type: InstrumentType::Lead,
|
||||
};
|
||||
|
||||
composition.tracks.push(track);
|
||||
|
||||
let stats = composition.get_stats();
|
||||
assert_eq!(stats.total_notes, 3);
|
||||
assert_eq!(stats.track_count, 1);
|
||||
}
|
||||
}
|
822
src/effects.rs
Normal file
822
src/effects.rs
Normal file
@ -0,0 +1,822 @@
|
||||
//! Audio effects processing module
|
||||
//!
|
||||
//! This module provides various audio effects that can be applied to synthesized audio,
|
||||
//! including filters, delays, reverbs, distortion, and modulation effects.
|
||||
|
||||
use crate::{SAMPLE_RATE, lerp, low_pass_filter};
|
||||
use std::collections::VecDeque;
|
||||
use std::f32::consts::PI;
|
||||
|
||||
/// Trait for audio effects that process samples
|
||||
pub trait AudioEffect {
|
||||
/// Process a single audio sample
|
||||
fn process_sample(&mut self, input: f32) -> f32;
|
||||
|
||||
/// Process a buffer of audio samples
|
||||
fn process_buffer(&mut self, buffer: &mut [f32]) {
|
||||
for sample in buffer.iter_mut() {
|
||||
*sample = self.process_sample(*sample);
|
||||
}
|
||||
}
|
||||
|
||||
/// Reset the effect's internal state
|
||||
fn reset(&mut self);
|
||||
|
||||
/// Set a parameter of the effect
|
||||
fn set_parameter(&mut self, param: &str, value: f32);
|
||||
}
|
||||
|
||||
/// Low-pass filter effect
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct LowPassFilter {
|
||||
pub cutoff_frequency: f32,
|
||||
pub resonance: f32,
|
||||
|
||||
// Internal state
|
||||
x1: f32,
|
||||
x2: f32,
|
||||
y1: f32,
|
||||
y2: f32,
|
||||
|
||||
// Filter coefficients
|
||||
a0: f32,
|
||||
a1: f32,
|
||||
a2: f32,
|
||||
b1: f32,
|
||||
b2: f32,
|
||||
}
|
||||
|
||||
impl LowPassFilter {
|
||||
/// Create a new low-pass filter
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `cutoff_frequency` - Cutoff frequency in Hz
|
||||
/// * `resonance` - Resonance factor (0.1 to 10.0)
|
||||
pub fn new(cutoff_frequency: f32, resonance: f32) -> Self {
|
||||
let mut filter = Self {
|
||||
cutoff_frequency,
|
||||
resonance: resonance.clamp(0.1, 10.0),
|
||||
x1: 0.0,
|
||||
x2: 0.0,
|
||||
y1: 0.0,
|
||||
y2: 0.0,
|
||||
a0: 1.0,
|
||||
a1: 0.0,
|
||||
a2: 0.0,
|
||||
b1: 0.0,
|
||||
b2: 0.0,
|
||||
};
|
||||
|
||||
filter.update_coefficients();
|
||||
filter
|
||||
}
|
||||
|
||||
fn update_coefficients(&mut self) {
|
||||
let omega = 2.0 * PI * self.cutoff_frequency / SAMPLE_RATE;
|
||||
let sin_omega = omega.sin();
|
||||
let cos_omega = omega.cos();
|
||||
let alpha = sin_omega / (2.0 * self.resonance);
|
||||
|
||||
let b0 = (1.0 - cos_omega) / 2.0;
|
||||
let b1 = 1.0 - cos_omega;
|
||||
let b2 = (1.0 - cos_omega) / 2.0;
|
||||
let a0 = 1.0 + alpha;
|
||||
let a1 = -2.0 * cos_omega;
|
||||
let a2 = 1.0 - alpha;
|
||||
|
||||
// Normalize coefficients
|
||||
self.a0 = b0 / a0;
|
||||
self.a1 = b1 / a0;
|
||||
self.a2 = b2 / a0;
|
||||
self.b1 = a1 / a0;
|
||||
self.b2 = a2 / a0;
|
||||
}
|
||||
|
||||
pub fn set_cutoff(&mut self, frequency: f32) {
|
||||
self.cutoff_frequency = frequency.clamp(20.0, SAMPLE_RATE / 2.0);
|
||||
self.update_coefficients();
|
||||
}
|
||||
|
||||
pub fn set_resonance(&mut self, resonance: f32) {
|
||||
self.resonance = resonance.clamp(0.1, 10.0);
|
||||
self.update_coefficients();
|
||||
}
|
||||
}
|
||||
|
||||
impl AudioEffect for LowPassFilter {
|
||||
fn process_sample(&mut self, input: f32) -> f32 {
|
||||
let output = self.a0 * input + self.a1 * self.x1 + self.a2 * self.x2
|
||||
- self.b1 * self.y1
|
||||
- self.b2 * self.y2;
|
||||
|
||||
// Update delay line
|
||||
self.x2 = self.x1;
|
||||
self.x1 = input;
|
||||
self.y2 = self.y1;
|
||||
self.y1 = output;
|
||||
|
||||
output
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.x1 = 0.0;
|
||||
self.x2 = 0.0;
|
||||
self.y1 = 0.0;
|
||||
self.y2 = 0.0;
|
||||
}
|
||||
|
||||
fn set_parameter(&mut self, param: &str, value: f32) {
|
||||
match param {
|
||||
"cutoff" => self.set_cutoff(value),
|
||||
"resonance" => self.set_resonance(value),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// High-pass filter effect
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct HighPassFilter {
|
||||
pub cutoff_frequency: f32,
|
||||
pub resonance: f32,
|
||||
|
||||
// Internal state (same as low-pass)
|
||||
x1: f32,
|
||||
x2: f32,
|
||||
y1: f32,
|
||||
y2: f32,
|
||||
|
||||
// Filter coefficients
|
||||
a0: f32,
|
||||
a1: f32,
|
||||
a2: f32,
|
||||
b1: f32,
|
||||
b2: f32,
|
||||
}
|
||||
|
||||
impl HighPassFilter {
|
||||
pub fn new(cutoff_frequency: f32, resonance: f32) -> Self {
|
||||
let mut filter = Self {
|
||||
cutoff_frequency,
|
||||
resonance: resonance.clamp(0.1, 10.0),
|
||||
x1: 0.0,
|
||||
x2: 0.0,
|
||||
y1: 0.0,
|
||||
y2: 0.0,
|
||||
a0: 1.0,
|
||||
a1: 0.0,
|
||||
a2: 0.0,
|
||||
b1: 0.0,
|
||||
b2: 0.0,
|
||||
};
|
||||
|
||||
filter.update_coefficients();
|
||||
filter
|
||||
}
|
||||
|
||||
fn update_coefficients(&mut self) {
|
||||
let omega = 2.0 * PI * self.cutoff_frequency / SAMPLE_RATE;
|
||||
let sin_omega = omega.sin();
|
||||
let cos_omega = omega.cos();
|
||||
let alpha = sin_omega / (2.0 * self.resonance);
|
||||
|
||||
let b0 = (1.0 + cos_omega) / 2.0;
|
||||
let b1 = -(1.0 + cos_omega);
|
||||
let b2 = (1.0 + cos_omega) / 2.0;
|
||||
let a0 = 1.0 + alpha;
|
||||
let a1 = -2.0 * cos_omega;
|
||||
let a2 = 1.0 - alpha;
|
||||
|
||||
// Normalize coefficients
|
||||
self.a0 = b0 / a0;
|
||||
self.a1 = b1 / a0;
|
||||
self.a2 = b2 / a0;
|
||||
self.b1 = a1 / a0;
|
||||
self.b2 = a2 / a0;
|
||||
}
|
||||
|
||||
pub fn set_cutoff(&mut self, frequency: f32) {
|
||||
self.cutoff_frequency = frequency.clamp(20.0, SAMPLE_RATE / 2.0);
|
||||
self.update_coefficients();
|
||||
}
|
||||
|
||||
pub fn set_resonance(&mut self, resonance: f32) {
|
||||
self.resonance = resonance.clamp(0.1, 10.0);
|
||||
self.update_coefficients();
|
||||
}
|
||||
}
|
||||
|
||||
impl AudioEffect for HighPassFilter {
|
||||
fn process_sample(&mut self, input: f32) -> f32 {
|
||||
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;
|
||||
self.y2 = self.y1;
|
||||
self.y1 = output;
|
||||
|
||||
output
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.x1 = 0.0;
|
||||
self.x2 = 0.0;
|
||||
self.y1 = 0.0;
|
||||
self.y2 = 0.0;
|
||||
}
|
||||
|
||||
fn set_parameter(&mut self, param: &str, value: f32) {
|
||||
match param {
|
||||
"cutoff" => self.set_cutoff(value),
|
||||
"resonance" => self.set_resonance(value),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Delay effect with feedback
|
||||
#[derive(Debug)]
|
||||
pub struct Delay {
|
||||
pub delay_time: f32, // Delay time in seconds
|
||||
pub feedback: f32, // Feedback amount (0.0 to 0.95)
|
||||
pub mix: f32, // Wet/dry mix (0.0 = dry, 1.0 = wet)
|
||||
|
||||
delay_buffer: VecDeque<f32>,
|
||||
delay_samples: usize,
|
||||
}
|
||||
|
||||
impl Delay {
|
||||
/// Create a new delay effect
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `delay_time` - Delay time in seconds
|
||||
/// * `feedback` - Feedback amount (0.0 to 0.95)
|
||||
/// * `mix` - Wet/dry mix (0.0 to 1.0)
|
||||
pub fn new(delay_time: f32, feedback: f32, mix: f32) -> Self {
|
||||
let delay_samples = (delay_time * SAMPLE_RATE) as usize;
|
||||
let mut delay_buffer = VecDeque::with_capacity(delay_samples);
|
||||
|
||||
// Initialize with zeros
|
||||
for _ in 0..delay_samples {
|
||||
delay_buffer.push_back(0.0);
|
||||
}
|
||||
|
||||
Self {
|
||||
delay_time,
|
||||
feedback: feedback.clamp(0.0, 0.95),
|
||||
mix: mix.clamp(0.0, 1.0),
|
||||
delay_buffer,
|
||||
delay_samples,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_delay_time(&mut self, time: f32) {
|
||||
self.delay_time = time.max(0.001); // Minimum 1ms delay
|
||||
let new_samples = (time * SAMPLE_RATE) as usize;
|
||||
|
||||
if new_samples != self.delay_samples {
|
||||
self.delay_samples = new_samples;
|
||||
self.delay_buffer.clear();
|
||||
|
||||
for _ in 0..new_samples {
|
||||
self.delay_buffer.push_back(0.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_feedback(&mut self, feedback: f32) {
|
||||
self.feedback = feedback.clamp(0.0, 0.95);
|
||||
}
|
||||
|
||||
pub fn set_mix(&mut self, mix: f32) {
|
||||
self.mix = mix.clamp(0.0, 1.0);
|
||||
}
|
||||
}
|
||||
|
||||
impl AudioEffect for Delay {
|
||||
fn process_sample(&mut self, input: f32) -> f32 {
|
||||
let delayed_sample = self.delay_buffer.front().copied().unwrap_or(0.0);
|
||||
let feedback_sample = input + delayed_sample * self.feedback;
|
||||
|
||||
// Add new sample to delay line
|
||||
self.delay_buffer.pop_front();
|
||||
self.delay_buffer.push_back(feedback_sample);
|
||||
|
||||
// Mix dry and wet signals
|
||||
lerp(input, delayed_sample, self.mix)
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
for sample in self.delay_buffer.iter_mut() {
|
||||
*sample = 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
fn set_parameter(&mut self, param: &str, value: f32) {
|
||||
match param {
|
||||
"delay_time" => self.set_delay_time(value),
|
||||
"feedback" => self.set_feedback(value),
|
||||
"mix" => self.set_mix(value),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Simple reverb effect using multiple delay lines
|
||||
#[derive(Debug)]
|
||||
pub struct Reverb {
|
||||
pub room_size: f32,
|
||||
pub damping: f32,
|
||||
pub mix: f32,
|
||||
|
||||
delay_lines: Vec<VecDeque<f32>>,
|
||||
feedback_gains: Vec<f32>,
|
||||
damping_filters: Vec<f32>, // Simple one-pole filters for damping
|
||||
}
|
||||
|
||||
impl Reverb {
|
||||
/// Create a new reverb effect
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `room_size` - Room size factor (0.0 to 1.0)
|
||||
/// * `damping` - High frequency damping (0.0 to 1.0)
|
||||
/// * `mix` - Wet/dry mix (0.0 to 1.0)
|
||||
pub fn new(room_size: f32, damping: f32, mix: f32) -> Self {
|
||||
// Use Schroeder's reverb design with 4 comb filters and 2 allpass filters
|
||||
let base_delays = vec![1116, 1188, 1277, 1356, 225, 556]; // Prime numbers for better diffusion
|
||||
let base_gains = vec![0.7, 0.7, 0.7, 0.7, 0.5, 0.5];
|
||||
|
||||
let mut delay_lines = Vec::new();
|
||||
let mut feedback_gains = Vec::new();
|
||||
|
||||
let room_factor = 1.0 + room_size * 0.5; // Scale delays based on room size
|
||||
|
||||
for (i, &base_delay) in base_delays.iter().enumerate() {
|
||||
let delay_samples = (base_delay as f32 * room_factor) as usize;
|
||||
let mut delay_line = VecDeque::with_capacity(delay_samples);
|
||||
|
||||
for _ in 0..delay_samples {
|
||||
delay_line.push_back(0.0);
|
||||
}
|
||||
|
||||
delay_lines.push(delay_line);
|
||||
feedback_gains.push(base_gains[i] * room_size);
|
||||
}
|
||||
|
||||
Self {
|
||||
room_size: room_size.clamp(0.0, 1.0),
|
||||
damping: damping.clamp(0.0, 1.0),
|
||||
mix: mix.clamp(0.0, 1.0),
|
||||
delay_lines,
|
||||
feedback_gains,
|
||||
damping_filters: vec![0.0; base_delays.len()],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_room_size(&mut self, size: f32) {
|
||||
self.room_size = size.clamp(0.0, 1.0);
|
||||
// Update feedback gains
|
||||
for (i, gain) in self.feedback_gains.iter_mut().enumerate() {
|
||||
let base_gains = vec![0.7, 0.7, 0.7, 0.7, 0.5, 0.5];
|
||||
*gain = base_gains[i] * self.room_size;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_damping(&mut self, damping: f32) {
|
||||
self.damping = damping.clamp(0.0, 1.0);
|
||||
}
|
||||
|
||||
pub fn set_mix(&mut self, mix: f32) {
|
||||
self.mix = mix.clamp(0.0, 1.0);
|
||||
}
|
||||
}
|
||||
|
||||
impl AudioEffect for Reverb {
|
||||
fn process_sample(&mut self, input: f32) -> f32 {
|
||||
let mut output = 0.0;
|
||||
|
||||
// Process comb filters (first 4 delay lines)
|
||||
for i in 0..4 {
|
||||
if let Some(delayed) = self.delay_lines[i].front().copied() {
|
||||
// Apply damping filter
|
||||
self.damping_filters[i] =
|
||||
low_pass_filter(delayed, self.damping_filters[i], 1.0 - self.damping);
|
||||
|
||||
let feedback_sample = input + self.damping_filters[i] * self.feedback_gains[i];
|
||||
|
||||
self.delay_lines[i].pop_front();
|
||||
self.delay_lines[i].push_back(feedback_sample);
|
||||
|
||||
output += delayed;
|
||||
}
|
||||
}
|
||||
|
||||
// Process allpass filters (last 2 delay lines)
|
||||
let mut allpass_input = output * 0.25; // Scale down comb filter output
|
||||
|
||||
for i in 4..6 {
|
||||
if let Some(delayed) = self.delay_lines[i].front().copied() {
|
||||
let feedback_sample = allpass_input + delayed * self.feedback_gains[i];
|
||||
|
||||
self.delay_lines[i].pop_front();
|
||||
self.delay_lines[i].push_back(feedback_sample);
|
||||
|
||||
allpass_input = delayed - allpass_input * self.feedback_gains[i];
|
||||
}
|
||||
}
|
||||
|
||||
// Mix dry and wet signals
|
||||
lerp(input, allpass_input, self.mix)
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
for delay_line in &mut self.delay_lines {
|
||||
for sample in delay_line.iter_mut() {
|
||||
*sample = 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
for filter in &mut self.damping_filters {
|
||||
*filter = 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
fn set_parameter(&mut self, param: &str, value: f32) {
|
||||
match param {
|
||||
"room_size" => self.set_room_size(value),
|
||||
"damping" => self.set_damping(value),
|
||||
"mix" => self.set_mix(value),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Distortion effect
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Distortion {
|
||||
pub drive: f32, // Distortion amount (1.0 to 10.0)
|
||||
pub tone: f32, // Tone control (0.0 to 1.0)
|
||||
pub output_gain: f32, // Output gain compensation
|
||||
|
||||
pre_filter: LowPassFilter,
|
||||
post_filter: LowPassFilter,
|
||||
}
|
||||
|
||||
impl Distortion {
|
||||
/// Create a new distortion effect
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `drive` - Distortion drive amount (1.0 to 10.0)
|
||||
/// * `tone` - Tone control (0.0 to 1.0)
|
||||
pub fn new(drive: f32, tone: f32) -> Self {
|
||||
Self {
|
||||
drive: drive.clamp(1.0, 10.0),
|
||||
tone: tone.clamp(0.0, 1.0),
|
||||
output_gain: 1.0 / drive.clamp(1.0, 10.0), // Automatic gain compensation
|
||||
pre_filter: LowPassFilter::new(8000.0, 0.7),
|
||||
post_filter: LowPassFilter::new(4000.0 + tone * 4000.0, 0.7),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_drive(&mut self, drive: f32) {
|
||||
self.drive = drive.clamp(1.0, 10.0);
|
||||
self.output_gain = 1.0 / self.drive;
|
||||
}
|
||||
|
||||
pub fn set_tone(&mut self, tone: f32) {
|
||||
self.tone = tone.clamp(0.0, 1.0);
|
||||
self.post_filter.set_cutoff(4000.0 + self.tone * 4000.0);
|
||||
}
|
||||
|
||||
fn waveshaper(&self, input: f32) -> f32 {
|
||||
let driven = input * self.drive;
|
||||
|
||||
// Soft clipping using tanh
|
||||
if driven.abs() <= 1.0 {
|
||||
driven * (1.0 - driven.abs() / 3.0)
|
||||
} else {
|
||||
driven.signum() * (2.0 / 3.0 + (driven.abs() - 1.0) / (driven.abs() + 1.0) / 3.0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AudioEffect for Distortion {
|
||||
fn process_sample(&mut self, input: f32) -> f32 {
|
||||
// Pre-filtering to reduce aliasing
|
||||
let filtered_input = self.pre_filter.process_sample(input);
|
||||
|
||||
// Apply waveshaping distortion
|
||||
let distorted = self.waveshaper(filtered_input);
|
||||
|
||||
// Post-filtering for tone shaping
|
||||
let shaped = self.post_filter.process_sample(distorted);
|
||||
|
||||
// Apply output gain compensation
|
||||
shaped * self.output_gain
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.pre_filter.reset();
|
||||
self.post_filter.reset();
|
||||
}
|
||||
|
||||
fn set_parameter(&mut self, param: &str, value: f32) {
|
||||
match param {
|
||||
"drive" => self.set_drive(value),
|
||||
"tone" => self.set_tone(value),
|
||||
"output_gain" => self.output_gain = value.clamp(0.0, 2.0),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Chorus effect using multiple delay lines with LFO modulation
|
||||
#[derive(Debug)]
|
||||
pub struct Chorus {
|
||||
pub rate: f32, // LFO rate in Hz
|
||||
pub depth: f32, // Modulation depth (0.0 to 1.0)
|
||||
pub mix: f32, // Wet/dry mix
|
||||
pub layers: usize, // Number of chorus layers
|
||||
|
||||
delay_lines: Vec<VecDeque<f32>>,
|
||||
lfo_phases: Vec<f32>,
|
||||
base_delay_samples: usize,
|
||||
}
|
||||
|
||||
impl Chorus {
|
||||
/// Create a new chorus effect
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `rate` - LFO rate in Hz
|
||||
/// * `depth` - Modulation depth (0.0 to 1.0)
|
||||
/// * `mix` - Wet/dry mix (0.0 to 1.0)
|
||||
/// * `layers` - Number of chorus layers (1 to 4)
|
||||
pub fn new(rate: f32, depth: f32, mix: f32, layers: usize) -> Self {
|
||||
let layers = layers.clamp(1, 4);
|
||||
let base_delay_ms = 15.0; // Base delay in milliseconds
|
||||
let max_delay_ms = base_delay_ms + depth * 10.0; // Max modulation range
|
||||
|
||||
let base_delay_samples = (base_delay_ms * SAMPLE_RATE / 1000.0) as usize;
|
||||
let max_delay_samples = (max_delay_ms * SAMPLE_RATE / 1000.0) as usize;
|
||||
|
||||
let mut delay_lines = Vec::new();
|
||||
let mut lfo_phases = Vec::new();
|
||||
|
||||
for i in 0..layers {
|
||||
let mut delay_line = VecDeque::with_capacity(max_delay_samples);
|
||||
for _ in 0..max_delay_samples {
|
||||
delay_line.push_back(0.0);
|
||||
}
|
||||
delay_lines.push(delay_line);
|
||||
|
||||
// Distribute LFO phases evenly
|
||||
lfo_phases.push(i as f32 * 2.0 * PI / layers as f32);
|
||||
}
|
||||
|
||||
Self {
|
||||
rate: rate.clamp(0.1, 10.0),
|
||||
depth: depth.clamp(0.0, 1.0),
|
||||
mix: mix.clamp(0.0, 1.0),
|
||||
layers,
|
||||
delay_lines,
|
||||
lfo_phases,
|
||||
base_delay_samples,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_rate(&mut self, rate: f32) {
|
||||
self.rate = rate.clamp(0.1, 10.0);
|
||||
}
|
||||
|
||||
pub fn set_depth(&mut self, depth: f32) {
|
||||
self.depth = depth.clamp(0.0, 1.0);
|
||||
}
|
||||
|
||||
fn get_delayed_sample(&self, delay_line: &VecDeque<f32>, delay_samples: f32) -> f32 {
|
||||
let delay_int = delay_samples.floor() as usize;
|
||||
let delay_frac = delay_samples - delay_int as f32;
|
||||
|
||||
if delay_int >= delay_line.len() {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
let sample1 = delay_line.get(delay_int).copied().unwrap_or(0.0);
|
||||
let sample2 = delay_line.get(delay_int + 1).copied().unwrap_or(0.0);
|
||||
|
||||
// Linear interpolation
|
||||
lerp(sample1, sample2, delay_frac)
|
||||
}
|
||||
}
|
||||
|
||||
impl AudioEffect for Chorus {
|
||||
fn process_sample(&mut self, input: f32) -> f32 {
|
||||
let mut chorus_output = 0.0;
|
||||
|
||||
for i in 0..self.layers {
|
||||
// Update LFO phase
|
||||
self.lfo_phases[i] += 2.0 * PI * self.rate / SAMPLE_RATE;
|
||||
if self.lfo_phases[i] >= 2.0 * PI {
|
||||
self.lfo_phases[i] -= 2.0 * PI;
|
||||
}
|
||||
|
||||
// Calculate modulated delay
|
||||
let lfo_value = self.lfo_phases[i].sin();
|
||||
let delay_offset = self.depth * 10.0 * SAMPLE_RATE / 1000.0; // Convert to samples
|
||||
let total_delay = self.base_delay_samples as f32 + lfo_value * delay_offset;
|
||||
|
||||
// Get delayed sample with interpolation
|
||||
let delayed_sample = self.get_delayed_sample(&self.delay_lines[i], total_delay);
|
||||
chorus_output += delayed_sample;
|
||||
|
||||
// Add input to delay line
|
||||
self.delay_lines[i].pop_front();
|
||||
self.delay_lines[i].push_back(input);
|
||||
}
|
||||
|
||||
// Average the chorus layers
|
||||
chorus_output /= self.layers as f32;
|
||||
|
||||
// Mix with dry signal
|
||||
lerp(input, chorus_output, self.mix)
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
for delay_line in &mut self.delay_lines {
|
||||
for sample in delay_line.iter_mut() {
|
||||
*sample = 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
for phase in &mut self.lfo_phases {
|
||||
*phase = 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
fn set_parameter(&mut self, param: &str, value: f32) {
|
||||
match param {
|
||||
"rate" => self.set_rate(value),
|
||||
"depth" => self.set_depth(value),
|
||||
"mix" => self.mix = value.clamp(0.0, 1.0),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Effects chain that can combine multiple effects
|
||||
pub struct EffectsChain {
|
||||
effects: Vec<Box<dyn AudioEffect>>,
|
||||
}
|
||||
|
||||
impl EffectsChain {
|
||||
/// Create a new empty effects chain
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
effects: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Add an effect to the chain
|
||||
pub fn add_effect(&mut self, effect: Box<dyn AudioEffect>) {
|
||||
self.effects.push(effect);
|
||||
}
|
||||
|
||||
/// Remove an effect from the chain
|
||||
pub fn remove_effect(&mut self, index: usize) -> Option<Box<dyn AudioEffect>> {
|
||||
if index < self.effects.len() {
|
||||
Some(self.effects.remove(index))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Clear all effects from the chain
|
||||
pub fn clear(&mut self) {
|
||||
self.effects.clear();
|
||||
}
|
||||
|
||||
/// Get the number of effects in the chain
|
||||
pub fn len(&self) -> usize {
|
||||
self.effects.len()
|
||||
}
|
||||
|
||||
/// Check if the chain is empty
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.effects.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
impl AudioEffect for EffectsChain {
|
||||
fn process_sample(&mut self, input: f32) -> f32 {
|
||||
let mut signal = input;
|
||||
|
||||
for effect in &mut self.effects {
|
||||
signal = effect.process_sample(signal);
|
||||
}
|
||||
|
||||
signal
|
||||
}
|
||||
|
||||
fn process_buffer(&mut self, buffer: &mut [f32]) {
|
||||
for effect in &mut self.effects {
|
||||
effect.process_buffer(buffer);
|
||||
}
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
for effect in &mut self.effects {
|
||||
effect.reset();
|
||||
}
|
||||
}
|
||||
|
||||
fn set_parameter(&mut self, param: &str, value: f32) {
|
||||
// This would require a more sophisticated parameter routing system
|
||||
// For now, we'll just ignore it
|
||||
let _ = (param, value);
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for EffectsChain {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_low_pass_filter() {
|
||||
let mut filter = LowPassFilter::new(1000.0, 1.0);
|
||||
let output = filter.process_sample(1.0);
|
||||
assert!(output.is_finite());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_high_pass_filter() {
|
||||
let mut filter = HighPassFilter::new(1000.0, 1.0);
|
||||
let output = filter.process_sample(1.0);
|
||||
assert!(output.is_finite());
|
||||
}
|
||||
|
||||
#[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());
|
||||
|
||||
// After enough samples, we should get some delayed signal
|
||||
for _ in 0..(0.1 * SAMPLE_RATE) as usize {
|
||||
delay.process_sample(0.0);
|
||||
}
|
||||
let delayed_output = delay.process_sample(0.0);
|
||||
assert!(delayed_output.abs() > 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_reverb() {
|
||||
let mut reverb = Reverb::new(0.5, 0.3, 0.4);
|
||||
let output = reverb.process_sample(1.0);
|
||||
assert!(output.is_finite());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_distortion() {
|
||||
let mut distortion = Distortion::new(3.0, 0.5);
|
||||
let output = distortion.process_sample(0.5);
|
||||
assert!(output.is_finite());
|
||||
assert!(output.abs() <= 1.0); // Should not clip beyond reasonable bounds
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_chorus() {
|
||||
let mut chorus = Chorus::new(1.0, 0.5, 0.3, 2);
|
||||
let output = chorus.process_sample(1.0);
|
||||
assert!(output.is_finite());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_effects_chain() {
|
||||
let mut chain = EffectsChain::new();
|
||||
|
||||
chain.add_effect(Box::new(LowPassFilter::new(2000.0, 1.0)));
|
||||
chain.add_effect(Box::new(Delay::new(0.05, 0.3, 0.2)));
|
||||
|
||||
assert_eq!(chain.len(), 2);
|
||||
|
||||
let output = chain.process_sample(1.0);
|
||||
assert!(output.is_finite());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_filter_parameter_setting() {
|
||||
let mut filter = LowPassFilter::new(1000.0, 1.0);
|
||||
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);
|
||||
}
|
||||
}
|
137
src/lib.rs
Normal file
137
src/lib.rs
Normal file
@ -0,0 +1,137 @@
|
||||
//! Generate electronic music without AI
|
||||
//!
|
||||
//! This library provides tools for generating electronic music
|
||||
//! with synthesis, sequencing, and composition algorithms.
|
||||
|
||||
pub mod audio;
|
||||
pub mod composition;
|
||||
pub mod config;
|
||||
pub mod core;
|
||||
pub mod effects;
|
||||
pub mod patterns;
|
||||
pub mod scales;
|
||||
pub mod sequencer;
|
||||
pub mod synthesis;
|
||||
|
||||
use std::f32::consts::PI;
|
||||
|
||||
/// Sample rate for audio generation (44.1 kHz)
|
||||
pub const SAMPLE_RATE: f32 = 44100.0;
|
||||
|
||||
/// Standard tuning reference frequency (A4 = 440 Hz)
|
||||
pub const A4_FREQUENCY: f32 = 440.0;
|
||||
|
||||
/// Convert MIDI note number to frequency in Hz
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `midi_note` - MIDI note number (0-127, where 69 = A4 = 440Hz)
|
||||
///
|
||||
/// # Returns
|
||||
/// Frequency in Hz
|
||||
pub fn midi_to_frequency(midi_note: u8) -> f32 {
|
||||
A4_FREQUENCY * 2.0_f32.powf((midi_note as f32 - 69.0) / 12.0)
|
||||
}
|
||||
|
||||
/// Convert frequency to MIDI note number
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `frequency` - Frequency in Hz
|
||||
///
|
||||
/// # Returns
|
||||
/// MIDI note number (rounded to nearest integer)
|
||||
pub fn frequency_to_midi(frequency: f32) -> u8 {
|
||||
(69.0 + 12.0 * (frequency / A4_FREQUENCY).log2()).round() as u8
|
||||
}
|
||||
|
||||
/// Convert beats per minute to samples per beat
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `bpm` - Beats per minute
|
||||
///
|
||||
/// # Returns
|
||||
/// Number of samples per beat at current sample rate
|
||||
pub fn bpm_to_samples_per_beat(bpm: f32) -> usize {
|
||||
(SAMPLE_RATE * 60.0 / bpm) as usize
|
||||
}
|
||||
|
||||
/// Normalize a value to the range [0.0, 1.0]
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `value` - Input value
|
||||
/// * `min` - Minimum possible value
|
||||
/// * `max` - Maximum possible value
|
||||
///
|
||||
/// # Returns
|
||||
/// Normalized value between 0.0 and 1.0
|
||||
pub fn normalize(value: f32, min: f32, max: f32) -> f32 {
|
||||
(value - min) / (max - min)
|
||||
}
|
||||
|
||||
/// Linear interpolation between two values
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `a` - Start value
|
||||
/// * `b` - End value
|
||||
/// * `t` - Interpolation factor (0.0 to 1.0)
|
||||
///
|
||||
/// # Returns
|
||||
/// Interpolated value
|
||||
pub fn lerp(a: f32, b: f32, t: f32) -> f32 {
|
||||
a + (b - a) * t
|
||||
}
|
||||
|
||||
/// Apply a simple low-pass filter to smooth audio transitions
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `input` - Input sample
|
||||
/// * `previous` - Previous filtered sample
|
||||
/// * `alpha` - Filter coefficient (0.0 to 1.0, higher = less filtering)
|
||||
///
|
||||
/// # Returns
|
||||
/// Filtered sample
|
||||
pub fn low_pass_filter(input: f32, previous: f32, alpha: f32) -> f32 {
|
||||
alpha * input + (1.0 - alpha) * previous
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_midi_to_frequency() {
|
||||
// A4 should be 440 Hz
|
||||
assert_eq!(midi_to_frequency(69), 440.0);
|
||||
|
||||
// C4 should be approximately 261.63 Hz
|
||||
let c4_freq = midi_to_frequency(60);
|
||||
assert!((c4_freq - 261.63).abs() < 0.1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_frequency_to_midi() {
|
||||
assert_eq!(frequency_to_midi(440.0), 69);
|
||||
assert_eq!(frequency_to_midi(261.63), 60);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bpm_conversion() {
|
||||
let samples_per_beat = bpm_to_samples_per_beat(120.0);
|
||||
// At 120 BPM, each beat should be 0.5 seconds
|
||||
// At 44100 Hz sample rate, that's 22050 samples
|
||||
assert_eq!(samples_per_beat, 22050);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_normalize() {
|
||||
assert_eq!(normalize(5.0, 0.0, 10.0), 0.5);
|
||||
assert_eq!(normalize(0.0, 0.0, 10.0), 0.0);
|
||||
assert_eq!(normalize(10.0, 0.0, 10.0), 1.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lerp() {
|
||||
assert_eq!(lerp(0.0, 10.0, 0.5), 5.0);
|
||||
assert_eq!(lerp(0.0, 10.0, 0.0), 0.0);
|
||||
assert_eq!(lerp(0.0, 10.0, 1.0), 10.0);
|
||||
}
|
||||
}
|
206
src/main.rs
Normal file
206
src/main.rs
Normal file
@ -0,0 +1,206 @@
|
||||
//! Generate electronic music without AI
|
||||
//!
|
||||
//! A command-line tool for generating electronic music without the use of AI.
|
||||
|
||||
use clap::{Parser, Subcommand};
|
||||
use musicgen::audio::AudioExporter;
|
||||
use musicgen::composition::Composition;
|
||||
use musicgen::config::CompositionConfig;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "musicgen")]
|
||||
#[command(about = "Generate electronic music without AI")]
|
||||
#[command(version = "0.1.0")]
|
||||
struct Cli {
|
||||
#[command(subcommand)]
|
||||
command: Commands,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum Commands {
|
||||
/// Generate composition from JSON configuration file
|
||||
Json {
|
||||
/// Path to JSON configuration file
|
||||
config: PathBuf,
|
||||
|
||||
/// Override output filename
|
||||
#[arg(short, long)]
|
||||
output: Option<String>,
|
||||
|
||||
/// Print example JSON configuration
|
||||
#[arg(long)]
|
||||
example: bool,
|
||||
},
|
||||
|
||||
/// Show information about available options
|
||||
Info {
|
||||
/// Show available scales
|
||||
#[arg(long)]
|
||||
scales: bool,
|
||||
|
||||
/// Show available instruments
|
||||
#[arg(long)]
|
||||
instruments: bool,
|
||||
|
||||
/// Show all information
|
||||
#[arg(short, long)]
|
||||
all: bool,
|
||||
},
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let cli = Cli::parse();
|
||||
|
||||
let result = match cli.command {
|
||||
Commands::Json {
|
||||
config,
|
||||
output,
|
||||
example,
|
||||
} => handle_json_command(config, output, example),
|
||||
|
||||
Commands::Info {
|
||||
scales,
|
||||
instruments,
|
||||
all,
|
||||
} => show_info(scales, instruments, all),
|
||||
};
|
||||
|
||||
if let Err(e) = result {
|
||||
eprintln!("Error: {}", e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
fn show_info(scales: bool, instruments: bool, all: bool) -> Result<(), Box<dyn std::error::Error>> {
|
||||
if all || scales {
|
||||
println!("Available Scales:");
|
||||
println!(" major - Major scale (Ionian mode)");
|
||||
println!(" minor - Natural minor scale (Aeolian mode)");
|
||||
println!(" dorian - Dorian mode");
|
||||
println!(" phrygian - Phrygian mode");
|
||||
println!(" lydian - Lydian mode");
|
||||
println!(" mixolydian - Mixolydian mode");
|
||||
println!(" aeolian - Aeolian mode (same as minor)");
|
||||
println!(" locrian - Locrian mode");
|
||||
println!(" pentatonic - Pentatonic scale");
|
||||
println!(" blues - Blues scale");
|
||||
println!(" chromatic - Chromatic scale (all 12 notes)");
|
||||
println!();
|
||||
}
|
||||
|
||||
if all || instruments {
|
||||
println!("Available Instruments:");
|
||||
println!(" sine - Pure sine wave (smooth, warm)");
|
||||
println!(" square - Square wave (classic electronic, punchy)");
|
||||
println!(" sawtooth - Sawtooth wave (bright, buzzy)");
|
||||
println!(" triangle - Triangle wave (mellow, soft)");
|
||||
println!(" noise - White noise (for percussion, textures)");
|
||||
println!();
|
||||
}
|
||||
|
||||
if all {
|
||||
println!("MIDI Note Numbers:");
|
||||
println!(" C4 = 60, C#4 = 61, D4 = 62, D#4 = 63, E4 = 64, F4 = 65");
|
||||
println!(" F#4 = 66, G4 = 67, G#4 = 68, A4 = 69, A#4 = 70, B4 = 71");
|
||||
println!(" (Add/subtract 12 for different octaves)");
|
||||
println!();
|
||||
|
||||
println!("Pattern Types:");
|
||||
println!(" custom - Specify exact notes with timing");
|
||||
println!(" chord - Play chord progressions");
|
||||
println!(" arpeggio - Arpeggiate chords");
|
||||
println!(" sequence - Step sequencer patterns");
|
||||
println!();
|
||||
|
||||
println!("Effect Types:");
|
||||
println!(" lowpass - Low-pass filter (removes highs)");
|
||||
println!(" highpass - High-pass filter (removes lows)");
|
||||
println!(" delay - Echo effect");
|
||||
println!(" reverb - Spatial reverb effect");
|
||||
println!(" chorus - Modulation/thickening effect");
|
||||
println!(" distortion - Harmonic distortion");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_json_command(
|
||||
config_path: PathBuf,
|
||||
output_override: Option<String>,
|
||||
show_example: bool,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
if show_example {
|
||||
let example = CompositionConfig::example();
|
||||
println!("{}", example.to_json_pretty()?);
|
||||
println!("\n# Save this to a .json file and run:");
|
||||
println!("# cargo run --bin musicgen json your_config.json");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
println!("Loading configuration from: {}", config_path.display());
|
||||
|
||||
let config = CompositionConfig::from_file(&config_path)?;
|
||||
|
||||
println!("Composition: {}", config.metadata.title);
|
||||
println!("Key: {:?}", config.composition.key);
|
||||
println!("Scale: {}", config.composition.scale);
|
||||
println!("Tempo: {} BPM", config.composition.tempo);
|
||||
println!("Measures: {}", config.composition.measures);
|
||||
println!("Tracks: {}", config.tracks.len());
|
||||
|
||||
// Use composition system
|
||||
let mut composition = Composition::from_config(config.clone())?;
|
||||
|
||||
println!("\nGenerating composition...");
|
||||
composition.generate()?;
|
||||
|
||||
let stats = composition.get_stats();
|
||||
println!(
|
||||
"Generated {} tracks with {} total notes",
|
||||
stats.track_count, stats.total_notes
|
||||
);
|
||||
|
||||
// Export based on config
|
||||
let exporter = AudioExporter::default();
|
||||
let filename = output_override.unwrap_or(config.export.filename.clone());
|
||||
let filename_with_ext = if !filename.ends_with(".wav") {
|
||||
format!("{}.wav", filename)
|
||||
} else {
|
||||
filename
|
||||
};
|
||||
|
||||
println!("\nExporting to: output/{}", filename_with_ext);
|
||||
|
||||
// Convert to core Composition format for export
|
||||
let export_composition = composition.to_composition();
|
||||
|
||||
if config.export.stereo {
|
||||
exporter.export_stereo_wav(
|
||||
&export_composition,
|
||||
&filename_with_ext,
|
||||
config.export.max_duration,
|
||||
)?;
|
||||
} else {
|
||||
exporter.export_wav(
|
||||
&export_composition,
|
||||
&filename_with_ext,
|
||||
config.export.max_duration,
|
||||
)?;
|
||||
}
|
||||
|
||||
// Generate variations if requested
|
||||
if let Some(ref variations) = config.export.variations {
|
||||
println!("\nGenerating {} variations...", variations.count);
|
||||
let base_name = filename_with_ext.trim_end_matches(".wav");
|
||||
exporter.export_variations(
|
||||
&export_composition,
|
||||
base_name,
|
||||
variations.count,
|
||||
config.export.max_duration,
|
||||
)?;
|
||||
}
|
||||
|
||||
println!("\nExport complete!");
|
||||
Ok(())
|
||||
}
|
730
src/patterns.rs
Normal file
730
src/patterns.rs
Normal file
@ -0,0 +1,730 @@
|
||||
//! Rhythmic and melodic pattern generation module
|
||||
//!
|
||||
//! This module provides tools for generating and manipulating musical patterns,
|
||||
//! including rhythm patterns, melodic sequences, and pattern transformations.
|
||||
|
||||
use crate::scales::Scale;
|
||||
use rand::Rng;
|
||||
|
||||
/// A rhythmic pattern representation
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RhythmPattern {
|
||||
pub steps: Vec<RhythmStep>,
|
||||
pub steps_per_beat: usize,
|
||||
pub length: usize, // Total number of steps
|
||||
}
|
||||
|
||||
/// A single step in a rhythm pattern
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct RhythmStep {
|
||||
pub active: bool,
|
||||
pub velocity: f32,
|
||||
pub accent: bool,
|
||||
}
|
||||
|
||||
impl RhythmStep {
|
||||
pub fn new(active: bool, velocity: f32, accent: bool) -> Self {
|
||||
Self {
|
||||
active,
|
||||
velocity: velocity.clamp(0.0, 1.0),
|
||||
accent,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn hit(velocity: f32) -> Self {
|
||||
Self::new(true, velocity, false)
|
||||
}
|
||||
|
||||
pub fn accent_hit(velocity: f32) -> Self {
|
||||
Self::new(true, velocity, true)
|
||||
}
|
||||
|
||||
pub fn rest() -> Self {
|
||||
Self::new(false, 0.0, false)
|
||||
}
|
||||
}
|
||||
|
||||
impl RhythmPattern {
|
||||
/// Create a new rhythm pattern
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `length` - Total number of steps in the pattern
|
||||
/// * `steps_per_beat` - Number of steps per beat (e.g., 4 for sixteenth notes)
|
||||
pub fn new(length: usize, steps_per_beat: usize) -> Self {
|
||||
let steps = vec![RhythmStep::rest(); length];
|
||||
Self {
|
||||
steps,
|
||||
steps_per_beat,
|
||||
length,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a basic kick drum pattern (4/4 time)
|
||||
pub fn kick_pattern() -> Self {
|
||||
let mut pattern = Self::new(16, 4);
|
||||
pattern.steps[0] = RhythmStep::accent_hit(1.0); // Beat 1
|
||||
pattern.steps[4] = RhythmStep::hit(0.8); // Beat 2
|
||||
pattern.steps[8] = RhythmStep::accent_hit(1.0); // Beat 3
|
||||
pattern.steps[12] = RhythmStep::hit(0.8); // Beat 4
|
||||
pattern
|
||||
}
|
||||
|
||||
/// Create a basic snare pattern
|
||||
pub fn snare_pattern() -> Self {
|
||||
let mut pattern = Self::new(16, 4);
|
||||
pattern.steps[4] = RhythmStep::accent_hit(0.9); // Beat 2
|
||||
pattern.steps[12] = RhythmStep::accent_hit(0.9); // Beat 4
|
||||
pattern
|
||||
}
|
||||
|
||||
/// Create a hi-hat pattern
|
||||
pub fn hihat_pattern() -> Self {
|
||||
let mut pattern = Self::new(16, 4);
|
||||
for i in 0..16 {
|
||||
if i % 2 == 0 {
|
||||
pattern.steps[i] = RhythmStep::hit(0.6);
|
||||
} else {
|
||||
pattern.steps[i] = RhythmStep::hit(0.4);
|
||||
}
|
||||
}
|
||||
pattern
|
||||
}
|
||||
|
||||
/// Create a Euclidean rhythm pattern
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `hits` - Number of hits to distribute
|
||||
/// * `steps` - Total number of steps
|
||||
pub fn euclidean(hits: usize, steps: usize) -> Self {
|
||||
let mut pattern = Self::new(steps, 4);
|
||||
|
||||
if hits == 0 {
|
||||
return pattern;
|
||||
}
|
||||
|
||||
let mut remainders = vec![hits];
|
||||
let mut counts = vec![steps / hits];
|
||||
|
||||
let mut divisor = steps % hits;
|
||||
let mut level = 0;
|
||||
|
||||
while divisor != 0 && level < 16 {
|
||||
let temp_divisor = remainders[level] % divisor;
|
||||
let temp_count = remainders[level] / divisor;
|
||||
|
||||
counts.push(temp_count);
|
||||
remainders.push(divisor);
|
||||
|
||||
remainders[level] = temp_divisor;
|
||||
divisor = temp_divisor;
|
||||
level += 1;
|
||||
}
|
||||
|
||||
// Build the pattern
|
||||
let mut result = Vec::new();
|
||||
for i in 0..counts.len() {
|
||||
for _ in 0..remainders[i] {
|
||||
if i % 2 == 0 {
|
||||
result.push(true);
|
||||
} else {
|
||||
result.push(false);
|
||||
}
|
||||
for _ in 1..counts[i] {
|
||||
result.push(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fill remaining steps
|
||||
while result.len() < steps {
|
||||
result.push(false);
|
||||
}
|
||||
|
||||
for (i, &active) in result.iter().enumerate() {
|
||||
if i < pattern.steps.len() {
|
||||
pattern.steps[i] = if active {
|
||||
RhythmStep::hit(0.8)
|
||||
} else {
|
||||
RhythmStep::rest()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
pattern
|
||||
}
|
||||
|
||||
/// Set a step in the pattern
|
||||
pub fn set_step(&mut self, index: usize, step: RhythmStep) {
|
||||
if index < self.steps.len() {
|
||||
self.steps[index] = step;
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a step from the pattern
|
||||
pub fn get_step(&self, index: usize) -> Option<RhythmStep> {
|
||||
self.steps.get(index).copied()
|
||||
}
|
||||
|
||||
/// Rotate the pattern by a number of steps
|
||||
pub fn rotate(&mut self, steps: i32) {
|
||||
if self.steps.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let len = self.steps.len() as i32;
|
||||
let effective_steps = ((steps % len) + len) % len;
|
||||
|
||||
self.steps.rotate_right(effective_steps as usize);
|
||||
}
|
||||
|
||||
/// Reverse the pattern
|
||||
pub fn reverse(&mut self) {
|
||||
self.steps.reverse();
|
||||
}
|
||||
|
||||
/// Add random variations to the pattern
|
||||
pub fn add_variation(&mut self, probability: f32) {
|
||||
let mut rng = rand::thread_rng();
|
||||
|
||||
for step in &mut self.steps {
|
||||
if rng.r#gen::<f32>() < probability {
|
||||
if step.active {
|
||||
// Randomly remove hits
|
||||
if rng.r#gen::<f32>() < 0.3 {
|
||||
step.active = false;
|
||||
} else {
|
||||
// Vary velocity
|
||||
step.velocity = (step.velocity + rng.gen_range(-0.2..0.2)).clamp(0.0, 1.0);
|
||||
}
|
||||
} else {
|
||||
// Randomly add hits
|
||||
if rng.r#gen::<f32>() < 0.1 {
|
||||
step.active = true;
|
||||
step.velocity = 0.3 + rng.r#gen::<f32>() * 0.4;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Get timing information for active steps
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `beat_duration` - Duration of one beat in samples
|
||||
///
|
||||
/// # Returns
|
||||
/// Vector of (step_index, timing_in_samples, velocity)
|
||||
pub fn get_timings(&self, beat_duration: usize) -> Vec<(usize, usize, f32)> {
|
||||
let mut timings = Vec::new();
|
||||
let step_duration = beat_duration / self.steps_per_beat;
|
||||
|
||||
for (i, step) in self.steps.iter().enumerate() {
|
||||
if step.active {
|
||||
let timing = (i * step_duration) / self.steps_per_beat;
|
||||
timings.push((i, timing, step.velocity));
|
||||
}
|
||||
}
|
||||
|
||||
timings
|
||||
}
|
||||
|
||||
/// Combine this pattern with another pattern
|
||||
pub fn combine(&self, other: &RhythmPattern, mode: CombineMode) -> RhythmPattern {
|
||||
let max_len = self.steps.len().max(other.steps.len());
|
||||
let mut combined = RhythmPattern::new(max_len, self.steps_per_beat);
|
||||
|
||||
for i in 0..max_len {
|
||||
let self_step = self.steps.get(i).copied().unwrap_or(RhythmStep::rest());
|
||||
let other_step = other.steps.get(i).copied().unwrap_or(RhythmStep::rest());
|
||||
|
||||
combined.steps[i] = match mode {
|
||||
CombineMode::Or => {
|
||||
if self_step.active || other_step.active {
|
||||
RhythmStep::hit(self_step.velocity.max(other_step.velocity))
|
||||
} else {
|
||||
RhythmStep::rest()
|
||||
}
|
||||
}
|
||||
CombineMode::And => {
|
||||
if self_step.active && other_step.active {
|
||||
RhythmStep::hit((self_step.velocity + other_step.velocity) / 2.0)
|
||||
} else {
|
||||
RhythmStep::rest()
|
||||
}
|
||||
}
|
||||
CombineMode::Xor => {
|
||||
if self_step.active ^ other_step.active {
|
||||
RhythmStep::hit(self_step.velocity.max(other_step.velocity))
|
||||
} else {
|
||||
RhythmStep::rest()
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
combined
|
||||
}
|
||||
}
|
||||
|
||||
/// Modes for combining rhythm patterns
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum CombineMode {
|
||||
Or, // Either pattern has a hit
|
||||
And, // Both patterns have a hit
|
||||
Xor, // Only one pattern has a hit
|
||||
}
|
||||
|
||||
/// A melodic pattern representation
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MelodicPattern {
|
||||
pub notes: Vec<PatternNote>,
|
||||
pub scale: Scale,
|
||||
pub octave_range: u8,
|
||||
}
|
||||
|
||||
/// A note in a melodic pattern
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct PatternNote {
|
||||
pub scale_degree: usize, // 1-based scale degree
|
||||
pub octave_offset: i8, // Octave offset from base
|
||||
pub duration: f32, // In beats
|
||||
pub velocity: f32,
|
||||
pub is_rest: bool,
|
||||
}
|
||||
|
||||
impl PatternNote {
|
||||
pub fn new(scale_degree: usize, octave_offset: i8, duration: f32, velocity: f32) -> Self {
|
||||
Self {
|
||||
scale_degree,
|
||||
octave_offset,
|
||||
duration,
|
||||
velocity: velocity.clamp(0.0, 1.0),
|
||||
is_rest: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn rest(duration: f32) -> Self {
|
||||
Self {
|
||||
scale_degree: 1,
|
||||
octave_offset: 0,
|
||||
duration,
|
||||
velocity: 0.0,
|
||||
is_rest: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MelodicPattern {
|
||||
/// Create a new melodic pattern
|
||||
pub fn new(scale: Scale, octave_range: u8) -> Self {
|
||||
Self {
|
||||
notes: Vec::new(),
|
||||
scale,
|
||||
octave_range,
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a note to the pattern
|
||||
pub fn add_note(&mut self, note: PatternNote) {
|
||||
self.notes.push(note);
|
||||
}
|
||||
|
||||
/// Generate a random melodic pattern
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `length` - Number of notes in the pattern
|
||||
/// * `note_durations` - Possible note durations
|
||||
pub fn generate_random(&mut self, length: usize, note_durations: &[f32]) {
|
||||
let mut rng = rand::thread_rng();
|
||||
self.notes.clear();
|
||||
|
||||
let scale_degrees = self.scale.intervals.len();
|
||||
|
||||
for _ in 0..length {
|
||||
if rng.r#gen::<f32>() < 0.1 {
|
||||
// 10% chance of rest
|
||||
let duration = note_durations[rng.gen_range(0..note_durations.len())];
|
||||
self.notes.push(PatternNote::rest(duration));
|
||||
} else {
|
||||
let degree = rng.gen_range(1..=scale_degrees);
|
||||
let octave_offset = rng.gen_range(-1..=1);
|
||||
let duration = note_durations[rng.gen_range(0..note_durations.len())];
|
||||
let velocity = 0.5 + rng.r#gen::<f32>() * 0.4;
|
||||
|
||||
self.notes
|
||||
.push(PatternNote::new(degree, octave_offset, duration, velocity));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate an ascending scale pattern
|
||||
pub fn generate_ascending(&mut self, octaves: u8) {
|
||||
self.notes.clear();
|
||||
|
||||
for octave in 0..octaves {
|
||||
for (i, _) in self.scale.intervals.iter().enumerate() {
|
||||
let degree = i + 1;
|
||||
let octave_offset = octave as i8;
|
||||
self.notes
|
||||
.push(PatternNote::new(degree, octave_offset, 0.5, 0.8));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate a descending scale pattern
|
||||
pub fn generate_descending(&mut self, octaves: u8) {
|
||||
self.notes.clear();
|
||||
|
||||
for octave in (0..octaves).rev() {
|
||||
for (i, _) in self.scale.intervals.iter().enumerate().rev() {
|
||||
let degree = i + 1;
|
||||
let octave_offset = octave as i8;
|
||||
self.notes
|
||||
.push(PatternNote::new(degree, octave_offset, 0.5, 0.8));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate an arpeggio pattern
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `chord_degrees` - Scale degrees that form the chord (e.g., [1, 3, 5])
|
||||
/// * `direction` - 1 for ascending, -1 for descending
|
||||
pub fn generate_arpeggio(
|
||||
&mut self,
|
||||
chord_degrees: &[usize],
|
||||
direction: i8,
|
||||
repetitions: usize,
|
||||
) {
|
||||
self.notes.clear();
|
||||
|
||||
for _ in 0..repetitions {
|
||||
let degrees = if direction > 0 {
|
||||
chord_degrees.to_vec()
|
||||
} else {
|
||||
let mut rev = chord_degrees.to_vec();
|
||||
rev.reverse();
|
||||
rev
|
||||
};
|
||||
|
||||
for °ree in °rees {
|
||||
self.notes.push(PatternNote::new(degree, 0, 0.25, 0.7));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply swing timing to the pattern
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `swing_ratio` - Swing ratio (0.5 = straight, 0.67 = heavy swing)
|
||||
pub fn apply_swing(&mut self, swing_ratio: f32) {
|
||||
let swing_ratio = swing_ratio.clamp(0.5, 0.8);
|
||||
|
||||
for (i, note) in self.notes.iter_mut().enumerate() {
|
||||
if i % 2 == 1 && !note.is_rest {
|
||||
// Apply swing to off-beat notes
|
||||
note.duration *= swing_ratio;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Transpose the pattern by semitones
|
||||
pub fn transpose(&mut self, semitones: i8) {
|
||||
// Note: This would require adjusting the scale root
|
||||
// For now, we'll adjust octave offsets when possible
|
||||
let octave_adjustment = semitones / 12;
|
||||
|
||||
for note in &mut self.notes {
|
||||
note.octave_offset += octave_adjustment;
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the MIDI notes for this pattern
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `base_octave` - Base octave for the pattern
|
||||
///
|
||||
/// # Returns
|
||||
/// Vector of (midi_note, start_time, duration, velocity)
|
||||
pub fn get_midi_notes(&self, base_octave: u8) -> Vec<(u8, f32, f32, f32)> {
|
||||
let mut midi_notes = Vec::new();
|
||||
let mut current_time = 0.0;
|
||||
|
||||
for note in &self.notes {
|
||||
if !note.is_rest {
|
||||
if let Some(midi_note) = self.scale.get_degree(
|
||||
note.scale_degree,
|
||||
(base_octave as i8 + note.octave_offset).max(0) as u8,
|
||||
) {
|
||||
midi_notes.push((midi_note, current_time, note.duration, note.velocity));
|
||||
}
|
||||
}
|
||||
current_time += note.duration;
|
||||
}
|
||||
|
||||
midi_notes
|
||||
}
|
||||
|
||||
/// Get the total duration of the pattern
|
||||
pub fn total_duration(&self) -> f32 {
|
||||
self.notes.iter().map(|n| n.duration).sum()
|
||||
}
|
||||
|
||||
/// Repeat the pattern a number of times
|
||||
pub fn repeat(&mut self, times: usize) {
|
||||
if times <= 1 {
|
||||
return;
|
||||
}
|
||||
|
||||
let original_notes = self.notes.clone();
|
||||
self.notes.clear();
|
||||
|
||||
for _ in 0..times {
|
||||
self.notes.extend(original_notes.iter().cloned());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Pattern transformation utilities
|
||||
pub struct PatternTransforms;
|
||||
|
||||
impl PatternTransforms {
|
||||
/// Create a polyrhythm by combining patterns of different lengths
|
||||
pub fn create_polyrhythm(patterns: Vec<RhythmPattern>) -> RhythmPattern {
|
||||
if patterns.is_empty() {
|
||||
return RhythmPattern::new(16, 4);
|
||||
}
|
||||
|
||||
// Find the least common multiple of all pattern lengths
|
||||
let lcm = patterns.iter().fold(1, |acc, p| lcm(acc, p.length));
|
||||
let steps_per_beat = patterns[0].steps_per_beat;
|
||||
|
||||
let mut result = RhythmPattern::new(lcm, steps_per_beat);
|
||||
|
||||
for (i, step) in result.steps.iter_mut().enumerate() {
|
||||
let mut combined_velocity = 0.0;
|
||||
let mut has_hit = false;
|
||||
|
||||
for pattern in &patterns {
|
||||
let pattern_index = i % pattern.length;
|
||||
if pattern.steps[pattern_index].active {
|
||||
has_hit = true;
|
||||
combined_velocity += pattern.steps[pattern_index].velocity;
|
||||
}
|
||||
}
|
||||
|
||||
if has_hit {
|
||||
*step = RhythmStep::hit(combined_velocity / patterns.len() as f32);
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// Create a rhythmic canon (same pattern with time offsets)
|
||||
pub fn create_canon(
|
||||
pattern: &RhythmPattern,
|
||||
tracks: usize,
|
||||
offset_steps: usize,
|
||||
) -> Vec<RhythmPattern> {
|
||||
let mut canons = Vec::new();
|
||||
|
||||
for track in 0..tracks {
|
||||
let mut canon_pattern = pattern.clone();
|
||||
let offset = (track * offset_steps) % pattern.length;
|
||||
canon_pattern.rotate(offset as i32);
|
||||
canons.push(canon_pattern);
|
||||
}
|
||||
|
||||
canons
|
||||
}
|
||||
|
||||
/// Generate variations of a melodic pattern
|
||||
pub fn generate_melodic_variations(
|
||||
pattern: &MelodicPattern,
|
||||
variations: usize,
|
||||
) -> Vec<MelodicPattern> {
|
||||
let mut variations_vec = Vec::new();
|
||||
let mut rng = rand::thread_rng();
|
||||
|
||||
for _ in 0..variations {
|
||||
let mut variation = pattern.clone();
|
||||
|
||||
// Apply random transformations
|
||||
match rng.gen_range(0..4) {
|
||||
0 => {
|
||||
// Rhythmic variation
|
||||
for note in &mut variation.notes {
|
||||
if rng.r#gen::<f32>() < 0.3 {
|
||||
note.duration *= rng.gen_range(0.5..2.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
1 => {
|
||||
// Octave displacement
|
||||
for note in &mut variation.notes {
|
||||
if rng.r#gen::<f32>() < 0.2 {
|
||||
note.octave_offset += if rng.r#gen::<bool>() { 1 } else { -1 };
|
||||
}
|
||||
}
|
||||
}
|
||||
2 => {
|
||||
// Add ornaments (grace notes)
|
||||
let mut ornamented = Vec::new();
|
||||
for note in &variation.notes {
|
||||
if rng.r#gen::<f32>() < 0.15 && !note.is_rest {
|
||||
// Add grace note
|
||||
let grace_degree = if note.scale_degree > 1 {
|
||||
note.scale_degree - 1
|
||||
} else {
|
||||
note.scale_degree + 1
|
||||
};
|
||||
ornamented.push(PatternNote::new(
|
||||
grace_degree,
|
||||
note.octave_offset,
|
||||
0.1,
|
||||
note.velocity * 0.7,
|
||||
));
|
||||
}
|
||||
ornamented.push(*note);
|
||||
}
|
||||
variation.notes = ornamented;
|
||||
}
|
||||
3 => {
|
||||
// Dynamic variation
|
||||
for note in &mut variation.notes {
|
||||
note.velocity = (note.velocity + rng.gen_range(-0.2..0.2)).clamp(0.1, 1.0);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
variations_vec.push(variation);
|
||||
}
|
||||
|
||||
variations_vec
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculate least common multiple
|
||||
fn lcm(a: usize, b: usize) -> usize {
|
||||
a * b / gcd(a, b)
|
||||
}
|
||||
|
||||
/// Calculate greatest common divisor
|
||||
fn gcd(a: usize, b: usize) -> usize {
|
||||
if b == 0 { a } else { gcd(b, a % b) }
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::scales::{Scale, ScaleType};
|
||||
|
||||
#[test]
|
||||
fn test_rhythm_pattern_creation() {
|
||||
let pattern = RhythmPattern::new(16, 4);
|
||||
assert_eq!(pattern.length, 16);
|
||||
assert_eq!(pattern.steps_per_beat, 4);
|
||||
assert_eq!(pattern.steps.len(), 16);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_kick_pattern() {
|
||||
let pattern = RhythmPattern::kick_pattern();
|
||||
assert!(pattern.steps[0].active);
|
||||
assert!(pattern.steps[8].active);
|
||||
assert!(pattern.steps[0].accent);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_euclidean_rhythm() {
|
||||
let pattern = RhythmPattern::euclidean(3, 8);
|
||||
let active_count = pattern.steps.iter().filter(|s| s.active).count();
|
||||
assert_eq!(active_count, 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pattern_rotation() {
|
||||
let mut pattern = RhythmPattern::kick_pattern();
|
||||
let original_first = pattern.steps[0].active;
|
||||
pattern.rotate(1);
|
||||
assert_eq!(pattern.steps[1].active, original_first);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pattern_combination() {
|
||||
let kick = RhythmPattern::kick_pattern();
|
||||
let snare = RhythmPattern::snare_pattern();
|
||||
let combined = kick.combine(&snare, CombineMode::Or);
|
||||
|
||||
// Should have hits from both patterns
|
||||
assert!(combined.steps[0].active); // Kick
|
||||
assert!(combined.steps[4].active); // Snare
|
||||
assert!(combined.steps[8].active); // Kick
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_melodic_pattern_creation() {
|
||||
let scale = Scale::new(ScaleType::Major, 60);
|
||||
let pattern = MelodicPattern::new(scale, 2);
|
||||
assert_eq!(pattern.octave_range, 2);
|
||||
assert_eq!(pattern.notes.len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ascending_pattern() {
|
||||
let scale = Scale::new(ScaleType::Major, 60);
|
||||
let mut pattern = MelodicPattern::new(scale, 1);
|
||||
pattern.generate_ascending(1);
|
||||
|
||||
assert_eq!(pattern.notes.len(), 7); // 7 notes in major scale
|
||||
assert_eq!(pattern.notes[0].scale_degree, 1);
|
||||
assert_eq!(pattern.notes[6].scale_degree, 7);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_arpeggio_pattern() {
|
||||
let scale = Scale::new(ScaleType::Major, 60);
|
||||
let mut pattern = MelodicPattern::new(scale, 1);
|
||||
pattern.generate_arpeggio(&[1, 3, 5], 1, 2);
|
||||
|
||||
assert_eq!(pattern.notes.len(), 6); // 3 notes × 2 repetitions
|
||||
assert_eq!(pattern.notes[0].scale_degree, 1);
|
||||
assert_eq!(pattern.notes[1].scale_degree, 3);
|
||||
assert_eq!(pattern.notes[2].scale_degree, 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_midi_note_generation() {
|
||||
let scale = Scale::new(ScaleType::Major, 60);
|
||||
let mut pattern = MelodicPattern::new(scale, 1);
|
||||
pattern.add_note(PatternNote::new(1, 0, 1.0, 0.8)); // Root note
|
||||
pattern.add_note(PatternNote::new(3, 0, 1.0, 0.8)); // Third
|
||||
|
||||
let midi_notes = pattern.get_midi_notes(4);
|
||||
assert_eq!(midi_notes.len(), 2);
|
||||
assert_eq!(midi_notes[0].0, 60); // C4
|
||||
assert_eq!(midi_notes[1].0, 64); // E4
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pattern_total_duration() {
|
||||
let scale = Scale::new(ScaleType::Major, 60);
|
||||
let mut pattern = MelodicPattern::new(scale, 1);
|
||||
pattern.add_note(PatternNote::new(1, 0, 1.0, 0.8));
|
||||
pattern.add_note(PatternNote::new(3, 0, 2.0, 0.8));
|
||||
|
||||
assert_eq!(pattern.total_duration(), 3.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_polyrhythm_creation() {
|
||||
let pattern1 = RhythmPattern::euclidean(3, 4);
|
||||
let pattern2 = RhythmPattern::euclidean(2, 3);
|
||||
let patterns = vec![pattern1, pattern2];
|
||||
|
||||
let polyrhythm = PatternTransforms::create_polyrhythm(patterns);
|
||||
assert_eq!(polyrhythm.length, 12); // LCM of 4 and 3
|
||||
}
|
||||
}
|
476
src/scales.rs
Normal file
476
src/scales.rs
Normal file
@ -0,0 +1,476 @@
|
||||
//! Musical scales and chord generation module
|
||||
//!
|
||||
//! This module provides definitions for various musical scales, chord progressions,
|
||||
//! and utilities for generating harmonically pleasing note sequences.
|
||||
|
||||
use crate::midi_to_frequency;
|
||||
use rand::Rng;
|
||||
|
||||
/// Musical scale types
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum ScaleType {
|
||||
Major,
|
||||
Minor,
|
||||
Dorian,
|
||||
Phrygian,
|
||||
Lydian,
|
||||
Mixolydian,
|
||||
Aeolian,
|
||||
Locrian,
|
||||
Pentatonic,
|
||||
Blues,
|
||||
Chromatic,
|
||||
}
|
||||
|
||||
/// A musical scale containing note intervals
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Scale {
|
||||
pub scale_type: ScaleType,
|
||||
pub root_note: u8, // MIDI note number
|
||||
pub intervals: Vec<u8>, // Semitone intervals from root
|
||||
}
|
||||
|
||||
impl Scale {
|
||||
/// Create a new scale
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `scale_type` - Type of scale
|
||||
/// * `root_note` - Root note as MIDI number (0-127)
|
||||
pub fn new(scale_type: ScaleType, root_note: u8) -> Self {
|
||||
let intervals = match scale_type {
|
||||
ScaleType::Major => vec![0, 2, 4, 5, 7, 9, 11],
|
||||
ScaleType::Minor => vec![0, 2, 3, 5, 7, 8, 10],
|
||||
ScaleType::Dorian => vec![0, 2, 3, 5, 7, 9, 10],
|
||||
ScaleType::Phrygian => vec![0, 1, 3, 5, 7, 8, 10],
|
||||
ScaleType::Lydian => vec![0, 2, 4, 6, 7, 9, 11],
|
||||
ScaleType::Mixolydian => vec![0, 2, 4, 5, 7, 9, 10],
|
||||
ScaleType::Aeolian => vec![0, 2, 3, 5, 7, 8, 10], // Same as minor
|
||||
ScaleType::Locrian => vec![0, 1, 3, 5, 6, 8, 10],
|
||||
ScaleType::Pentatonic => vec![0, 2, 4, 7, 9],
|
||||
ScaleType::Blues => vec![0, 3, 5, 6, 7, 10],
|
||||
ScaleType::Chromatic => vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
|
||||
};
|
||||
|
||||
Self {
|
||||
scale_type,
|
||||
root_note,
|
||||
intervals,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get all notes in the scale within a given octave range
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `octave_range` - Number of octaves to span (default 1)
|
||||
///
|
||||
/// # Returns
|
||||
/// Vector of MIDI note numbers in the scale
|
||||
pub fn get_notes(&self, octave_range: u8) -> Vec<u8> {
|
||||
let mut notes = Vec::new();
|
||||
|
||||
for octave in 0..octave_range {
|
||||
for &interval in &self.intervals {
|
||||
let note = self.root_note + interval + (octave * 12);
|
||||
if note <= 127 {
|
||||
notes.push(note);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
notes
|
||||
}
|
||||
|
||||
/// Get notes in the scale as frequencies
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `octave_range` - Number of octaves to span
|
||||
///
|
||||
/// # Returns
|
||||
/// Vector of frequencies in Hz
|
||||
pub fn get_frequencies(&self, octave_range: u8) -> Vec<f32> {
|
||||
self.get_notes(octave_range)
|
||||
.iter()
|
||||
.map(|¬e| midi_to_frequency(note))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Get a specific degree of the scale
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `degree` - Scale degree (1-based, 1 = root)
|
||||
/// * `octave` - Octave number (0-based)
|
||||
///
|
||||
/// # Returns
|
||||
/// MIDI note number, or None if degree is invalid
|
||||
pub fn get_degree(&self, degree: usize, octave: u8) -> Option<u8> {
|
||||
if degree == 0 || degree > self.intervals.len() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let interval = self.intervals[degree - 1];
|
||||
let note = self.root_note + interval + (octave * 12);
|
||||
|
||||
if note <= 127 { Some(note) } else { None }
|
||||
}
|
||||
|
||||
/// Get a random note from the scale
|
||||
pub fn random_note(&self, octave_range: u8) -> u8 {
|
||||
let notes = self.get_notes(octave_range);
|
||||
let mut rng = rand::thread_rng();
|
||||
notes[rng.gen_range(0..notes.len())]
|
||||
}
|
||||
|
||||
/// Check if a MIDI note is in this scale
|
||||
pub fn contains_note(&self, note: u8) -> bool {
|
||||
let note_in_octave = note % 12;
|
||||
let root_in_octave = self.root_note % 12;
|
||||
|
||||
self.intervals
|
||||
.iter()
|
||||
.any(|&interval| (root_in_octave + interval) % 12 == note_in_octave)
|
||||
}
|
||||
}
|
||||
|
||||
/// Chord types
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum ChordType {
|
||||
Major,
|
||||
Minor,
|
||||
Diminished,
|
||||
Augmented,
|
||||
Sus2,
|
||||
Sus4,
|
||||
Major7,
|
||||
Minor7,
|
||||
Dominant7,
|
||||
Major9,
|
||||
Minor9,
|
||||
}
|
||||
|
||||
/// A musical chord
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Chord {
|
||||
pub chord_type: ChordType,
|
||||
pub root_note: u8,
|
||||
pub notes: Vec<u8>,
|
||||
}
|
||||
|
||||
impl Chord {
|
||||
/// Create a new chord
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `chord_type` - Type of chord
|
||||
/// * `root_note` - Root note as MIDI number
|
||||
pub fn new(chord_type: ChordType, root_note: u8) -> Self {
|
||||
let intervals = match chord_type {
|
||||
ChordType::Major => vec![0, 4, 7],
|
||||
ChordType::Minor => vec![0, 3, 7],
|
||||
ChordType::Diminished => vec![0, 3, 6],
|
||||
ChordType::Augmented => vec![0, 4, 8],
|
||||
ChordType::Sus2 => vec![0, 2, 7],
|
||||
ChordType::Sus4 => vec![0, 5, 7],
|
||||
ChordType::Major7 => vec![0, 4, 7, 11],
|
||||
ChordType::Minor7 => vec![0, 3, 7, 10],
|
||||
ChordType::Dominant7 => vec![0, 4, 7, 10],
|
||||
ChordType::Major9 => vec![0, 4, 7, 11, 14],
|
||||
ChordType::Minor9 => vec![0, 3, 7, 10, 14],
|
||||
};
|
||||
|
||||
let notes = intervals
|
||||
.iter()
|
||||
.map(|&interval| root_note + interval)
|
||||
.filter(|¬e| note <= 127)
|
||||
.collect();
|
||||
|
||||
Self {
|
||||
chord_type,
|
||||
root_note,
|
||||
notes,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get chord notes as frequencies
|
||||
pub fn get_frequencies(&self) -> Vec<f32> {
|
||||
self.notes
|
||||
.iter()
|
||||
.map(|¬e| midi_to_frequency(note))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Get an inversion of the chord
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `inversion` - Inversion number (0 = root position, 1 = first inversion, etc.)
|
||||
pub fn get_inversion(&self, inversion: usize) -> Vec<u8> {
|
||||
if inversion == 0 || inversion >= self.notes.len() {
|
||||
return self.notes.clone();
|
||||
}
|
||||
|
||||
let mut inverted = self.notes.clone();
|
||||
|
||||
// Move the bottom notes up an octave
|
||||
for i in 0..inversion {
|
||||
if inverted[i] + 12 <= 127 {
|
||||
inverted[i] += 12;
|
||||
}
|
||||
}
|
||||
|
||||
// Sort the notes
|
||||
inverted.sort();
|
||||
inverted
|
||||
}
|
||||
}
|
||||
|
||||
/// Chord progression patterns
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ChordProgression {
|
||||
pub scale: Scale,
|
||||
pub progression: Vec<usize>, // Scale degrees (1-based)
|
||||
}
|
||||
|
||||
impl ChordProgression {
|
||||
/// Create a new chord progression
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `scale` - The scale to build chords from
|
||||
/// * `progression` - Vector of scale degrees (1-based)
|
||||
pub fn new(scale: Scale, progression: Vec<usize>) -> Self {
|
||||
Self { scale, progression }
|
||||
}
|
||||
|
||||
/// Get common chord progressions
|
||||
pub fn common_progressions() -> Vec<Vec<usize>> {
|
||||
vec![
|
||||
vec![1, 4, 5, 1], // I-IV-V-I
|
||||
vec![1, 5, 6, 4], // I-V-vi-IV (pop progression)
|
||||
vec![6, 4, 1, 5], // vi-IV-I-V
|
||||
vec![1, 6, 4, 5], // I-vi-IV-V (50s progression)
|
||||
vec![2, 5, 1], // ii-V-I (jazz)
|
||||
vec![1, 7, 4, 1], // I-VII-IV-I
|
||||
vec![1, 3, 4, 1], // I-iii-IV-I
|
||||
]
|
||||
}
|
||||
|
||||
/// Generate chords for the progression
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `octave` - Base octave for the chords
|
||||
///
|
||||
/// # Returns
|
||||
/// Vector of chords
|
||||
pub fn generate_chords(&self, octave: u8) -> Vec<Chord> {
|
||||
self.progression
|
||||
.iter()
|
||||
.filter_map(|°ree| {
|
||||
self.scale.get_degree(degree, octave).map(|root_note| {
|
||||
// Determine chord type based on scale degree
|
||||
let chord_type = match self.scale.scale_type {
|
||||
ScaleType::Major => match degree {
|
||||
1 | 4 | 5 => ChordType::Major,
|
||||
2 | 3 | 6 => ChordType::Minor,
|
||||
7 => ChordType::Diminished,
|
||||
_ => ChordType::Major,
|
||||
},
|
||||
ScaleType::Minor => match degree {
|
||||
1 | 4 | 5 => ChordType::Minor,
|
||||
3 | 6 | 7 => ChordType::Major,
|
||||
2 => ChordType::Diminished,
|
||||
_ => ChordType::Minor,
|
||||
},
|
||||
_ => ChordType::Major, // Default for other scales
|
||||
};
|
||||
|
||||
Chord::new(chord_type, root_note)
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
/// Melody generator using scales
|
||||
pub struct MelodyGenerator {
|
||||
pub scale: Scale,
|
||||
pub octave_range: u8,
|
||||
available_notes: Vec<u8>,
|
||||
}
|
||||
|
||||
impl MelodyGenerator {
|
||||
/// Create a new melody generator
|
||||
pub fn new(scale: Scale, octave_range: u8) -> Self {
|
||||
let available_notes = scale.get_notes(octave_range);
|
||||
Self {
|
||||
scale,
|
||||
octave_range,
|
||||
available_notes,
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate a random melody
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `length` - Number of notes in the melody
|
||||
///
|
||||
/// # Returns
|
||||
/// Vector of MIDI note numbers
|
||||
pub fn generate_random_melody(&self, length: usize) -> Vec<u8> {
|
||||
let mut rng = rand::thread_rng();
|
||||
(0..length)
|
||||
.map(|_| self.available_notes[rng.gen_range(0..self.available_notes.len())])
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Generate a melody using step-wise motion
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `length` - Number of notes in the melody
|
||||
/// * `step_probability` - Probability of moving by step vs. leap (0.0 to 1.0)
|
||||
///
|
||||
/// # Returns
|
||||
/// Vector of MIDI note numbers
|
||||
pub fn generate_stepwise_melody(&self, length: usize, step_probability: f32) -> Vec<u8> {
|
||||
if self.available_notes.is_empty() || length == 0 {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let mut rng = rand::thread_rng();
|
||||
let mut melody = Vec::with_capacity(length);
|
||||
|
||||
// Start with a random note
|
||||
let mut current_index = rng.gen_range(0..self.available_notes.len());
|
||||
melody.push(self.available_notes[current_index]);
|
||||
|
||||
for _ in 1..length {
|
||||
if rng.r#gen::<f32>() < step_probability {
|
||||
// Step-wise motion (move to adjacent note in scale)
|
||||
let direction = if rng.r#gen::<bool>() { 1 } else { -1 };
|
||||
let new_index = (current_index as i32 + direction)
|
||||
.max(0)
|
||||
.min(self.available_notes.len() as i32 - 1)
|
||||
as usize;
|
||||
current_index = new_index;
|
||||
} else {
|
||||
// Leap (random note)
|
||||
current_index = rng.gen_range(0..self.available_notes.len());
|
||||
}
|
||||
|
||||
melody.push(self.available_notes[current_index]);
|
||||
}
|
||||
|
||||
melody
|
||||
}
|
||||
|
||||
/// Generate an arpeggio pattern
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `chord` - Chord to arpeggiate
|
||||
/// * `pattern_length` - Length of the arpeggio pattern
|
||||
///
|
||||
/// # Returns
|
||||
/// Vector of MIDI note numbers
|
||||
pub fn generate_arpeggio(&self, chord: &Chord, pattern_length: usize) -> Vec<u8> {
|
||||
if chord.notes.is_empty() || pattern_length == 0 {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let mut arpeggio = Vec::with_capacity(pattern_length);
|
||||
let chord_notes = &chord.notes;
|
||||
|
||||
for i in 0..pattern_length {
|
||||
let note_index = i % chord_notes.len();
|
||||
arpeggio.push(chord_notes[note_index]);
|
||||
}
|
||||
|
||||
arpeggio
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_scale_creation() {
|
||||
let scale = Scale::new(ScaleType::Major, 60); // C major
|
||||
assert_eq!(scale.intervals, vec![0, 2, 4, 5, 7, 9, 11]);
|
||||
assert_eq!(scale.root_note, 60);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_scale_notes() {
|
||||
let scale = Scale::new(ScaleType::Major, 60); // C major
|
||||
let notes = scale.get_notes(1);
|
||||
assert_eq!(notes, vec![60, 62, 64, 65, 67, 69, 71]); // C D E F G A B
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_scale_degree() {
|
||||
let scale = Scale::new(ScaleType::Major, 60); // C major
|
||||
assert_eq!(scale.get_degree(1, 0), Some(60)); // C
|
||||
assert_eq!(scale.get_degree(5, 0), Some(67)); // G
|
||||
assert_eq!(scale.get_degree(8, 0), None); // Invalid degree
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_chord_creation() {
|
||||
let chord = Chord::new(ChordType::Major, 60); // C major
|
||||
assert_eq!(chord.notes, vec![60, 64, 67]); // C E G
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_chord_inversion() {
|
||||
let chord = Chord::new(ChordType::Major, 60); // C major
|
||||
let first_inversion = chord.get_inversion(1);
|
||||
assert_eq!(first_inversion, vec![64, 67, 72]); // E G C
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_chord_progression() {
|
||||
let scale = Scale::new(ScaleType::Major, 60);
|
||||
let progression = ChordProgression::new(scale, vec![1, 4, 5, 1]);
|
||||
let chords = progression.generate_chords(4);
|
||||
assert_eq!(chords.len(), 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_melody_generator() {
|
||||
let scale = Scale::new(ScaleType::Pentatonic, 60);
|
||||
let generator = MelodyGenerator::new(scale, 2);
|
||||
let melody = generator.generate_random_melody(8);
|
||||
assert_eq!(melody.len(), 8);
|
||||
|
||||
// All notes should be in the scale
|
||||
for ¬e in &melody {
|
||||
assert!(generator.available_notes.contains(¬e));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_stepwise_melody() {
|
||||
let scale = Scale::new(ScaleType::Major, 60);
|
||||
let generator = MelodyGenerator::new(scale, 1);
|
||||
let melody = generator.generate_stepwise_melody(5, 1.0); // 100% stepwise
|
||||
assert_eq!(melody.len(), 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_arpeggio_generation() {
|
||||
let scale = Scale::new(ScaleType::Major, 60);
|
||||
let generator = MelodyGenerator::new(scale, 1);
|
||||
let chord = Chord::new(ChordType::Major, 60);
|
||||
let arpeggio = generator.generate_arpeggio(&chord, 6);
|
||||
|
||||
assert_eq!(arpeggio.len(), 6);
|
||||
// Should cycle through chord notes
|
||||
assert_eq!(arpeggio[0], chord.notes[0]);
|
||||
assert_eq!(arpeggio[3], chord.notes[0]); // After one full cycle
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_note_containment() {
|
||||
let scale = Scale::new(ScaleType::Major, 60); // C major
|
||||
assert!(scale.contains_note(60)); // C
|
||||
assert!(scale.contains_note(64)); // E
|
||||
assert!(!scale.contains_note(61)); // C#
|
||||
assert!(!scale.contains_note(63)); // D#
|
||||
}
|
||||
}
|
653
src/sequencer.rs
Normal file
653
src/sequencer.rs
Normal file
@ -0,0 +1,653 @@
|
||||
//! Sequencer module for playing and timing musical sequences
|
||||
//!
|
||||
//! This module provides a sequencer that can play back compositions,
|
||||
//! handle timing, and coordinate multiple tracks and patterns.
|
||||
|
||||
use crate::bpm_to_samples_per_beat;
|
||||
use crate::core::{Composition, InstrumentType};
|
||||
use crate::patterns::MelodicPattern;
|
||||
use crate::synthesis::{PolySynth, Waveform};
|
||||
use std::collections::VecDeque;
|
||||
|
||||
/// Main sequencer for playing compositions
|
||||
#[derive(Debug)]
|
||||
pub struct Sequencer {
|
||||
pub tempo: f32,
|
||||
pub is_playing: bool,
|
||||
pub current_position: f32, // Current position in beats
|
||||
pub loop_enabled: bool,
|
||||
pub loop_start: f32, // Loop start in beats
|
||||
pub loop_end: f32, // Loop end in beats
|
||||
|
||||
// Internal timing
|
||||
samples_per_beat: usize,
|
||||
current_sample: usize,
|
||||
|
||||
// Event scheduling
|
||||
scheduled_events: VecDeque<ScheduledEvent>,
|
||||
|
||||
// Playback state
|
||||
tracks: Vec<TrackState>,
|
||||
global_volume: f32,
|
||||
}
|
||||
|
||||
/// A scheduled musical event
|
||||
#[derive(Debug, Clone)]
|
||||
struct ScheduledEvent {
|
||||
pub event_time: f32, // Time in beats
|
||||
pub event_type: EventType,
|
||||
pub track_index: usize,
|
||||
}
|
||||
|
||||
/// Types of sequencer events
|
||||
#[derive(Debug, Clone)]
|
||||
#[allow(dead_code)]
|
||||
enum EventType {
|
||||
NoteOn { midi_note: u8, velocity: f32 },
|
||||
NoteOff { midi_note: u8 },
|
||||
VolumeChange { volume: f32 },
|
||||
TempoChange { new_tempo: f32 },
|
||||
}
|
||||
|
||||
/// State for each track in the sequencer
|
||||
#[derive(Debug)]
|
||||
struct TrackState {
|
||||
pub synth: PolySynth,
|
||||
pub volume: f32,
|
||||
pub is_muted: bool,
|
||||
pub current_notes: Vec<u8>, // Currently playing MIDI notes
|
||||
}
|
||||
|
||||
impl TrackState {
|
||||
fn new(waveform: Waveform, max_tracks: usize) -> Self {
|
||||
Self {
|
||||
synth: PolySynth::new(max_tracks, waveform),
|
||||
volume: 1.0,
|
||||
is_muted: false,
|
||||
current_notes: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Sequencer {
|
||||
/// Create a new sequencer
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `tempo` - Initial tempo in BPM
|
||||
pub fn new(tempo: f32) -> Self {
|
||||
let samples_per_beat = bpm_to_samples_per_beat(tempo);
|
||||
|
||||
Self {
|
||||
tempo,
|
||||
is_playing: false,
|
||||
current_position: 0.0,
|
||||
loop_enabled: false,
|
||||
loop_start: 0.0,
|
||||
loop_end: 16.0,
|
||||
samples_per_beat,
|
||||
current_sample: 0,
|
||||
scheduled_events: VecDeque::new(),
|
||||
tracks: Vec::new(),
|
||||
global_volume: 1.0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Load a composition into the sequencer
|
||||
pub fn load_composition(&mut self, composition: &Composition) -> Result<(), String> {
|
||||
self.clear();
|
||||
|
||||
// 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 mut track_state = TrackState::new(waveform, 8);
|
||||
track_state.volume = track.volume;
|
||||
self.tracks.push(track_state);
|
||||
}
|
||||
|
||||
// Schedule all note events
|
||||
self.schedule_composition_events(composition)?;
|
||||
|
||||
// Set loop end to composition duration
|
||||
self.loop_end = composition.total_duration;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Schedule events from a composition
|
||||
fn schedule_composition_events(&mut self, composition: &Composition) -> Result<(), String> {
|
||||
for (track_index, track) in composition.tracks.iter().enumerate() {
|
||||
for note in &track.notes {
|
||||
// Schedule note on
|
||||
let note_on_event = ScheduledEvent {
|
||||
event_time: note.start_time,
|
||||
event_type: EventType::NoteOn {
|
||||
midi_note: note.midi_note,
|
||||
velocity: note.velocity,
|
||||
},
|
||||
track_index,
|
||||
};
|
||||
|
||||
// Schedule note off
|
||||
let note_off_event = ScheduledEvent {
|
||||
event_time: note.start_time + note.duration,
|
||||
event_type: EventType::NoteOff {
|
||||
midi_note: note.midi_note,
|
||||
},
|
||||
track_index,
|
||||
};
|
||||
|
||||
self.scheduled_events.push_back(note_on_event);
|
||||
self.scheduled_events.push_back(note_off_event);
|
||||
}
|
||||
}
|
||||
|
||||
// Sort events by time
|
||||
let mut events: Vec<_> = self.scheduled_events.drain(..).collect();
|
||||
events.sort_by(|a, b| a.event_time.partial_cmp(&b.event_time).unwrap());
|
||||
self.scheduled_events.extend(events);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Start playback
|
||||
pub fn play(&mut self) {
|
||||
self.is_playing = true;
|
||||
}
|
||||
|
||||
/// Stop playback
|
||||
pub fn stop(&mut self) {
|
||||
self.is_playing = false;
|
||||
self.current_position = 0.0;
|
||||
self.current_sample = 0;
|
||||
|
||||
// Stop all playing notes
|
||||
for track in &mut self.tracks {
|
||||
for ¬e in track.current_notes.clone().iter() {
|
||||
track.synth.stop_note(crate::midi_to_frequency(note));
|
||||
}
|
||||
track.current_notes.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/// Pause playback
|
||||
pub fn pause(&mut self) {
|
||||
self.is_playing = false;
|
||||
}
|
||||
|
||||
/// Set the current playback position
|
||||
pub fn set_position(&mut self, position: f32) {
|
||||
self.current_position = position;
|
||||
self.current_sample = (position * self.samples_per_beat as f32) as usize;
|
||||
|
||||
// Stop all current notes when seeking
|
||||
for track in &mut self.tracks {
|
||||
for ¬e in track.current_notes.clone().iter() {
|
||||
track.synth.stop_note(crate::midi_to_frequency(note));
|
||||
}
|
||||
track.current_notes.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the tempo
|
||||
pub fn set_tempo(&mut self, tempo: f32) {
|
||||
self.tempo = tempo;
|
||||
self.samples_per_beat = bmp_to_samples_per_beat(tempo);
|
||||
}
|
||||
|
||||
/// Enable/disable looping
|
||||
pub fn set_loop(&mut self, enabled: bool, start: f32, end: f32) {
|
||||
self.loop_enabled = enabled;
|
||||
self.loop_start = start;
|
||||
self.loop_end = end;
|
||||
}
|
||||
|
||||
/// Set global volume
|
||||
pub fn set_global_volume(&mut self, volume: f32) {
|
||||
self.global_volume = volume.clamp(0.0, 1.0);
|
||||
}
|
||||
|
||||
/// Mute/unmute a track
|
||||
pub fn set_track_mute(&mut self, track_index: usize, muted: bool) {
|
||||
if let Some(track) = self.tracks.get_mut(track_index) {
|
||||
track.is_muted = muted;
|
||||
if muted {
|
||||
// Stop all notes in this track
|
||||
for ¬e in track.current_notes.clone().iter() {
|
||||
track.synth.stop_note(crate::midi_to_frequency(note));
|
||||
}
|
||||
track.current_notes.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Set track volume
|
||||
pub fn set_track_volume(&mut self, track_index: usize, volume: f32) {
|
||||
if let Some(track) = self.tracks.get_mut(track_index) {
|
||||
track.volume = volume.clamp(0.0, 1.0);
|
||||
}
|
||||
}
|
||||
|
||||
/// Process audio for a given number of samples
|
||||
pub fn process_audio(&mut self, buffer: &mut [f32]) -> Result<(), String> {
|
||||
if !self.is_playing {
|
||||
// Fill with silence
|
||||
for sample in buffer.iter_mut() {
|
||||
*sample = 0.0;
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
for i in 0..buffer.len() {
|
||||
// Update position
|
||||
self.current_position = self.current_sample as f32 / self.samples_per_beat as f32;
|
||||
|
||||
// Process scheduled events
|
||||
self.process_events_at_position(self.current_position);
|
||||
|
||||
// Generate audio sample
|
||||
let mut sample = 0.0;
|
||||
|
||||
for track in &mut self.tracks {
|
||||
if !track.is_muted {
|
||||
let track_sample = track.synth.next_sample();
|
||||
sample += track_sample * track.volume;
|
||||
}
|
||||
}
|
||||
|
||||
// Apply global volume
|
||||
buffer[i] = sample * self.global_volume;
|
||||
|
||||
// Advance sample counter
|
||||
self.current_sample += 1;
|
||||
|
||||
// Handle looping
|
||||
if self.loop_enabled && self.current_position >= self.loop_end {
|
||||
self.set_position(self.loop_start);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Process events that should occur at the current position
|
||||
fn process_events_at_position(&mut self, position: f32) {
|
||||
while let Some(event) = self.scheduled_events.front() {
|
||||
if event.event_time <= position {
|
||||
let event = self.scheduled_events.pop_front().unwrap();
|
||||
self.process_event(event);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Process a single event
|
||||
fn process_event(&mut self, event: ScheduledEvent) {
|
||||
if event.track_index >= self.tracks.len() {
|
||||
return;
|
||||
}
|
||||
|
||||
let track = &mut self.tracks[event.track_index];
|
||||
|
||||
if track.is_muted {
|
||||
return;
|
||||
}
|
||||
|
||||
match event.event_type {
|
||||
EventType::NoteOn {
|
||||
midi_note,
|
||||
velocity: _,
|
||||
} => {
|
||||
let frequency = crate::midi_to_frequency(midi_note);
|
||||
track.synth.play_note(frequency);
|
||||
track.current_notes.push(midi_note);
|
||||
}
|
||||
EventType::NoteOff { midi_note } => {
|
||||
let frequency = crate::midi_to_frequency(midi_note);
|
||||
track.synth.stop_note(frequency);
|
||||
track.current_notes.retain(|&n| n != midi_note);
|
||||
}
|
||||
EventType::VolumeChange { volume } => {
|
||||
track.volume = volume;
|
||||
}
|
||||
EventType::TempoChange { new_tempo } => {
|
||||
self.set_tempo(new_tempo);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Clear all scheduled events and reset state
|
||||
pub fn clear(&mut self) {
|
||||
self.scheduled_events.clear();
|
||||
self.tracks.clear();
|
||||
self.current_position = 0.0;
|
||||
self.current_sample = 0;
|
||||
self.is_playing = false;
|
||||
}
|
||||
|
||||
/// Get current playback information
|
||||
pub fn get_playback_info(&self) -> PlaybackInfo {
|
||||
PlaybackInfo {
|
||||
is_playing: self.is_playing,
|
||||
current_position: self.current_position,
|
||||
tempo: self.tempo,
|
||||
loop_enabled: self.loop_enabled,
|
||||
loop_start: self.loop_start,
|
||||
loop_end: self.loop_end,
|
||||
track_count: self.tracks.len(),
|
||||
active_notes: self.tracks.iter().map(|t| t.current_notes.len()).sum(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a real-time note event
|
||||
pub fn trigger_note(&mut self, track_index: usize, midi_note: u8, _velocity: f32) {
|
||||
if let Some(track) = self.tracks.get_mut(track_index) {
|
||||
if !track.is_muted {
|
||||
let frequency = crate::midi_to_frequency(midi_note);
|
||||
track.synth.play_note(frequency);
|
||||
track.current_notes.push(midi_note);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Release a real-time note
|
||||
pub fn release_note(&mut self, track_index: usize, midi_note: u8) {
|
||||
if let Some(track) = self.tracks.get_mut(track_index) {
|
||||
let frequency = crate::midi_to_frequency(midi_note);
|
||||
track.synth.stop_note(frequency);
|
||||
track.current_notes.retain(|&n| n != midi_note);
|
||||
}
|
||||
}
|
||||
|
||||
/// Schedule a pattern to play
|
||||
pub fn schedule_pattern(
|
||||
&mut self,
|
||||
pattern: &MelodicPattern,
|
||||
track_index: usize,
|
||||
start_time: f32,
|
||||
) {
|
||||
let midi_notes = pattern.get_midi_notes(4); // Use octave 4 as base
|
||||
|
||||
for (midi_note, offset_time, duration, velocity) in midi_notes {
|
||||
let note_on_event = ScheduledEvent {
|
||||
event_time: start_time + offset_time,
|
||||
event_type: EventType::NoteOn {
|
||||
midi_note,
|
||||
velocity,
|
||||
},
|
||||
track_index,
|
||||
};
|
||||
|
||||
let note_off_event = ScheduledEvent {
|
||||
event_time: start_time + offset_time + duration,
|
||||
event_type: EventType::NoteOff { midi_note },
|
||||
track_index,
|
||||
};
|
||||
|
||||
// Insert events in the correct position to maintain time order
|
||||
self.insert_event_sorted(note_on_event);
|
||||
self.insert_event_sorted(note_off_event);
|
||||
}
|
||||
}
|
||||
|
||||
/// Insert an event maintaining time order
|
||||
fn insert_event_sorted(&mut self, event: ScheduledEvent) {
|
||||
let mut insert_index = self.scheduled_events.len();
|
||||
|
||||
for (i, existing_event) in self.scheduled_events.iter().enumerate() {
|
||||
if event.event_time < existing_event.event_time {
|
||||
insert_index = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
self.scheduled_events.insert(insert_index, event);
|
||||
}
|
||||
}
|
||||
|
||||
/// Information about current playback state
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PlaybackInfo {
|
||||
pub is_playing: bool,
|
||||
pub current_position: f32,
|
||||
pub tempo: f32,
|
||||
pub loop_enabled: bool,
|
||||
pub loop_start: f32,
|
||||
pub loop_end: f32,
|
||||
pub track_count: usize,
|
||||
pub active_notes: usize,
|
||||
}
|
||||
|
||||
/// A real-time pattern player for live performance
|
||||
#[derive(Debug)]
|
||||
pub struct PatternPlayer {
|
||||
pub patterns: Vec<MelodicPattern>,
|
||||
pub current_pattern: usize,
|
||||
pub is_recording: bool,
|
||||
recorded_pattern: MelodicPattern,
|
||||
quantization: f32, // Beat quantization (e.g., 0.25 for sixteenth notes)
|
||||
}
|
||||
|
||||
impl PatternPlayer {
|
||||
/// Create a new pattern player
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
patterns: Vec::new(),
|
||||
current_pattern: 0,
|
||||
is_recording: false,
|
||||
recorded_pattern: MelodicPattern::new(
|
||||
crate::scales::Scale::new(crate::scales::ScaleType::Major, 60),
|
||||
2,
|
||||
),
|
||||
quantization: 0.25,
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a pattern to the player
|
||||
pub fn add_pattern(&mut self, pattern: MelodicPattern) {
|
||||
self.patterns.push(pattern);
|
||||
}
|
||||
|
||||
/// Select a pattern to play
|
||||
pub fn select_pattern(&mut self, index: usize) {
|
||||
if index < self.patterns.len() {
|
||||
self.current_pattern = index;
|
||||
}
|
||||
}
|
||||
|
||||
/// Start recording a new pattern
|
||||
pub fn start_recording(&mut self) {
|
||||
self.is_recording = true;
|
||||
self.recorded_pattern.notes.clear();
|
||||
}
|
||||
|
||||
/// Stop recording and save the pattern
|
||||
pub fn stop_recording(&mut self) -> Option<MelodicPattern> {
|
||||
if self.is_recording {
|
||||
self.is_recording = false;
|
||||
if !self.recorded_pattern.notes.is_empty() {
|
||||
return Some(self.recorded_pattern.clone());
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Record a note during recording
|
||||
pub fn record_note(&mut self, scale_degree: usize, duration: f32, velocity: f32) {
|
||||
if self.is_recording {
|
||||
let note = crate::patterns::PatternNote::new(scale_degree, 0, duration, velocity);
|
||||
self.recorded_pattern.add_note(note);
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the current pattern
|
||||
pub fn get_current_pattern(&self) -> Option<&MelodicPattern> {
|
||||
self.patterns.get(self.current_pattern)
|
||||
}
|
||||
|
||||
/// Set quantization amount
|
||||
pub fn set_quantization(&mut self, quantization: f32) {
|
||||
self.quantization = quantization.max(0.0625); // Minimum 64th note quantization
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for PatternPlayer {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
// Fix the typo in the helper function
|
||||
fn bmp_to_samples_per_beat(bpm: f32) -> usize {
|
||||
bpm_to_samples_per_beat(bpm)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::core::{CompositionBuilder, CompositionStyle};
|
||||
use crate::scales::{Scale, ScaleType};
|
||||
|
||||
#[test]
|
||||
fn test_sequencer_creation() {
|
||||
let sequencer = Sequencer::new(120.0);
|
||||
assert_eq!(sequencer.tempo, 120.0);
|
||||
assert!(!sequencer.is_playing);
|
||||
assert_eq!(sequencer.current_position, 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_playback_control() {
|
||||
let mut sequencer = Sequencer::new(120.0);
|
||||
|
||||
sequencer.play();
|
||||
assert!(sequencer.is_playing);
|
||||
|
||||
sequencer.pause();
|
||||
assert!(!sequencer.is_playing);
|
||||
|
||||
sequencer.stop();
|
||||
assert!(!sequencer.is_playing);
|
||||
assert_eq!(sequencer.current_position, 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_position_setting() {
|
||||
let mut sequencer = Sequencer::new(120.0);
|
||||
sequencer.set_position(4.0);
|
||||
assert_eq!(sequencer.current_position, 4.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tempo_change() {
|
||||
let mut sequencer = Sequencer::new(120.0);
|
||||
sequencer.set_tempo(140.0);
|
||||
assert_eq!(sequencer.tempo, 140.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_loop_settings() {
|
||||
let mut sequencer = Sequencer::new(120.0);
|
||||
sequencer.set_loop(true, 0.0, 8.0);
|
||||
assert!(sequencer.loop_enabled);
|
||||
assert_eq!(sequencer.loop_start, 0.0);
|
||||
assert_eq!(sequencer.loop_end, 8.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_composition_loading() {
|
||||
let mut sequencer = Sequencer::new(120.0);
|
||||
let mut composition = CompositionBuilder::new()
|
||||
.style(CompositionStyle::Electronic)
|
||||
.measures(4)
|
||||
.build();
|
||||
|
||||
let _ = composition.generate();
|
||||
let result = sequencer.load_composition(&composition);
|
||||
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(sequencer.tracks.len(), composition.tracks.len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pattern_player() {
|
||||
let mut player = PatternPlayer::new();
|
||||
let scale = Scale::new(ScaleType::Major, 60);
|
||||
let mut pattern = MelodicPattern::new(scale, 1);
|
||||
pattern.generate_ascending(1);
|
||||
|
||||
player.add_pattern(pattern);
|
||||
assert_eq!(player.patterns.len(), 1);
|
||||
|
||||
player.select_pattern(0);
|
||||
assert_eq!(player.current_pattern, 0);
|
||||
assert!(player.get_current_pattern().is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pattern_recording() {
|
||||
let mut player = PatternPlayer::new();
|
||||
|
||||
player.start_recording();
|
||||
assert!(player.is_recording);
|
||||
|
||||
player.record_note(1, 1.0, 0.8);
|
||||
player.record_note(3, 1.0, 0.7);
|
||||
|
||||
let recorded = player.stop_recording();
|
||||
assert!(recorded.is_some());
|
||||
assert_eq!(recorded.unwrap().notes.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_real_time_notes() {
|
||||
let mut sequencer = Sequencer::new(120.0);
|
||||
|
||||
// Create a simple composition to have tracks
|
||||
let mut composition = CompositionBuilder::new().measures(1).build();
|
||||
let _ = composition.generate();
|
||||
let _ = sequencer.load_composition(&composition);
|
||||
|
||||
// Only test if we have tracks
|
||||
if sequencer.tracks.len() > 0 {
|
||||
// Test real-time note triggering
|
||||
sequencer.trigger_note(0, 60, 0.8);
|
||||
assert!(sequencer.tracks[0].current_notes.contains(&60));
|
||||
|
||||
sequencer.release_note(0, 60);
|
||||
assert!(!sequencer.tracks[0].current_notes.contains(&60));
|
||||
} else {
|
||||
// If no tracks, just verify the sequencer doesn't crash
|
||||
sequencer.trigger_note(0, 60, 0.8);
|
||||
sequencer.release_note(0, 60);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_track_control() {
|
||||
let mut sequencer = Sequencer::new(120.0);
|
||||
|
||||
// Create a composition with multiple tracks
|
||||
let mut composition = CompositionBuilder::new().measures(2).build();
|
||||
let _ = composition.generate();
|
||||
let _ = sequencer.load_composition(&composition);
|
||||
|
||||
if sequencer.tracks.len() > 0 {
|
||||
// Test muting
|
||||
sequencer.set_track_mute(0, true);
|
||||
assert!(sequencer.tracks[0].is_muted);
|
||||
|
||||
// Test volume control
|
||||
sequencer.set_track_volume(0, 0.5);
|
||||
assert_eq!(sequencer.tracks[0].volume, 0.5);
|
||||
}
|
||||
}
|
||||
}
|
456
src/synthesis.rs
Normal file
456
src/synthesis.rs
Normal file
@ -0,0 +1,456 @@
|
||||
//! Audio synthesis module for generating various waveforms and tones
|
||||
//!
|
||||
//! This module provides oscillators for generating basic waveforms like sine, square,
|
||||
//! sawtooth, and triangle waves, as well as more complex synthesis techniques.
|
||||
|
||||
use crate::{PI, SAMPLE_RATE};
|
||||
use std::f32::consts::TAU;
|
||||
|
||||
/// Different types of waveforms that can be generated
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum Waveform {
|
||||
Sine,
|
||||
Square,
|
||||
Sawtooth,
|
||||
Triangle,
|
||||
Noise,
|
||||
}
|
||||
|
||||
/// A basic oscillator that generates waveforms at a given frequency
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Oscillator {
|
||||
pub waveform: Waveform,
|
||||
pub frequency: f32,
|
||||
pub amplitude: f32,
|
||||
pub phase: f32,
|
||||
pub phase_increment: f32,
|
||||
}
|
||||
|
||||
impl Oscillator {
|
||||
/// Create a new oscillator
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `waveform` - Type of waveform to generate
|
||||
/// * `frequency` - Frequency in Hz
|
||||
/// * `amplitude` - Amplitude (0.0 to 1.0)
|
||||
///
|
||||
/// # Returns
|
||||
/// New oscillator instance
|
||||
pub fn new(waveform: Waveform, frequency: f32, amplitude: f32) -> Self {
|
||||
let phase_increment = frequency * TAU / SAMPLE_RATE;
|
||||
|
||||
Self {
|
||||
waveform,
|
||||
frequency,
|
||||
amplitude,
|
||||
phase: 0.0,
|
||||
phase_increment,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the frequency and update the phase increment
|
||||
pub fn set_frequency(&mut self, frequency: f32) {
|
||||
self.frequency = frequency;
|
||||
self.phase_increment = frequency * TAU / SAMPLE_RATE;
|
||||
}
|
||||
|
||||
/// Set the amplitude
|
||||
pub fn set_amplitude(&mut self, amplitude: f32) {
|
||||
self.amplitude = amplitude.clamp(0.0, 1.0);
|
||||
}
|
||||
|
||||
/// Generate the next sample
|
||||
pub fn next_sample(&mut self) -> f32 {
|
||||
let sample = match self.waveform {
|
||||
Waveform::Sine => self.sine_wave(),
|
||||
Waveform::Square => self.square_wave(),
|
||||
Waveform::Sawtooth => self.sawtooth_wave(),
|
||||
Waveform::Triangle => self.triangle_wave(),
|
||||
Waveform::Noise => self.noise_wave(),
|
||||
};
|
||||
|
||||
// Advance phase
|
||||
self.phase += self.phase_increment;
|
||||
if self.phase >= TAU {
|
||||
self.phase -= TAU;
|
||||
}
|
||||
|
||||
sample * self.amplitude
|
||||
}
|
||||
|
||||
/// Generate multiple samples into a buffer
|
||||
pub fn fill_buffer(&mut self, buffer: &mut [f32]) {
|
||||
for sample in buffer.iter_mut() {
|
||||
*sample = self.next_sample();
|
||||
}
|
||||
}
|
||||
|
||||
/// Reset the oscillator phase
|
||||
pub fn reset_phase(&mut self) {
|
||||
self.phase = 0.0;
|
||||
}
|
||||
|
||||
fn sine_wave(&self) -> f32 {
|
||||
self.phase.sin()
|
||||
}
|
||||
|
||||
fn square_wave(&self) -> f32 {
|
||||
if self.phase < PI { 1.0 } else { -1.0 }
|
||||
}
|
||||
|
||||
fn sawtooth_wave(&self) -> f32 {
|
||||
(self.phase / PI) - 1.0
|
||||
}
|
||||
|
||||
fn triangle_wave(&self) -> f32 {
|
||||
if self.phase < PI {
|
||||
(2.0 / PI) * self.phase - 1.0
|
||||
} else {
|
||||
3.0 - (2.0 / PI) * self.phase
|
||||
}
|
||||
}
|
||||
|
||||
fn noise_wave(&self) -> f32 {
|
||||
use rand::Rng;
|
||||
let mut rng = rand::thread_rng();
|
||||
rng.gen_range(-1.0..1.0)
|
||||
}
|
||||
}
|
||||
|
||||
/// An envelope generator for controlling amplitude over time
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct EnvelopeGenerator {
|
||||
pub attack_time: f32, // seconds
|
||||
pub decay_time: f32, // seconds
|
||||
pub sustain_level: f32, // 0.0 to 1.0
|
||||
pub release_time: f32, // seconds
|
||||
|
||||
state: EnvelopeState,
|
||||
current_level: f32,
|
||||
time_in_state: f32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
enum EnvelopeState {
|
||||
Idle,
|
||||
Attack,
|
||||
Decay,
|
||||
Sustain,
|
||||
Release,
|
||||
}
|
||||
|
||||
impl EnvelopeGenerator {
|
||||
/// Create a new ADSR envelope generator
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `attack` - Attack time in seconds
|
||||
/// * `decay` - Decay time in seconds
|
||||
/// * `sustain` - Sustain level (0.0 to 1.0)
|
||||
/// * `release` - Release time in seconds
|
||||
pub fn new(attack: f32, decay: f32, sustain: f32, release: f32) -> Self {
|
||||
Self {
|
||||
attack_time: attack,
|
||||
decay_time: decay,
|
||||
sustain_level: sustain.clamp(0.0, 1.0),
|
||||
release_time: release,
|
||||
state: EnvelopeState::Idle,
|
||||
current_level: 0.0,
|
||||
time_in_state: 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Trigger the envelope (start attack phase)
|
||||
pub fn trigger(&mut self) {
|
||||
self.state = EnvelopeState::Attack;
|
||||
self.time_in_state = 0.0;
|
||||
}
|
||||
|
||||
/// Release the envelope (start release phase)
|
||||
pub fn release(&mut self) {
|
||||
if self.state != EnvelopeState::Idle && self.state != EnvelopeState::Release {
|
||||
self.state = EnvelopeState::Release;
|
||||
self.time_in_state = 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the next envelope value
|
||||
pub fn next_sample(&mut self) -> f32 {
|
||||
let dt = 1.0 / SAMPLE_RATE;
|
||||
|
||||
match self.state {
|
||||
EnvelopeState::Idle => {
|
||||
self.current_level = 0.0;
|
||||
}
|
||||
EnvelopeState::Attack => {
|
||||
if self.attack_time > 0.0 {
|
||||
self.current_level = self.time_in_state / self.attack_time;
|
||||
if self.current_level >= 1.0 {
|
||||
self.current_level = 1.0;
|
||||
self.state = EnvelopeState::Decay;
|
||||
self.time_in_state = 0.0;
|
||||
}
|
||||
} else {
|
||||
self.current_level = 1.0;
|
||||
self.state = EnvelopeState::Decay;
|
||||
self.time_in_state = 0.0;
|
||||
}
|
||||
}
|
||||
EnvelopeState::Decay => {
|
||||
if self.decay_time > 0.0 {
|
||||
let decay_progress = self.time_in_state / self.decay_time;
|
||||
self.current_level = 1.0 - decay_progress * (1.0 - self.sustain_level);
|
||||
if decay_progress >= 1.0 {
|
||||
self.current_level = self.sustain_level;
|
||||
self.state = EnvelopeState::Sustain;
|
||||
self.time_in_state = 0.0;
|
||||
}
|
||||
} else {
|
||||
self.current_level = self.sustain_level;
|
||||
self.state = EnvelopeState::Sustain;
|
||||
self.time_in_state = 0.0;
|
||||
}
|
||||
}
|
||||
EnvelopeState::Sustain => {
|
||||
self.current_level = self.sustain_level;
|
||||
}
|
||||
EnvelopeState::Release => {
|
||||
if self.release_time > 0.0 {
|
||||
let release_start_level = if self.time_in_state == 0.0 {
|
||||
self.current_level
|
||||
} else {
|
||||
self.sustain_level
|
||||
};
|
||||
|
||||
let release_progress = self.time_in_state / self.release_time;
|
||||
self.current_level = release_start_level * (1.0 - release_progress);
|
||||
|
||||
if release_progress >= 1.0 {
|
||||
self.current_level = 0.0;
|
||||
self.state = EnvelopeState::Idle;
|
||||
self.time_in_state = 0.0;
|
||||
}
|
||||
} else {
|
||||
self.current_level = 0.0;
|
||||
self.state = EnvelopeState::Idle;
|
||||
self.time_in_state = 0.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.time_in_state += dt;
|
||||
self.current_level.clamp(0.0, 1.0)
|
||||
}
|
||||
|
||||
/// Check if the envelope is finished (in idle state)
|
||||
pub fn is_finished(&self) -> bool {
|
||||
self.state == EnvelopeState::Idle
|
||||
}
|
||||
}
|
||||
|
||||
/// A track combines an oscillator with an envelope for musical notes
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Track {
|
||||
pub oscillator: Oscillator,
|
||||
pub envelope: EnvelopeGenerator,
|
||||
pub is_active: bool,
|
||||
}
|
||||
|
||||
impl Track {
|
||||
/// Create a new track
|
||||
pub fn new(waveform: Waveform, frequency: f32, amplitude: f32) -> Self {
|
||||
Self {
|
||||
oscillator: Oscillator::new(waveform, frequency, amplitude),
|
||||
envelope: EnvelopeGenerator::new(0.01, 0.1, 0.7, 0.3), // Default ADSR
|
||||
is_active: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Trigger the track (start playing a note)
|
||||
pub fn trigger(&mut self, frequency: f32) {
|
||||
self.oscillator.set_frequency(frequency);
|
||||
self.oscillator.reset_phase();
|
||||
self.envelope.trigger();
|
||||
self.is_active = true;
|
||||
}
|
||||
|
||||
/// Release the track (stop playing)
|
||||
pub fn release(&mut self) {
|
||||
self.envelope.release();
|
||||
}
|
||||
|
||||
/// Generate the next sample
|
||||
pub fn next_sample(&mut self) -> f32 {
|
||||
if !self.is_active {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
let osc_sample = self.oscillator.next_sample();
|
||||
let env_level = self.envelope.next_sample();
|
||||
|
||||
if self.envelope.is_finished() {
|
||||
self.is_active = false;
|
||||
}
|
||||
|
||||
osc_sample * env_level
|
||||
}
|
||||
|
||||
/// Check if the track is currently active
|
||||
pub fn is_active(&self) -> bool {
|
||||
self.is_active && !self.envelope.is_finished()
|
||||
}
|
||||
}
|
||||
|
||||
/// A polyphonic synthesizer that manages multiple tracks
|
||||
#[derive(Debug)]
|
||||
pub struct PolySynth {
|
||||
pub tracks: Vec<Track>,
|
||||
pub max_tracks: usize,
|
||||
}
|
||||
|
||||
impl PolySynth {
|
||||
/// Create a new polyphonic synthesizer
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `max_tracks` - Maximum number of simultaneous tracks
|
||||
/// * `waveform` - Default waveform for all tracks
|
||||
pub fn new(max_tracks: usize, waveform: Waveform) -> Self {
|
||||
let mut tracks = Vec::with_capacity(max_tracks);
|
||||
for _ in 0..max_tracks {
|
||||
tracks.push(Track::new(waveform, 440.0, 0.5));
|
||||
}
|
||||
|
||||
Self { tracks, max_tracks }
|
||||
}
|
||||
|
||||
/// Play a note
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `frequency` - Frequency of the note in Hz
|
||||
/// Play a note on an available track
|
||||
pub fn play_note(&mut self, frequency: f32) {
|
||||
// Find an inactive track or steal the oldest one
|
||||
if let Some(track) = self.tracks.iter_mut().find(|t| !t.is_active()) {
|
||||
track.trigger(frequency);
|
||||
} else if let Some(track) = self.tracks.first_mut() {
|
||||
// Track stealing - use the first track
|
||||
track.trigger(frequency);
|
||||
}
|
||||
}
|
||||
|
||||
/// Stop a note
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `frequency` - Frequency of the note to stop
|
||||
/// Stop a note with the given frequency
|
||||
pub fn stop_note(&mut self, frequency: f32) {
|
||||
const FREQ_TOLERANCE: f32 = 1.0; // Hz tolerance for frequency matching
|
||||
|
||||
for track in &mut self.tracks {
|
||||
if track.is_active() && (track.oscillator.frequency - frequency).abs() < FREQ_TOLERANCE
|
||||
{
|
||||
track.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate the next sample (sum of all active tracks)
|
||||
pub fn next_sample(&mut self) -> f32 {
|
||||
let mut sample = 0.0;
|
||||
for track in &mut self.tracks {
|
||||
sample += track.next_sample();
|
||||
}
|
||||
|
||||
// Normalize by number of tracks to prevent clipping
|
||||
sample / self.max_tracks as f32
|
||||
}
|
||||
|
||||
/// Fill a buffer with samples
|
||||
pub fn fill_buffer(&mut self, buffer: &mut [f32]) {
|
||||
for sample in buffer.iter_mut() {
|
||||
*sample = self.next_sample();
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the number of currently active tracks
|
||||
pub fn active_track_count(&self) -> usize {
|
||||
self.tracks.iter().filter(|t| t.is_active()).count()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_oscillator_creation() {
|
||||
let osc = Oscillator::new(Waveform::Sine, 440.0, 0.5);
|
||||
assert_eq!(osc.frequency, 440.0);
|
||||
assert_eq!(osc.amplitude, 0.5);
|
||||
assert_eq!(osc.waveform, Waveform::Sine);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_oscillator_sample_generation() {
|
||||
let mut osc = Oscillator::new(Waveform::Sine, 440.0, 1.0);
|
||||
let sample = osc.next_sample();
|
||||
assert!(sample >= -1.0 && sample <= 1.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_envelope_generator() {
|
||||
let mut env = EnvelopeGenerator::new(0.1, 0.1, 0.5, 0.1);
|
||||
env.trigger();
|
||||
|
||||
// Should start at 0 and increase during attack
|
||||
let initial = env.next_sample();
|
||||
assert!(initial >= 0.0);
|
||||
|
||||
// Generate some samples to move through attack phase
|
||||
for _ in 0..1000 {
|
||||
env.next_sample();
|
||||
}
|
||||
|
||||
env.release();
|
||||
let release_sample = env.next_sample();
|
||||
assert!(release_sample >= 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_track_triggering() {
|
||||
let mut track = Track::new(Waveform::Sine, 440.0, 0.5);
|
||||
assert!(!track.is_active());
|
||||
|
||||
track.trigger(440.0);
|
||||
assert!(track.is_active());
|
||||
|
||||
track.release();
|
||||
// Track should still be active until envelope finishes
|
||||
assert!(track.is_active());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_polysynth() {
|
||||
let mut synth = PolySynth::new(4, Waveform::Sine);
|
||||
assert_eq!(synth.active_track_count(), 0);
|
||||
|
||||
synth.play_note(440.0);
|
||||
assert_eq!(synth.active_track_count(), 1);
|
||||
|
||||
synth.play_note(880.0);
|
||||
assert_eq!(synth.active_track_count(), 2);
|
||||
|
||||
// Allow envelope to ramp up by getting several samples
|
||||
let mut non_zero_found = false;
|
||||
for _ in 0..100 {
|
||||
let sample = synth.next_sample();
|
||||
if sample.abs() > 0.0 {
|
||||
non_zero_found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
assert!(
|
||||
non_zero_found,
|
||||
"Expected to find non-zero samples from active tracks"
|
||||
);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user