Scale Generator
1. Readme
音阶生成器
译:真不知道说什么。
给出一个音调,或是开始的音符以及一组间隔,从音调开始并遵循指定的间隔模式生成音阶。
西方音乐中的音阶,基于彩色(12 音符)音阶。该音阶可表示为以下一组音高:
A,A#,B,C,C#,D,D#,E,F,F#,G,G#
给出一个尖音符(用#表示)也可以表示为它前面的平音符(用 b 表示),所以半音音阶也可以这样写:
A,Bb,B,C,Db,D,Eb,E,F,Gb,G,Ab
大调和小调的音阶和模式是这个十二音高集合的子集。它们有七个音高,称为全音阶音阶。这些音阶中的音符集合使用锐利或平面,根据音调。这是一个列表,其中包括:
没有尖(Sharps)或平(Flats):C 大调(major)和 a 小调(minor)
使用 Sharps: G, D, A, E, B, F# major e, b, f#, c#, g#, d# minor
使用 Flats: F, Bb, Eb, Ab, Db, Gb major d, g, c, f, bb, eb minor
全音阶音阶以及源自半音音阶的所有其他音阶都是间隔建立的。间隔是两个音高之间的间距。
最简单的间隔是在两个相邻音符之间,称为”半步”或”小秒”(有时写为小写”m”)。具有中间音符的两个音符之间的间隔称为”整步”或”大秒”(写为大写”M”)。仅使用相邻音符之间的这两个间隔,来构建全音阶音阶。
非全音阶音阶可以包含其他间隔。一个”先增强”的间隔,写上 A,具有两个中间音符(例如,从 A 到 C 或从 D 到 E)。间隔也越来越小,但他们不会参与这个练习。
2. 开始你的表演
// You should change this. // // Depending on your implementation, there are a variety of potential errors // which might occur. They aren't checked by the test suite in order to // allow the greatest freedom of implementation, but real libraries should // provide useful, descriptive errors so that downstream code can react // appropriately. // // One common idiom is to define an Error enum which wraps all potential // errors. Another common idiom is to use a helper type such as failure::Error // which does more or less the same thing but automatically. pub type Error = (); pub struct Scale; impl Scale { pub fn new(tonic: &str, intervals: &str) -> Result<Scale, Error> { unimplemented!( "Construct a new scale with tonic {} and intervals {}", tonic, intervals ) } pub fn chromatic(tonic: &str) -> Result<Scale, Error> { unimplemented!("Construct a new chromatic scale with tonic {}", tonic) } pub fn enumerate(&self) -> Vec<String> { unimplemented!() } }
3. 测试代码查看
# #![allow(unused_variables)] #fn main() { /// Tests for scale-generator /// /// Generated by [script][script] using [canonical data][canonical-data] /// /// [script]: https://github.com/exercism/rust/blob/master/bin/init_exercise.py /// [canonical-data]: https://raw.githubusercontent.com/exercism/problem-specifications/master/exercises/scale-generator/canonical_data.json /// Process a single test case for the property `chromatic` /// /// All cases for the `chromatic` property are implemented /// in terms of this function. fn process_chromatic_case(tonic: &str, expected: &[&str]) { let s = Scale::chromatic(tonic).unwrap(); assert_eq!(s.enumerate(), expected); } /// Process a single test case for the property `interval` /// /// All cases for the `interval` property are implemented /// in terms of this function. fn process_interval_case(tonic: &str, intervals: &str, expected: &[&str]) { let s = Scale::new(tonic, intervals).unwrap(); assert_eq!(s.enumerate(), expected); } // Chromatic scales // These tests have no interval. // The chromatic scale is considered the default scale #[test] /// Chromatic scale with sharps fn test_chromatic_scale_with_sharps() { process_chromatic_case( "C", &[ "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B", ], ); } #[test] //#[ignore] /// Chromatic scale with flats fn test_chromatic_scale_with_flats() { process_chromatic_case( "F", &[ "F", "Gb", "G", "Ab", "A", "Bb", "B", "C", "Db", "D", "Eb", "E", ], ); } // Scales with specified intervals // These tests all have intervals and are explorations of different // traversals of the scale. #[test] //#[ignore] /// Simple major scale /// /// The simplest major scale, with no sharps or flats. fn test_simple_major_scale() { process_interval_case("C", "MMmMMMm", &["C", "D", "E", "F", "G", "A", "B"]); } #[test] //#[ignore] /// Major scale with sharps fn test_major_scale_with_sharps() { process_interval_case("G", "MMmMMMm", &["G", "A", "B", "C", "D", "E", "F#"]); } #[test] //#[ignore] /// Major scale with flats fn test_major_scale_with_flats() { process_interval_case("F", "MMmMMMm", &["F", "G", "A", "Bb", "C", "D", "E"]); } #[test] //#[ignore] /// Minor scale with sharps fn test_minor_scale_with_sharps() { process_interval_case("f#", "MmMMmMM", &["F#", "G#", "A", "B", "C#", "D", "E"]); } #[test] //#[ignore] /// Minor scale with flats fn test_minor_scale_with_flats() { process_interval_case("bb", "MmMMmMM", &["Bb", "C", "Db", "Eb", "F", "Gb", "Ab"]); } #[test] //#[ignore] /// Dorian mode fn test_dorian_mode() { process_interval_case("d", "MmMMMmM", &["D", "E", "F", "G", "A", "B", "C"]); } #[test] //#[ignore] /// Mixolydian mode fn test_mixolydian_mode() { process_interval_case("Eb", "MMmMMmM", &["Eb", "F", "G", "Ab", "Bb", "C", "Db"]); } #[test] //#[ignore] /// Lydian mode fn test_lydian_mode() { process_interval_case("a", "MMMmMMm", &["A", "B", "C#", "D#", "E", "F#", "G#"]); } #[test] //#[ignore] /// Phrygian mode fn test_phrygian_mode() { process_interval_case("e", "mMMMmMM", &["E", "F", "G", "A", "B", "C", "D"]); } #[test] //#[ignore] /// Locrian mode fn test_locrian_mode() { process_interval_case("g", "mMMmMMM", &["G", "Ab", "Bb", "C", "Db", "Eb", "F"]); } #[test] //#[ignore] /// Harmonic minor /// /// Note that this case introduces the accidental interval (A) fn test_harmonic_minor() { process_interval_case("d", "MmMMmAm", &["D", "E", "F", "G", "A", "Bb", "Db"]); } #[test] //#[ignore] /// Octatonic fn test_octatonic() { process_interval_case( "C", "MmMmMmMm", &["C", "D", "D#", "F", "F#", "G#", "A", "B"], ); } #[test] //#[ignore] /// Hexatonic fn test_hexatonic() { process_interval_case("Db", "MMMMMM", &["Db", "Eb", "F", "G", "A", "B"]); } #[test] //#[ignore] /// Pentatonic fn test_pentatonic() { process_interval_case("A", "MMAMA", &["A", "B", "C#", "E", "F#"]); } #[test] //#[ignore] /// Enigmatic fn test_enigmatic() { process_interval_case("G", "mAMMMmm", &["G", "G#", "B", "C#", "D#", "F", "F#"]); } #}
4. 答案
# #![allow(unused_variables)] #fn main() { #[macro_use] extern crate enum_primitive_derive; extern crate failure; #[macro_use] extern crate failure_derive; extern crate itertools; extern crate num_traits; pub use self::interval::{Interval, Intervals}; use self::note::Accidental; pub use self::note::Note; use failure::Error; use std::str::FromStr; pub mod interval { use itertools::Itertools; use std::fmt; use std::ops::Deref; use std::str::FromStr; #[derive(Debug, Clone, Copy, PartialEq, Eq, Fail)] pub enum ParseErr { #[fail(display = "invalid interval")] InvalidInterval, #[fail(display = "wrong number of semitones")] WrongNumberOfSemitones, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Primitive)] pub enum Interval { HalfStep = 1, WholeStep = 2, AugmentedFirst = 3, } impl fmt::Display for Interval { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { use self::Interval::*; write!( f, "{}", match self { HalfStep => "m", WholeStep => "M", AugmentedFirst => "A", } ) } } impl FromStr for Interval { type Err = ParseErr; fn from_str(s: &str) -> Result<Self, Self::Err> { use self::Interval::*; match s { "m" => Ok(HalfStep), "M" => Ok(WholeStep), "A" => Ok(AugmentedFirst), _ => Err(ParseErr::InvalidInterval), } } } #[derive(Debug, Clone, PartialEq, Eq)] pub struct Intervals(Vec<Interval>); impl fmt::Display for Intervals { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}", self.0.iter().join("")) } } impl FromStr for Intervals { type Err = ParseErr; fn from_str(s: &str) -> Result<Self, Self::Err> { let mut semitones = Vec::with_capacity(s.len()); for (i, c) in s.char_indices() { semitones.push(Interval::from_str(&s[i..i + c.len_utf8()])?); } if semitones.iter().take(12).map(|&i| i as u8).sum::<u8>() == 12 { Ok(Intervals(semitones)) } else { Err(ParseErr::WrongNumberOfSemitones) } } } impl Deref for Intervals { type Target = Vec<Interval>; fn deref(&self) -> &Self::Target { &self.0 } } #[cfg(test)] mod test { use super::*; #[test] fn test_parse_chromatic() { assert!("mmmmmmmmmmmm".parse::<Intervals>().is_ok()); } #[test] fn test_parse_major() { assert!("MMmMMMm".parse::<Intervals>().is_ok()); } #[test] fn test_parse_minor() { assert!("MmMMmMM".parse::<Intervals>().is_ok()); } } } pub mod note { use interval::Interval; use num_traits::{FromPrimitive, ToPrimitive}; use std::fmt; use std::ops::AddAssign; use std::str::FromStr; pub const SEMITONES: i8 = 12; #[derive(Debug, Clone, Copy, PartialEq, Eq, Primitive)] pub enum Semitone { A = 0, ASharp = 1, B = 2, C = 3, CSharp = 4, D = 5, DSharp = 6, E = 7, F = 8, FSharp = 9, G = 10, GSharp = 11, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Primitive)] pub enum Root { A = 0, B = 2, C = 3, D = 5, E = 7, F = 8, G = 10, } impl fmt::Display for Root { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{:?}", self) } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Accidental { Sharp, Flat, } impl Accidental { fn to_i8(&self) -> i8 { match *self { Accidental::Sharp => 1, Accidental::Flat => -1, } } pub fn from_tonic(tonic: &str) -> Accidental { match tonic { "C" | "a" | "G" | "D" | "A" | "E" | "B" | "F#" | "e" | "b" | "f#" | "c#" | "g#" | "d#" => Accidental::Sharp, _ => Accidental::Flat, } } } impl fmt::Display for Accidental { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!( f, "{}", match &self { Accidental::Sharp => '#', Accidental::Flat => 'b', } ) } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct Note { tonic: Root, accidental: Option<Accidental>, } impl Note { pub fn canonicalize(&self, lean: Accidental) -> Note { let mut n: Note = Semitone::from(*self).into(); if let Some(accidental) = n.accidental { if accidental != lean { if lean == Accidental::Flat { n += Interval::HalfStep; n.accidental = Some(Accidental::Flat); } } } n } } impl fmt::Display for Note { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!( f, "{}{}", self.tonic, self.accidental.map_or(String::new(), |a| a.to_string()), ) } } #[derive(Debug, Clone, Copy, PartialEq, Eq, Fail)] pub enum ParseErr { #[fail(display = "invalid length")] InvalidLength, #[fail(display = "invalid tonic")] InvalidTonic, #[fail(display = "invalid accidental")] InvalidAccidental, } impl FromStr for Note { type Err = ParseErr; fn from_str(s: &str) -> Result<Self, Self::Err> { let lc = s.to_lowercase(); let mut iter = lc.chars(); let mut note = match iter.next() { Some(c) if 'a' <= c && 'g' >= c => Note { tonic: match c { 'a' => Root::A, 'b' => Root::B, 'c' => Root::C, 'd' => Root::D, 'e' => Root::E, 'f' => Root::F, 'g' => Root::G, _ => return Err(ParseErr::InvalidTonic), }, accidental: None, }, Some(_) => return Err(ParseErr::InvalidTonic), None => return Err(ParseErr::InvalidLength), }; match iter.next() { Some('b') => note.accidental = Some(Accidental::Flat), Some('#') => note.accidental = Some(Accidental::Sharp), Some(_) => return Err(ParseErr::InvalidAccidental), None => {} } if iter.next().is_some() { return Err(ParseErr::InvalidLength); } Ok(note) } } impl From<Semitone> for Note { fn from(s: Semitone) -> Self { Note { tonic: match s { Semitone::A | Semitone::ASharp => Root::A, Semitone::B => Root::B, Semitone::C | Semitone::CSharp => Root::C, Semitone::D | Semitone::DSharp => Root::D, Semitone::E => Root::E, Semitone::F | Semitone::FSharp => Root::F, Semitone::G | Semitone::GSharp => Root::G, }, accidental: match s { Semitone::ASharp | Semitone::CSharp | Semitone::DSharp | Semitone::FSharp | Semitone::GSharp => Some(Accidental::Sharp), _ => None, }, } } } impl From<Note> for Semitone { fn from(n: Note) -> Self { Semitone::from_i8( (SEMITONES + n.tonic.to_i8().unwrap() + n.accidental.map_or(0, |a| a.to_i8())) % SEMITONES, ).expect("must have valid semitone") } } impl AddAssign<Interval> for Note { fn add_assign(&mut self, rhs: Interval) { *self = Semitone::from_i8( (SEMITONES + Semitone::from(*self).to_i8().unwrap() + rhs.to_i8().unwrap()) % SEMITONES, ).unwrap() .into(); } } } #[derive(Debug)] pub struct Scale { tonic: Note, lean: Accidental, intervals: Intervals, } impl Scale { pub fn new(tonic: &str, intervals: &str) -> Result<Scale, Error> { Ok(Scale { tonic: Note::from_str(tonic)?, lean: Accidental::from_tonic(tonic), intervals: Intervals::from_str(intervals)?, }) } pub fn chromatic(tonic: &str) -> Result<Scale, Error> { Scale::new(tonic, "mmmmmmmmmmmm") } pub fn enumerate(&self) -> Vec<String> { let mut out = Vec::with_capacity(self.intervals.len()); let mut note = self.tonic; for &interval in self.intervals.iter() { out.push(note.canonicalize(self.lean).to_string()); note += interval; } out } } #}