Initial commit
This commit is contained in:
476
src/scales.rs
Normal file
476
src/scales.rs
Normal file
@ -0,0 +1,476 @@
|
||||
//! Musical scales and chord generation module
|
||||
//!
|
||||
//! This module provides definitions for various musical scales, chord progressions,
|
||||
//! and utilities for generating harmonically pleasing note sequences.
|
||||
|
||||
use crate::midi_to_frequency;
|
||||
use rand::Rng;
|
||||
|
||||
/// Musical scale types
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum ScaleType {
|
||||
Major,
|
||||
Minor,
|
||||
Dorian,
|
||||
Phrygian,
|
||||
Lydian,
|
||||
Mixolydian,
|
||||
Aeolian,
|
||||
Locrian,
|
||||
Pentatonic,
|
||||
Blues,
|
||||
Chromatic,
|
||||
}
|
||||
|
||||
/// A musical scale containing note intervals
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Scale {
|
||||
pub scale_type: ScaleType,
|
||||
pub root_note: u8, // MIDI note number
|
||||
pub intervals: Vec<u8>, // Semitone intervals from root
|
||||
}
|
||||
|
||||
impl Scale {
|
||||
/// Create a new scale
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `scale_type` - Type of scale
|
||||
/// * `root_note` - Root note as MIDI number (0-127)
|
||||
pub fn new(scale_type: ScaleType, root_note: u8) -> Self {
|
||||
let intervals = match scale_type {
|
||||
ScaleType::Major => vec![0, 2, 4, 5, 7, 9, 11],
|
||||
ScaleType::Minor => vec![0, 2, 3, 5, 7, 8, 10],
|
||||
ScaleType::Dorian => vec![0, 2, 3, 5, 7, 9, 10],
|
||||
ScaleType::Phrygian => vec![0, 1, 3, 5, 7, 8, 10],
|
||||
ScaleType::Lydian => vec![0, 2, 4, 6, 7, 9, 11],
|
||||
ScaleType::Mixolydian => vec![0, 2, 4, 5, 7, 9, 10],
|
||||
ScaleType::Aeolian => vec![0, 2, 3, 5, 7, 8, 10], // Same as minor
|
||||
ScaleType::Locrian => vec![0, 1, 3, 5, 6, 8, 10],
|
||||
ScaleType::Pentatonic => vec![0, 2, 4, 7, 9],
|
||||
ScaleType::Blues => vec![0, 3, 5, 6, 7, 10],
|
||||
ScaleType::Chromatic => vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
|
||||
};
|
||||
|
||||
Self {
|
||||
scale_type,
|
||||
root_note,
|
||||
intervals,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get all notes in the scale within a given octave range
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `octave_range` - Number of octaves to span (default 1)
|
||||
///
|
||||
/// # Returns
|
||||
/// Vector of MIDI note numbers in the scale
|
||||
pub fn get_notes(&self, octave_range: u8) -> Vec<u8> {
|
||||
let mut notes = Vec::new();
|
||||
|
||||
for octave in 0..octave_range {
|
||||
for &interval in &self.intervals {
|
||||
let note = self.root_note + interval + (octave * 12);
|
||||
if note <= 127 {
|
||||
notes.push(note);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
notes
|
||||
}
|
||||
|
||||
/// Get notes in the scale as frequencies
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `octave_range` - Number of octaves to span
|
||||
///
|
||||
/// # Returns
|
||||
/// Vector of frequencies in Hz
|
||||
pub fn get_frequencies(&self, octave_range: u8) -> Vec<f32> {
|
||||
self.get_notes(octave_range)
|
||||
.iter()
|
||||
.map(|¬e| midi_to_frequency(note))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Get a specific degree of the scale
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `degree` - Scale degree (1-based, 1 = root)
|
||||
/// * `octave` - Octave number (0-based)
|
||||
///
|
||||
/// # Returns
|
||||
/// MIDI note number, or None if degree is invalid
|
||||
pub fn get_degree(&self, degree: usize, octave: u8) -> Option<u8> {
|
||||
if degree == 0 || degree > self.intervals.len() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let interval = self.intervals[degree - 1];
|
||||
let note = self.root_note + interval + (octave * 12);
|
||||
|
||||
if note <= 127 { Some(note) } else { None }
|
||||
}
|
||||
|
||||
/// Get a random note from the scale
|
||||
pub fn random_note(&self, octave_range: u8) -> u8 {
|
||||
let notes = self.get_notes(octave_range);
|
||||
let mut rng = rand::thread_rng();
|
||||
notes[rng.gen_range(0..notes.len())]
|
||||
}
|
||||
|
||||
/// Check if a MIDI note is in this scale
|
||||
pub fn contains_note(&self, note: u8) -> bool {
|
||||
let note_in_octave = note % 12;
|
||||
let root_in_octave = self.root_note % 12;
|
||||
|
||||
self.intervals
|
||||
.iter()
|
||||
.any(|&interval| (root_in_octave + interval) % 12 == note_in_octave)
|
||||
}
|
||||
}
|
||||
|
||||
/// Chord types
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum ChordType {
|
||||
Major,
|
||||
Minor,
|
||||
Diminished,
|
||||
Augmented,
|
||||
Sus2,
|
||||
Sus4,
|
||||
Major7,
|
||||
Minor7,
|
||||
Dominant7,
|
||||
Major9,
|
||||
Minor9,
|
||||
}
|
||||
|
||||
/// A musical chord
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Chord {
|
||||
pub chord_type: ChordType,
|
||||
pub root_note: u8,
|
||||
pub notes: Vec<u8>,
|
||||
}
|
||||
|
||||
impl Chord {
|
||||
/// Create a new chord
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `chord_type` - Type of chord
|
||||
/// * `root_note` - Root note as MIDI number
|
||||
pub fn new(chord_type: ChordType, root_note: u8) -> Self {
|
||||
let intervals = match chord_type {
|
||||
ChordType::Major => vec![0, 4, 7],
|
||||
ChordType::Minor => vec![0, 3, 7],
|
||||
ChordType::Diminished => vec![0, 3, 6],
|
||||
ChordType::Augmented => vec![0, 4, 8],
|
||||
ChordType::Sus2 => vec![0, 2, 7],
|
||||
ChordType::Sus4 => vec![0, 5, 7],
|
||||
ChordType::Major7 => vec![0, 4, 7, 11],
|
||||
ChordType::Minor7 => vec![0, 3, 7, 10],
|
||||
ChordType::Dominant7 => vec![0, 4, 7, 10],
|
||||
ChordType::Major9 => vec![0, 4, 7, 11, 14],
|
||||
ChordType::Minor9 => vec![0, 3, 7, 10, 14],
|
||||
};
|
||||
|
||||
let notes = intervals
|
||||
.iter()
|
||||
.map(|&interval| root_note + interval)
|
||||
.filter(|¬e| note <= 127)
|
||||
.collect();
|
||||
|
||||
Self {
|
||||
chord_type,
|
||||
root_note,
|
||||
notes,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get chord notes as frequencies
|
||||
pub fn get_frequencies(&self) -> Vec<f32> {
|
||||
self.notes
|
||||
.iter()
|
||||
.map(|¬e| midi_to_frequency(note))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Get an inversion of the chord
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `inversion` - Inversion number (0 = root position, 1 = first inversion, etc.)
|
||||
pub fn get_inversion(&self, inversion: usize) -> Vec<u8> {
|
||||
if inversion == 0 || inversion >= self.notes.len() {
|
||||
return self.notes.clone();
|
||||
}
|
||||
|
||||
let mut inverted = self.notes.clone();
|
||||
|
||||
// Move the bottom notes up an octave
|
||||
for i in 0..inversion {
|
||||
if inverted[i] + 12 <= 127 {
|
||||
inverted[i] += 12;
|
||||
}
|
||||
}
|
||||
|
||||
// Sort the notes
|
||||
inverted.sort();
|
||||
inverted
|
||||
}
|
||||
}
|
||||
|
||||
/// Chord progression patterns
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ChordProgression {
|
||||
pub scale: Scale,
|
||||
pub progression: Vec<usize>, // Scale degrees (1-based)
|
||||
}
|
||||
|
||||
impl ChordProgression {
|
||||
/// Create a new chord progression
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `scale` - The scale to build chords from
|
||||
/// * `progression` - Vector of scale degrees (1-based)
|
||||
pub fn new(scale: Scale, progression: Vec<usize>) -> Self {
|
||||
Self { scale, progression }
|
||||
}
|
||||
|
||||
/// Get common chord progressions
|
||||
pub fn common_progressions() -> Vec<Vec<usize>> {
|
||||
vec![
|
||||
vec![1, 4, 5, 1], // I-IV-V-I
|
||||
vec![1, 5, 6, 4], // I-V-vi-IV (pop progression)
|
||||
vec![6, 4, 1, 5], // vi-IV-I-V
|
||||
vec![1, 6, 4, 5], // I-vi-IV-V (50s progression)
|
||||
vec![2, 5, 1], // ii-V-I (jazz)
|
||||
vec![1, 7, 4, 1], // I-VII-IV-I
|
||||
vec![1, 3, 4, 1], // I-iii-IV-I
|
||||
]
|
||||
}
|
||||
|
||||
/// Generate chords for the progression
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `octave` - Base octave for the chords
|
||||
///
|
||||
/// # Returns
|
||||
/// Vector of chords
|
||||
pub fn generate_chords(&self, octave: u8) -> Vec<Chord> {
|
||||
self.progression
|
||||
.iter()
|
||||
.filter_map(|°ree| {
|
||||
self.scale.get_degree(degree, octave).map(|root_note| {
|
||||
// Determine chord type based on scale degree
|
||||
let chord_type = match self.scale.scale_type {
|
||||
ScaleType::Major => match degree {
|
||||
1 | 4 | 5 => ChordType::Major,
|
||||
2 | 3 | 6 => ChordType::Minor,
|
||||
7 => ChordType::Diminished,
|
||||
_ => ChordType::Major,
|
||||
},
|
||||
ScaleType::Minor => match degree {
|
||||
1 | 4 | 5 => ChordType::Minor,
|
||||
3 | 6 | 7 => ChordType::Major,
|
||||
2 => ChordType::Diminished,
|
||||
_ => ChordType::Minor,
|
||||
},
|
||||
_ => ChordType::Major, // Default for other scales
|
||||
};
|
||||
|
||||
Chord::new(chord_type, root_note)
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
/// Melody generator using scales
|
||||
pub struct MelodyGenerator {
|
||||
pub scale: Scale,
|
||||
pub octave_range: u8,
|
||||
available_notes: Vec<u8>,
|
||||
}
|
||||
|
||||
impl MelodyGenerator {
|
||||
/// Create a new melody generator
|
||||
pub fn new(scale: Scale, octave_range: u8) -> Self {
|
||||
let available_notes = scale.get_notes(octave_range);
|
||||
Self {
|
||||
scale,
|
||||
octave_range,
|
||||
available_notes,
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate a random melody
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `length` - Number of notes in the melody
|
||||
///
|
||||
/// # Returns
|
||||
/// Vector of MIDI note numbers
|
||||
pub fn generate_random_melody(&self, length: usize) -> Vec<u8> {
|
||||
let mut rng = rand::thread_rng();
|
||||
(0..length)
|
||||
.map(|_| self.available_notes[rng.gen_range(0..self.available_notes.len())])
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Generate a melody using step-wise motion
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `length` - Number of notes in the melody
|
||||
/// * `step_probability` - Probability of moving by step vs. leap (0.0 to 1.0)
|
||||
///
|
||||
/// # Returns
|
||||
/// Vector of MIDI note numbers
|
||||
pub fn generate_stepwise_melody(&self, length: usize, step_probability: f32) -> Vec<u8> {
|
||||
if self.available_notes.is_empty() || length == 0 {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let mut rng = rand::thread_rng();
|
||||
let mut melody = Vec::with_capacity(length);
|
||||
|
||||
// Start with a random note
|
||||
let mut current_index = rng.gen_range(0..self.available_notes.len());
|
||||
melody.push(self.available_notes[current_index]);
|
||||
|
||||
for _ in 1..length {
|
||||
if rng.r#gen::<f32>() < step_probability {
|
||||
// Step-wise motion (move to adjacent note in scale)
|
||||
let direction = if rng.r#gen::<bool>() { 1 } else { -1 };
|
||||
let new_index = (current_index as i32 + direction)
|
||||
.max(0)
|
||||
.min(self.available_notes.len() as i32 - 1)
|
||||
as usize;
|
||||
current_index = new_index;
|
||||
} else {
|
||||
// Leap (random note)
|
||||
current_index = rng.gen_range(0..self.available_notes.len());
|
||||
}
|
||||
|
||||
melody.push(self.available_notes[current_index]);
|
||||
}
|
||||
|
||||
melody
|
||||
}
|
||||
|
||||
/// Generate an arpeggio pattern
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `chord` - Chord to arpeggiate
|
||||
/// * `pattern_length` - Length of the arpeggio pattern
|
||||
///
|
||||
/// # Returns
|
||||
/// Vector of MIDI note numbers
|
||||
pub fn generate_arpeggio(&self, chord: &Chord, pattern_length: usize) -> Vec<u8> {
|
||||
if chord.notes.is_empty() || pattern_length == 0 {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let mut arpeggio = Vec::with_capacity(pattern_length);
|
||||
let chord_notes = &chord.notes;
|
||||
|
||||
for i in 0..pattern_length {
|
||||
let note_index = i % chord_notes.len();
|
||||
arpeggio.push(chord_notes[note_index]);
|
||||
}
|
||||
|
||||
arpeggio
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_scale_creation() {
|
||||
let scale = Scale::new(ScaleType::Major, 60); // C major
|
||||
assert_eq!(scale.intervals, vec![0, 2, 4, 5, 7, 9, 11]);
|
||||
assert_eq!(scale.root_note, 60);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_scale_notes() {
|
||||
let scale = Scale::new(ScaleType::Major, 60); // C major
|
||||
let notes = scale.get_notes(1);
|
||||
assert_eq!(notes, vec![60, 62, 64, 65, 67, 69, 71]); // C D E F G A B
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_scale_degree() {
|
||||
let scale = Scale::new(ScaleType::Major, 60); // C major
|
||||
assert_eq!(scale.get_degree(1, 0), Some(60)); // C
|
||||
assert_eq!(scale.get_degree(5, 0), Some(67)); // G
|
||||
assert_eq!(scale.get_degree(8, 0), None); // Invalid degree
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_chord_creation() {
|
||||
let chord = Chord::new(ChordType::Major, 60); // C major
|
||||
assert_eq!(chord.notes, vec![60, 64, 67]); // C E G
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_chord_inversion() {
|
||||
let chord = Chord::new(ChordType::Major, 60); // C major
|
||||
let first_inversion = chord.get_inversion(1);
|
||||
assert_eq!(first_inversion, vec![64, 67, 72]); // E G C
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_chord_progression() {
|
||||
let scale = Scale::new(ScaleType::Major, 60);
|
||||
let progression = ChordProgression::new(scale, vec![1, 4, 5, 1]);
|
||||
let chords = progression.generate_chords(4);
|
||||
assert_eq!(chords.len(), 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_melody_generator() {
|
||||
let scale = Scale::new(ScaleType::Pentatonic, 60);
|
||||
let generator = MelodyGenerator::new(scale, 2);
|
||||
let melody = generator.generate_random_melody(8);
|
||||
assert_eq!(melody.len(), 8);
|
||||
|
||||
// All notes should be in the scale
|
||||
for ¬e in &melody {
|
||||
assert!(generator.available_notes.contains(¬e));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_stepwise_melody() {
|
||||
let scale = Scale::new(ScaleType::Major, 60);
|
||||
let generator = MelodyGenerator::new(scale, 1);
|
||||
let melody = generator.generate_stepwise_melody(5, 1.0); // 100% stepwise
|
||||
assert_eq!(melody.len(), 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_arpeggio_generation() {
|
||||
let scale = Scale::new(ScaleType::Major, 60);
|
||||
let generator = MelodyGenerator::new(scale, 1);
|
||||
let chord = Chord::new(ChordType::Major, 60);
|
||||
let arpeggio = generator.generate_arpeggio(&chord, 6);
|
||||
|
||||
assert_eq!(arpeggio.len(), 6);
|
||||
// Should cycle through chord notes
|
||||
assert_eq!(arpeggio[0], chord.notes[0]);
|
||||
assert_eq!(arpeggio[3], chord.notes[0]); // After one full cycle
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_note_containment() {
|
||||
let scale = Scale::new(ScaleType::Major, 60); // C major
|
||||
assert!(scale.contains_note(60)); // C
|
||||
assert!(scale.contains_note(64)); // E
|
||||
assert!(!scale.contains_note(61)); // C#
|
||||
assert!(!scale.contains_note(63)); // D#
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user