#[derive(Debug, Clone, PartialEq)]
pub struct Note {
pub pitch_hz: f64,
pub duration_beats: f64,
pub velocity: u8,
}
impl Note {
pub fn new(pitch_hz: f64, duration_beats: f64, velocity: u8) -> Self {
Self {
pitch_hz,
duration_beats,
velocity: velocity.min(127),
}
}
}
struct Lcg {
state: u64,
}
impl Lcg {
fn new(seed: u64) -> Self {
Self { state: seed.wrapping_add(1) }
}
fn next_f64(&mut self) -> f64 {
self.state = self.state.wrapping_add(0x9e3779b97f4a7c15);
let mut z = self.state;
z = (z ^ (z >> 30)).wrapping_mul(0xbf58476d1ce4e5b9);
z = (z ^ (z >> 27)).wrapping_mul(0x94d049bb133111eb);
z ^= z >> 31;
(z as f64) / (u64::MAX as f64)
}
#[allow(dead_code)]
fn next_usize(&mut self, n: usize) -> usize {
(self.next_f64() * n as f64) as usize % n
}
#[allow(dead_code)]
fn next_range(&mut self, lo: i32, hi: i32) -> i32 {
let range = (hi - lo + 1) as usize;
lo + self.next_usize(range) as i32
}
}
pub struct RandomWalkComposer;
impl RandomWalkComposer {
pub fn compose(
steps: usize,
root_hz: f64,
scale_intervals: &[u8],
seed: u64,
) -> Vec<Note> {
if scale_intervals.is_empty() || steps == 0 {
return Vec::new();
}
let mut rng = Lcg::new(seed);
let scale_len = scale_intervals.len() as i32;
let step_choices: [(i32, u32); 5] = [(-2, 1), (-1, 3), (0, 2), (1, 3), (2, 1)];
let total_weight: u32 = step_choices.iter().map(|(_, w)| w).sum();
let mut degree: i32 = 0; let mut notes = Vec::with_capacity(steps);
for _ in 0..steps {
let hz = degree_to_hz(degree, root_hz, scale_intervals);
notes.push(Note::new(hz, 1.0, 80));
let roll = (rng.next_f64() * total_weight as f64) as u32;
let mut cumulative = 0u32;
let mut step = 0i32;
for (s, w) in &step_choices {
cumulative += w;
if roll < cumulative {
step = *s;
break;
}
}
degree += step;
if degree > 2 * scale_len {
degree -= scale_len;
} else if degree < -2 * scale_len {
degree += scale_len;
}
}
notes
}
pub fn with_rhythm(mut notes: Vec<Note>, pattern: &[f64], seed: u64) -> Vec<Note> {
if pattern.is_empty() {
return notes;
}
let mut rng = Lcg::new(seed);
let _ = rng.next_f64(); for (i, note) in notes.iter_mut().enumerate() {
note.duration_beats = pattern[i % pattern.len()];
}
notes
}
pub fn transpose(notes: &[Note], semitones: i8) -> Vec<Note> {
let ratio = 2.0_f64.powf(semitones as f64 / 12.0);
notes
.iter()
.map(|n| Note::new(n.pitch_hz * ratio, n.duration_beats, n.velocity))
.collect()
}
}
fn degree_to_hz(degree: i32, root_hz: f64, scale_intervals: &[u8]) -> f64 {
let n = scale_intervals.len() as i32;
let total_semitones: i32 = if degree >= 0 {
let octave = degree / n;
let idx = (degree % n) as usize;
let semi: i32 = scale_intervals[..idx].iter().map(|&s| s as i32).sum();
octave * 12 + semi
} else {
let abs_deg = (-degree) as usize;
let octave = (abs_deg as i32 - 1) / n + 1;
let idx = n as usize - (abs_deg % n as usize);
let idx = if idx == n as usize { 0 } else { idx };
let semi: i32 = scale_intervals[idx..].iter().map(|&s| s as i32).sum();
-(octave * 12 - (12 - semi))
};
root_hz * 2.0_f64.powf(total_semitones as f64 / 12.0)
}
pub struct BrownianMotionComposer;
impl BrownianMotionComposer {
pub fn compose_pitch_sequence(
steps: usize,
start_hz: f64,
volatility: f64,
seed: u64,
) -> Vec<f64> {
if steps == 0 {
return Vec::new();
}
let mut rng = Lcg::new(seed);
let log_min = 80.0_f64.ln();
let log_max = 4000.0_f64.ln();
let start_hz = start_hz.clamp(80.0, 4000.0);
let mut log_pitch = start_hz.ln();
let mut pitches = Vec::with_capacity(steps);
pitches.push(start_hz);
for _ in 1..steps {
let u1 = rng.next_f64().max(1e-15);
let u2 = rng.next_f64();
let normal = (-2.0 * u1.ln()).sqrt() * (2.0 * std::f64::consts::PI * u2).cos();
log_pitch += volatility * normal;
log_pitch = log_pitch.clamp(log_min, log_max);
pitches.push(log_pitch.exp());
}
pitches
}
pub fn to_notes(pitches: &[f64], base_duration: f64, velocity: u8) -> Vec<Note> {
pitches
.iter()
.map(|&hz| Note::new(hz, base_duration, velocity))
.collect()
}
}
#[derive(Debug, Clone)]
pub struct StochasticPhrase {
pub notes: Vec<Note>,
pub tempo_bpm: f64,
}
impl StochasticPhrase {
pub fn new(notes: Vec<Note>, tempo_bpm: f64) -> Self {
Self { notes, tempo_bpm }
}
pub fn duration_ms(&self) -> f64 {
let total_beats: f64 = self.notes.iter().map(|n| n.duration_beats).sum();
total_beats * 60_000.0 / self.tempo_bpm
}
}
#[cfg(test)]
mod tests {
use super::*;
const MAJOR: &[u8] = &[2, 2, 1, 2, 2, 2, 1];
#[test]
fn test_compose_length() {
let notes = RandomWalkComposer::compose(16, 440.0, MAJOR, 42);
assert_eq!(notes.len(), 16);
}
#[test]
fn test_compose_positive_hz() {
let notes = RandomWalkComposer::compose(32, 261.63, MAJOR, 7);
for n in ¬es {
assert!(n.pitch_hz > 0.0, "pitch must be positive, got {}", n.pitch_hz);
}
}
#[test]
fn test_compose_velocity_clamped() {
let notes = RandomWalkComposer::compose(8, 440.0, MAJOR, 1);
for n in ¬es {
assert!(n.velocity <= 127);
}
}
#[test]
fn test_with_rhythm() {
let notes = RandomWalkComposer::compose(6, 440.0, MAJOR, 1);
let pattern = vec![0.5, 0.25, 1.0];
let rhythmic = RandomWalkComposer::with_rhythm(notes, &pattern, 0);
assert_eq!(rhythmic[0].duration_beats, 0.5);
assert_eq!(rhythmic[1].duration_beats, 0.25);
assert_eq!(rhythmic[2].duration_beats, 1.0);
assert_eq!(rhythmic[3].duration_beats, 0.5);
}
#[test]
fn test_transpose_up() {
let notes = vec![Note::new(440.0, 1.0, 80)];
let transposed = RandomWalkComposer::transpose(¬es, 12);
let expected = 440.0 * 2.0;
assert!((transposed[0].pitch_hz - expected).abs() < 0.01);
}
#[test]
fn test_transpose_down() {
let notes = vec![Note::new(440.0, 1.0, 80)];
let transposed = RandomWalkComposer::transpose(¬es, -12);
let expected = 220.0;
assert!((transposed[0].pitch_hz - expected).abs() < 0.01);
}
#[test]
fn test_brownian_length() {
let pitches = BrownianMotionComposer::compose_pitch_sequence(20, 440.0, 0.1, 99);
assert_eq!(pitches.len(), 20);
}
#[test]
fn test_brownian_clamp() {
let pitches = BrownianMotionComposer::compose_pitch_sequence(100, 440.0, 10.0, 5);
for &p in &pitches {
assert!(p >= 79.9 && p <= 4001.0, "pitch {} out of range", p);
}
}
#[test]
fn test_brownian_to_notes() {
let pitches = vec![220.0, 330.0, 440.0];
let notes = BrownianMotionComposer::to_notes(&pitches, 0.5, 64);
assert_eq!(notes.len(), 3);
assert_eq!(notes[0].pitch_hz, 220.0);
assert_eq!(notes[0].duration_beats, 0.5);
assert_eq!(notes[0].velocity, 64);
}
#[test]
fn test_stochastic_phrase_duration() {
let notes = vec![
Note::new(440.0, 1.0, 80),
Note::new(550.0, 2.0, 80),
];
let phrase = StochasticPhrase::new(notes, 120.0);
assert!((phrase.duration_ms() - 1500.0).abs() < 0.01);
}
#[test]
fn test_stochastic_phrase_empty() {
let phrase = StochasticPhrase::new(vec![], 120.0);
assert_eq!(phrase.duration_ms(), 0.0);
}
#[test]
fn test_compose_empty_scale() {
let notes = RandomWalkComposer::compose(4, 440.0, &[], 1);
assert!(notes.is_empty());
}
#[test]
fn test_compose_zero_steps() {
let notes = RandomWalkComposer::compose(0, 440.0, MAJOR, 1);
assert!(notes.is_empty());
}
#[test]
fn test_deterministic_seed() {
let a = RandomWalkComposer::compose(10, 440.0, MAJOR, 12345);
let b = RandomWalkComposer::compose(10, 440.0, MAJOR, 12345);
assert_eq!(a.len(), b.len());
for (na, nb) in a.iter().zip(b.iter()) {
assert!((na.pitch_hz - nb.pitch_hz).abs() < 1e-9);
}
}
}