feat(takiy): takiy-core — teoría musical + modelo de partitura
Pitch MIDI (clase/octava/frecuencia ET A4=440), Scale (raíz + patrón de semitonos: mayor, menor natural, pentatónica), Chord (7 cualidades, voicing, nombres) y un Score multipista con tempo: ScoreNote en pulsos, Track con inserción ordenada y transposición atómica. 24 tests. Agnóstico de síntesis y UI, #![forbid(unsafe_code)]. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
//! Escalas — una raíz y un patrón de semitonos.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::pitch::{Pitch, PitchClass};
|
||||
|
||||
/// Una escala: clase raíz + offsets en semitonos desde la raíz.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Scale {
|
||||
root: PitchClass,
|
||||
/// Semitonos desde la raíz, ascendentes, dentro de una octava.
|
||||
intervals: Vec<u8>,
|
||||
}
|
||||
|
||||
impl Scale {
|
||||
/// Escala arbitraria desde su patrón de intervalos.
|
||||
pub fn new(root: PitchClass, intervals: Vec<u8>) -> Self {
|
||||
Self { root, intervals }
|
||||
}
|
||||
|
||||
/// Escala mayor (jónica): `T-T-S-T-T-T-S`.
|
||||
pub fn major(root: PitchClass) -> Self {
|
||||
Self::new(root, vec![0, 2, 4, 5, 7, 9, 11])
|
||||
}
|
||||
|
||||
/// Escala menor natural (eólica).
|
||||
pub fn natural_minor(root: PitchClass) -> Self {
|
||||
Self::new(root, vec![0, 2, 3, 5, 7, 8, 10])
|
||||
}
|
||||
|
||||
/// Escala pentatónica mayor.
|
||||
pub fn pentatonic_major(root: PitchClass) -> Self {
|
||||
Self::new(root, vec![0, 2, 4, 7, 9])
|
||||
}
|
||||
|
||||
/// Clase raíz.
|
||||
pub fn root(&self) -> PitchClass {
|
||||
self.root
|
||||
}
|
||||
|
||||
/// Cantidad de grados de la escala.
|
||||
pub fn degree_count(&self) -> usize {
|
||||
self.intervals.len()
|
||||
}
|
||||
|
||||
/// Clase de altura del grado `degree` (0-indexado, módulo el largo).
|
||||
pub fn degree(&self, degree: usize) -> PitchClass {
|
||||
let iv = self.intervals[degree % self.intervals.len()];
|
||||
PitchClass::from_semitone(self.root.semitone() + iv)
|
||||
}
|
||||
|
||||
/// `true` si la clase de `pitch` pertenece a la escala.
|
||||
pub fn contains(&self, pitch: Pitch) -> bool {
|
||||
let rel = (pitch.class().semitone() + 12 - self.root.semitone()) % 12;
|
||||
self.intervals.contains(&rel)
|
||||
}
|
||||
|
||||
/// Las alturas de la escala en la `octave` dada, un grado por
|
||||
/// elemento. Las que se salgan del rango MIDI se omiten.
|
||||
pub fn pitches_in_octave(&self, octave: i32) -> Vec<Pitch> {
|
||||
self.intervals
|
||||
.iter()
|
||||
.filter_map(|&iv| {
|
||||
Pitch::from_class_octave(self.root, octave)
|
||||
.and_then(|p| p.transpose(iv as i32))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn c_major_has_no_accidentals() {
|
||||
let s = Scale::major(PitchClass::C);
|
||||
for pc in [
|
||||
PitchClass::C,
|
||||
PitchClass::D,
|
||||
PitchClass::E,
|
||||
PitchClass::F,
|
||||
PitchClass::G,
|
||||
PitchClass::A,
|
||||
PitchClass::B,
|
||||
] {
|
||||
assert!(s.contains(Pitch::from_class_octave(pc, 4).unwrap()));
|
||||
}
|
||||
// F# no está en do mayor.
|
||||
assert!(!s.contains(Pitch::from_class_octave(PitchClass::Fs, 4).unwrap()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn degrees_of_a_minor() {
|
||||
let s = Scale::natural_minor(PitchClass::A);
|
||||
assert_eq!(s.degree(0), PitchClass::A);
|
||||
assert_eq!(s.degree(2), PitchClass::C);
|
||||
// El grado envuelve.
|
||||
assert_eq!(s.degree(7), PitchClass::A);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pitches_in_octave_count_matches_degrees() {
|
||||
let s = Scale::major(PitchClass::G);
|
||||
assert_eq!(s.pitches_in_octave(4).len(), 7);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pentatonic_has_five_degrees() {
|
||||
assert_eq!(Scale::pentatonic_major(PitchClass::D).degree_count(), 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn contains_is_octave_agnostic() {
|
||||
let s = Scale::major(PitchClass::C);
|
||||
assert!(s.contains(Pitch::from_class_octave(PitchClass::E, 2).unwrap()));
|
||||
assert!(s.contains(Pitch::from_class_octave(PitchClass::E, 7).unwrap()));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user