Initial commit

This commit is contained in:
2025-06-20 18:52:55 -06:00
commit 9e3bf41d61
18 changed files with 8429 additions and 0 deletions

593
src/audio.rs Normal file
View 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
View 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('♯') {
(&note_part[0..1], 1)
} else if note_part.ends_with('b') || note_part.ends_with('♭') || note_part.ends_with('B') {
(&note_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
View 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('♯') {
(&note_part[0..1], 1)
} else if note_part.ends_with('b') || note_part.ends_with('♭') || note_part.ends_with('B') {
(&note_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
View 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
View 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
View 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
View 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
View 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 &degree in &degrees {
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
View 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(|&note| 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(|&note| 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(|&note| 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(|&degree| {
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 &note in &melody {
assert!(generator.available_notes.contains(&note));
}
}
#[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
View 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 &note 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 &note 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 &note 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
View 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"
);
}
}