├── migrations ├── .gitkeep └── 2019-10-26-214811_create_initial_tables │ ├── down.sql │ └── up.sql ├── .env ├── .gitignore ├── assets ├── Casio Piano C5.wav └── thumbpiano A#3.wav ├── diesel.toml ├── src ├── schema.rs ├── lib.rs ├── bin │ ├── chord │ │ └── main.rs │ └── repl │ │ └── main.rs ├── chord_library.rs ├── database.rs ├── parser.rs ├── sequencer.rs └── music_theory.rs ├── Cargo.toml ├── LICENSE.md ├── README.md └── Cargo.lock /migrations/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | DATABASE_URL=:memory: 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | .music_repl_history 4 | .vscode 5 | -------------------------------------------------------------------------------- /assets/Casio Piano C5.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tiniuclx/harmony-explorer/HEAD/assets/Casio Piano C5.wav -------------------------------------------------------------------------------- /assets/thumbpiano A#3.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tiniuclx/harmony-explorer/HEAD/assets/thumbpiano A#3.wav -------------------------------------------------------------------------------- /migrations/2019-10-26-214811_create_initial_tables/down.sql: -------------------------------------------------------------------------------- 1 | -- This file should undo anything in `up.sql` 2 | DROP TABLE notes; 3 | 4 | DROP TABLE names; 5 | -------------------------------------------------------------------------------- /diesel.toml: -------------------------------------------------------------------------------- 1 | # For documentation on how to configure this file, 2 | # see diesel.rs/guides/configuring-diesel-cli 3 | 4 | [print_schema] 5 | file = "src/schema.rs" 6 | -------------------------------------------------------------------------------- /src/schema.rs: -------------------------------------------------------------------------------- 1 | table! { 2 | names (chord) { 3 | chord -> Text, 4 | alternative_name -> Text, 5 | } 6 | } 7 | 8 | table! { 9 | notes (chord, interval) { 10 | chord -> Text, 11 | degree -> Integer, 12 | interval -> Integer, 13 | } 14 | } 15 | 16 | allow_tables_to_appear_in_same_query!(names, notes,); 17 | -------------------------------------------------------------------------------- /migrations/2019-10-26-214811_create_initial_tables/up.sql: -------------------------------------------------------------------------------- 1 | -- Your SQL goes here 2 | CREATE TABLE notes ( 3 | chord TEXT NOT NULL, 4 | degree INTEGER NOT NULL, 5 | interval INTEGER NOT NULL, 6 | PRIMARY KEY (chord, interval) 7 | ) WITHOUT ROWID; 8 | 9 | CREATE TABLE names ( 10 | chord TEXT NOT NULL, 11 | alternative_name TEXT PRIMARY KEY 12 | ) WITHOUT ROWID; 13 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate diesel; 3 | #[macro_use] 4 | extern crate diesel_migrations; 5 | extern crate dasp; 6 | extern crate enum_primitive_derive; 7 | extern crate find_folder; // For easily finding the assets folder. 8 | extern crate nom; 9 | extern crate num_traits; 10 | extern crate pitch_calc; // To work with musical notes. 11 | extern crate portaudio; // For audio I/O 12 | extern crate rustyline; // To convert portaudio sample buffers to frames. 13 | //extern crate sampler; 14 | 15 | pub mod chord_library; 16 | pub mod database; 17 | pub mod music_theory; 18 | pub mod parser; 19 | pub mod schema; 20 | pub mod sequencer; 21 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "harmony_explorer" 3 | version = "0.1.0" 4 | authors = ["Alexandru Tiniuc "] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | # Music Theory 11 | num = "0.4.0" 12 | num-traits ="0.2.8" 13 | lexical-core = "0.7.6" 14 | pitch_calc = "0.12.0" 15 | enum-primitive-derive = "0.2.1" 16 | libsqlite3-sys = { version = ">=0.8.0, <0.13.0", features = ["bundled"] } 17 | diesel = {version = "1.4.3", features = ["sqlite"]} 18 | diesel_migrations = "1.4.0" 19 | # REPL 20 | rustyline = "5.0.3" 21 | nom = "6.2.1" 22 | # Audio 23 | # sampler = "0.2.0" 24 | find_folder = "0.3.0" 25 | portaudio = "0.7.0" 26 | dasp = "0.11.0" 27 | # CLI Tools 28 | clap = "3.0.0-beta.2" -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2019 Alexandru Tiniuc 2 | 3 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 4 | 5 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 6 | 7 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 8 | 9 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 10 | 11 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /src/bin/chord/main.rs: -------------------------------------------------------------------------------- 1 | extern crate clap; 2 | 3 | use clap::Clap; 4 | use harmony_explorer as hexp; 5 | 6 | use hexp::music_theory::{Chord, LetterOctave}; 7 | 8 | /// Print the notes of a given chord. 9 | #[derive(Clap)] 10 | #[clap(name = "Chord: Print the notes of a given chord")] 11 | #[clap(author = "Alexandru Tiniuc ")] 12 | struct Opts { 13 | /// Name of the chord to search for. 14 | chord: String, 15 | /// Octave to append to notes. By default, notes are output without octave number. 16 | #[clap(short, long, default_value = "3")] 17 | // TODO: make this optional & print notes without octave 18 | octave: i32, 19 | } 20 | 21 | // TODO: add options for flats/sharps, inversions... 22 | // TODO: add reading from STDIN 23 | 24 | fn main() { 25 | // Initialise chord database 26 | use hexp::database::*; 27 | let db = initialise_database().unwrap(); 28 | hexp::chord_library::populate_database(&db); 29 | 30 | let opts = Opts::parse(); 31 | 32 | // Parse chord within `opts.chord` CLI field 33 | match hexp::parser::command_chord(&opts.chord) { 34 | Ok(("", hexp::parser::Command::Chord(letter, quality))) => { 35 | // Look up parsed chord within database 36 | match get_quality(&quality, &db) { 37 | Some(q) => { 38 | // If found, output to stdout! 39 | let chord = Chord { 40 | root: LetterOctave(letter, opts.octave), 41 | quality: q, 42 | }; 43 | println!("{}", chord); 44 | } 45 | None => { 46 | eprintln!("Could not find chord {}!", opts.chord); 47 | } 48 | }; 49 | } 50 | _ => { 51 | eprintln!("Invalid input!"); 52 | } 53 | }; 54 | } 55 | -------------------------------------------------------------------------------- /src/chord_library.rs: -------------------------------------------------------------------------------- 1 | use crate::database::*; 2 | use crate::music_theory::*; 3 | use crate::schema::*; 4 | use diesel::prelude::*; 5 | use diesel::sqlite::SqliteConnection; 6 | 7 | pub fn generate_chords() -> Vec { 8 | use degree_intervals::*; 9 | vec![ 10 | // Triads 11 | ("major", Maj3rd), 12 | ("major", Per5th), 13 | ("minor", Min3rd), 14 | ("minor", Per5th), 15 | ("diminished", Min3rd), 16 | ("diminished", Dim5th), 17 | ("augmented", Maj3rd), 18 | ("augmented", Aug5th), 19 | // Tetrads 20 | ("major seventh", Maj3rd), 21 | ("major seventh", Per5th), 22 | ("major seventh", Maj7th), 23 | ("dominant seventh", Maj3rd), 24 | ("dominant seventh", Per5th), 25 | ("dominant seventh", Min7th), 26 | ("minor seventh", Min3rd), 27 | ("minor seventh", Per5th), 28 | ("minor seventh", Min7th), 29 | ("major sixth", Maj3rd), 30 | ("major sixth", Per5th), 31 | ("major sixth", Maj6th), 32 | ("diminished seventh", Min3rd), 33 | ("diminished seventh", Dim5th), 34 | ("diminished seventh", Dim7th), 35 | ] 36 | .into_iter() 37 | .map(|t| ChordNote::note(t.0, (t.1).0, (t.1).1)) 38 | .collect() 39 | } 40 | 41 | pub fn generate_names() -> Vec { 42 | vec![ 43 | ("major", ""), 44 | ("major", "maj"), 45 | ("minor", "m"), 46 | ("minor", "-"), 47 | ("diminished", "dim"), 48 | ("diminished", "o"), 49 | ("diminished", "*"), 50 | ("augmented", "+"), 51 | ("augmented", "aug"), 52 | ("major seventh", "M7"), 53 | ("major seventh", "maj7"), 54 | ("dominant seventh", "7"), 55 | ("minor seventh", "m7"), 56 | ("minor seventh", "-7"), 57 | ("major sixth", "6"), 58 | ("major sixth", "maj6"), 59 | ("diminished seventh", "dim7"), 60 | ("diminished seventh", "o7"), 61 | ] 62 | .into_iter() 63 | .map(|t| ChordName::name(t.0, t.1)) 64 | .collect() 65 | } 66 | 67 | pub fn populate_database(db: &SqliteConnection) { 68 | diesel::insert_into(notes::table) 69 | .values(generate_chords()) 70 | .execute(db) 71 | .unwrap(); 72 | 73 | diesel::insert_into(names::table) 74 | .values(generate_names()) 75 | .execute(db) 76 | .unwrap(); 77 | } 78 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Harmony Explorer 2 | Explore complex harmony without playing the chords. Type the name of any 3 | chord and Harmony Explorer plays it for you! 4 | 5 | ## Features 6 | 7 | - Hear any commonly used chord and see the notes the chord is composed of. 8 | ``` 9 | ♪♪♪ C 10 | Playing C4 E4 G4 11 | ♪♪♪ Bdim 12 | Playing B4 D5 F5 13 | ♪♪♪ G7 14 | Playing G4 B4 D5 F5 15 | ♪♪♪ Am 16 | Playing A4 C5 E5 17 | ``` 18 | 19 | - Use several commonly used names for each chord. 20 | ``` 21 | ♪♪♪ Bbm 22 | Playing Bb4 Db5 F5 23 | ♪♪♪ Bb- 24 | Playing Bb4 Db5 F5 25 | ♪♪♪ Bb minor 26 | Playing Bb4 Db5 F5 27 | ``` 28 | 29 | - Transpose chords up or down 30 | ``` 31 | ♪♪♪ transpose 5 Cmaj7 32 | Fmaj7 33 | Playing F4 A4 C5 E5 34 | ♪♪♪ transpose -5 Cmaj7 35 | Gmaj7 36 | Playing G4 B4 D5 Gb5 37 | ♪♪♪ t 5 Cmaj7 38 | Fmaj7 39 | Playing F4 A4 C5 E5 40 | ``` 41 | 42 | - Choose whether to display accidentals using sharps or flats. 43 | ``` 44 | ♪♪♪ F#dim 45 | Playing Gb4 A4 C5 Gb5 46 | ♪♪♪ sharps 47 | Notating accidentals using sharps. 48 | ♪♪♪ F#dim 49 | Playing F#4 A4 C5 F#5 50 | ``` 51 | - Command history support: use the up- and down-arrow keys to navigate 52 | through your previous commands. The commands are remembered after you close 53 | the program. 54 | 55 | - Press Enter to re-do the last command. Useful if you want to hear the chord 56 | that was last played without having to type it again. 57 | 58 | ## Building from source 59 | 60 | First, you must install several dependencies in order to build and run the 61 | project. 62 | 63 | ### Ubuntu & Debian: 64 | `sudo apt-get install git libasound2-dev` 65 | 66 | You will also need to install the Rust compiler. Instructions for doing so 67 | [can be found here.](https://www.rust-lang.org/tools/install) 68 | 69 | You may obtain the code using the following command: 70 | ``` 71 | git clone https://github.com/tiniuclx/harmony-explorer.git 72 | ``` 73 | 74 | Then, build & run it using Cargo, the Rust package manager & build system. 75 | This step will take a few minutes. Once it is complete, Harmony Explorer will 76 | start, you will see the prompt (three quavers, like in the example) and you 77 | can start typing your commands. 78 | 79 | ``` 80 | cd harmony-explorer 81 | cargo run 82 | ``` 83 | 84 | ### Windows 85 | 86 | You will first need to [install Rust](https://www.rust-lang.org/tools/install). 87 | For Windows, you will also have to build `portaudio` manually. For this, you will 88 | need CMake and Visual Studio Build Tools. Once you've downloaded the 89 | `portaudio` source, configure it to build using Visual Studio. You can then 90 | build it by running the folliwing inside a Windows developer shell: 91 | 92 | ```MSBuild.exe .\portaudio.sln /property:Configuration=Release``` 93 | 94 | Once the command finishes, the output can be found inside the `Release` 95 | folder within your build directory. 96 | 97 | You will also need to tell the linker where to find the library. There should 98 | be a way to do this more cleanly, but I did not find it yet. Something like 99 | [this 100 | answer](https://stackoverflow.com/questions/43826572/where-should-i-place-a-static-library-so-i-can-link-it-with-a-rust-program) 101 | should do the trick, but this didn't really work, or I did it incorrectly. 102 | 103 | 104 | Copy the generated files into the `/LIBPATH` passed to the link command (visible when 105 | attempting to build the project using Cargo). In my case, this is: 106 | ```path 107 | C:\Users\tiniu\.rustup\toolchains\stable-x86_64-pc-windows-msvc\lib\rustlib\x86_64-pc-windows-msvc\lib 108 | ``` 109 | The linker expects to find `portaudio.lib`, so you will also have to rename 110 | all the files to remove "_x64": `portaudio_x64.lib` shall become `portaudio.lib` and so on. 111 | 112 | Then, download, build and run the project as is typical for Rust: 113 | ``` 114 | git clone https://github.com/tiniuclx/harmony-explorer.git 115 | cd harmony-explorer 116 | cargo run 117 | ``` 118 | -------------------------------------------------------------------------------- /src/database.rs: -------------------------------------------------------------------------------- 1 | use crate::music_theory::*; 2 | use crate::schema::*; 3 | use diesel::prelude::*; 4 | use diesel::sqlite::SqliteConnection; 5 | use diesel_migrations::*; 6 | 7 | embed_migrations!("migrations/"); 8 | 9 | #[derive(Debug, PartialEq, Eq, Queryable, Insertable)] 10 | #[table_name = "notes"] 11 | pub struct ChordNote { 12 | pub chord: String, 13 | pub degree: Degree, 14 | pub interval: Interval, 15 | } 16 | 17 | impl ChordNote { 18 | pub fn note(chord: &str, degree: Degree, interval: Interval) -> ChordNote { 19 | ChordNote { 20 | chord: chord.to_string(), 21 | degree: degree, 22 | interval: interval, 23 | } 24 | } 25 | } 26 | 27 | #[derive(Debug, PartialEq, Eq, Queryable, Insertable)] 28 | #[table_name = "names"] 29 | pub struct ChordName { 30 | pub chord: String, 31 | pub alternative_name: String, 32 | } 33 | 34 | impl ChordName { 35 | pub fn name(chord: &str, alternative_name: &str) -> ChordName { 36 | ChordName { 37 | chord: chord.to_string(), 38 | alternative_name: alternative_name.to_string(), 39 | } 40 | } 41 | } 42 | 43 | pub fn initialise_database() -> Result> { 44 | let connection = SqliteConnection::establish(":memory:")?; 45 | embedded_migrations::run(&connection)?; 46 | Ok(connection) 47 | } 48 | 49 | /// Search the chord database for a chord quality with the given name. If the 50 | /// chord quality is not found, returns `None` 51 | pub fn get_quality(name: &str, conn: &SqliteConnection) -> Option { 52 | // Search for the full name in the abbreviations table 53 | let primary_name = names::table 54 | .filter(names::alternative_name.eq(name.trim())) 55 | .limit(1) 56 | .select(names::chord) 57 | .load::(conn) 58 | .ok()?; 59 | 60 | // If the full name was found, use that. Otherwise, use the given name. 61 | let searched_name: String; 62 | match primary_name.as_slice() { 63 | [found_name] => searched_name = found_name.clone(), 64 | [] => searched_name = name.trim().to_owned(), 65 | _ => return None, 66 | }; 67 | 68 | let quality = notes::table 69 | .filter(notes::chord.eq(searched_name)) 70 | .load::(conn) 71 | .ok() 72 | .map(|ns| ns.into_iter().map(|n| (n.degree, n.interval)).collect()); 73 | 74 | // If the query returns no notes, the chord does not exist! 75 | if quality == Some(vec![]) { 76 | None 77 | } else { 78 | quality 79 | } 80 | } 81 | 82 | #[cfg(test)] 83 | mod tests { 84 | use super::*; 85 | #[test] 86 | fn insert() { 87 | use degrees::*; 88 | use intervals::*; 89 | let conn = initialise_database().unwrap(); 90 | 91 | let inserted_notes = vec![ 92 | ChordNote { 93 | chord: "maj".to_string(), 94 | degree: III, 95 | interval: Maj3rd, 96 | }, 97 | ChordNote { 98 | chord: "maj".to_string(), 99 | degree: V, 100 | interval: Per5th, 101 | }, 102 | ]; 103 | 104 | // We are inserting two notes, make sure that is the case 105 | assert_eq!( 106 | 2, 107 | diesel::insert_into(notes::table) 108 | .values(inserted_notes) 109 | .execute(&conn) 110 | .expect("Could not insert note") 111 | ); 112 | 113 | let inserted_name = ChordName { 114 | chord: "maj".to_string(), 115 | alternative_name: "major".to_string(), 116 | }; 117 | 118 | assert_eq!( 119 | 1, 120 | diesel::insert_into(names::table) 121 | .values(inserted_name) 122 | .execute(&conn) 123 | .expect("Could not insert name") 124 | ); 125 | } 126 | 127 | #[test] 128 | fn retrieve() { 129 | use super::*; 130 | use degrees::*; 131 | use intervals::*; 132 | let conn = initialise_database().unwrap(); 133 | 134 | let new_note = ChordNote { 135 | chord: "maj".to_string(), 136 | degree: III, 137 | interval: Maj3rd, 138 | }; 139 | 140 | assert_eq!( 141 | 1, 142 | diesel::insert_into(notes::table) 143 | .values(&new_note) 144 | .execute(&conn) 145 | .expect("Could not insert note") 146 | ); 147 | 148 | let retrieved_notes = notes::table 149 | .filter(notes::chord.eq("maj")) 150 | .load::(&conn) 151 | .expect("Could not retrieve note"); 152 | 153 | assert_eq!(retrieved_notes.len(), 1); 154 | assert_eq!(retrieved_notes[0], new_note); 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /src/parser.rs: -------------------------------------------------------------------------------- 1 | use crate::music_theory::*; 2 | use nom::character::complete::multispace0; 3 | use nom::character::complete::not_line_ending; 4 | use nom::*; 5 | use std::collections::HashMap; 6 | use std::str::FromStr; 7 | 8 | /// This is the abstract syntax tree of the REPL. It describes the syntax of 9 | /// every command that can be used. 10 | #[derive(Debug, PartialEq, Eq)] 11 | pub enum Command { 12 | /// Nothing at all was typed. 13 | EmptyString, 14 | /// A valid note letter followed by the chord quality. 15 | Chord(Letter, String), 16 | /// The word "sharps" 17 | Sharps, 18 | /// The word "flats" 19 | Flats, 20 | /// The word "transpose", followed by a signed integer, followed by a chord 21 | Transpose(i32, Letter, String), 22 | } 23 | 24 | // Parsers & sub-parsers for Chord. 25 | 26 | named! { letter_accidental (&str) -> String, 27 | do_parse!( 28 | letter: one_of!("ABCDEFG") >> 29 | accidental: complete!(alt!(char!('#') | char!('b'))) >> 30 | ( [letter, accidental].iter().collect() ) 31 | ) 32 | } 33 | 34 | named! { letter_natural (&str) -> String, 35 | do_parse!( 36 | letter: one_of!("ABCDEFG") >> 37 | ( letter.to_string() ) 38 | ) 39 | } 40 | 41 | named! { select_letter (&str) -> String, 42 | alt!(letter_accidental | letter_natural) 43 | } 44 | 45 | fn note_map() -> HashMap { 46 | use Letter::*; 47 | [ 48 | ("C", C), 49 | ("C#", Csh), 50 | ("Db", Db), 51 | ("D", D), 52 | ("D#", Dsh), 53 | ("Eb", Eb), 54 | ("E", E), 55 | ("F", F), 56 | ("F#", Fsh), 57 | ("Gb", Gb), 58 | ("G", G), 59 | ("G#", Gsh), 60 | ("Ab", Ab), 61 | ("A", A), 62 | ("A#", Ash), 63 | ("Bb", Bb), 64 | ("B", B), 65 | ] 66 | .iter() 67 | .map(|t| ((t.0).to_string(), t.1)) 68 | .collect() 69 | } 70 | 71 | named! { pub letter (&str) -> Letter, 72 | map_opt!(select_letter, |s: String| note_map().get(&s).map(|l| l.clone())) 73 | } 74 | 75 | named! { pub command_chord (&str) -> Command, 76 | do_parse!( 77 | letter: letter >> 78 | chord: not_line_ending >> 79 | (Command::Chord(letter, chord.trim().to_string())) 80 | ) 81 | } 82 | 83 | // Parser for the empty string. 84 | named! { command_null (&str) -> Command, 85 | map!(eof!(), |_| Command::EmptyString) 86 | } 87 | 88 | // Parsers for the sharps and flats commands 89 | 90 | named! {command_sharps (&str) -> Command, 91 | map!( 92 | alt!( 93 | complete!(tag!("sharps")) | 94 | complete!(tag!("sharp")) 95 | ), 96 | |_| Command::Sharps 97 | ) 98 | } 99 | 100 | named! {command_flats (&str) -> Command, 101 | map!( 102 | alt!( 103 | complete!(tag!("flats")) | 104 | complete!(tag!("flat")) 105 | ), 106 | |_| Command::Flats 107 | ) 108 | } 109 | 110 | named! { parse_signed_i32 (&str) -> i32, 111 | map_res!( 112 | recognize!(tuple!(opt!(char!('-')), nom::character::complete::digit1)), 113 | i32::from_str) 114 | } 115 | 116 | named! { command_transpose (&str) -> Command, 117 | do_parse!( 118 | alt!(tag!("transpose") | tag!("t")) >> 119 | multispace0 >> 120 | distance: parse_signed_i32 >> 121 | multispace0 >> 122 | letter: letter >> 123 | chord: not_line_ending >> 124 | (Command::Transpose(distance, letter, chord.trim().to_string())) 125 | ) 126 | } 127 | 128 | // Top-level parser, containing the entire command syntax. 129 | named! { pub parse_command (&str) -> Command, 130 | alt!( 131 | command_null | 132 | command_flats | 133 | command_sharps | 134 | command_transpose | 135 | command_chord 136 | ) 137 | } 138 | 139 | #[cfg(test)] 140 | mod tests { 141 | use super::*; 142 | use nom::Err::*; 143 | use nom::Needed::*; 144 | use std::num::*; 145 | use Letter::*; 146 | 147 | #[test] 148 | fn letter_naturals() { 149 | assert_eq!(letter("A"), Ok(("", A))); 150 | assert_eq!(letter("D"), Ok(("", D))); 151 | assert_eq!(letter("E"), Ok(("", E))); 152 | assert_eq!(letter("E7b5"), Ok(("7b5", E))); 153 | 154 | assert_eq!( 155 | letter(""), 156 | Err(Incomplete(Size(NonZeroUsize::new(1).unwrap()))) 157 | ); 158 | assert_ne!(letter("a"), Ok(("", A))); 159 | } 160 | 161 | #[test] 162 | fn letter_accidentals() { 163 | assert_eq!(letter("Db"), Ok(("", Db))); 164 | assert_eq!(letter("Eb"), Ok(("", Eb))); 165 | assert_eq!(letter("G#"), Ok(("", Gsh))); 166 | assert_eq!(letter("Bb7add9"), Ok(("7add9", Bb))); 167 | 168 | assert_eq!( 169 | letter(""), 170 | Err(Incomplete(Size(NonZeroUsize::new(1).unwrap()))) 171 | ); 172 | assert_ne!(letter("Db"), Ok(("", D))); 173 | assert_ne!(letter("E#"), Ok(("", F))); 174 | } 175 | 176 | #[test] 177 | fn command_null() { 178 | assert_eq!(parse_command(""), Ok(("", Command::EmptyString))); 179 | assert_ne!( 180 | parse_command("asdfasdf"), 181 | Ok(("asdfasdf", Command::EmptyString)) 182 | ); 183 | } 184 | 185 | #[test] 186 | fn command_accidentals() { 187 | assert_eq!(parse_command("sharps"), Ok(("", Command::Sharps))); 188 | assert_eq!(parse_command("flat"), Ok(("", Command::Flats))); 189 | } 190 | 191 | #[test] 192 | fn command_transpose() { 193 | assert_eq!( 194 | parse_command("transpose 5 C#maj7"), 195 | Ok(("", Command::Transpose(5, Csh, "maj7".to_owned()))) 196 | ); 197 | 198 | assert_eq!( 199 | parse_command("transpose -7 C#maj7"), 200 | Ok(("", Command::Transpose(-7, Csh, "maj7".to_owned()))) 201 | ); 202 | 203 | assert_eq!( 204 | parse_command("t -7 C#maj7"), 205 | Ok(("", Command::Transpose(-7, Csh, "maj7".to_owned()))) 206 | ); 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /src/sequencer.rs: -------------------------------------------------------------------------------- 1 | use pitch_calc::LetterOctave; 2 | use std::cmp::Ordering; 3 | use std::collections::BinaryHeap; 4 | use std::option::Option; 5 | use std::sync::mpsc::{channel, Receiver, RecvTimeoutError, Sender}; 6 | use std::time::{Duration, Instant}; 7 | 8 | #[derive(PartialEq, Eq, Copy, Clone, Debug)] 9 | #[allow(dead_code)] 10 | pub enum Message { 11 | NoteOn(LetterOctave), 12 | NoteOff(LetterOctave), 13 | Stop, 14 | } 15 | 16 | #[derive(PartialEq, Eq, Copy, Clone, Debug)] 17 | pub struct Event { 18 | pub msg: Message, 19 | pub del: Duration, 20 | } 21 | 22 | #[derive(PartialEq, Eq)] 23 | struct EventAbs { 24 | msg: Message, 25 | ins: Instant, 26 | } 27 | 28 | impl Ord for EventAbs { 29 | fn cmp(&self, other: &EventAbs) -> Ordering { 30 | // sooner events come first 31 | return other.ins.cmp(&self.ins); 32 | } 33 | } 34 | 35 | impl PartialOrd for EventAbs { 36 | fn partial_cmp(&self, other: &EventAbs) -> Option { 37 | Some(self.cmp(other)) 38 | } 39 | } 40 | 41 | #[allow(dead_code)] 42 | fn rel_to_abs(rel: Event) -> EventAbs { 43 | EventAbs { 44 | msg: rel.msg, 45 | ins: Instant::now() + rel.del, 46 | } 47 | } 48 | 49 | pub fn start() -> (Sender, Receiver) { 50 | let (msg_tx, msg_rx) = channel(); 51 | let (event_tx, event_rx) = channel(); 52 | 53 | std::thread::spawn(move || { 54 | // Heap of events that are awaiting processing. The soonest event that must be processed 55 | // sits at the top of the heap 56 | let mut heap = BinaryHeap::::new(); 57 | 58 | loop { 59 | // outer Maybe: Some(_) if there is an event, None if there are no events 60 | // inner Maybe: Some(t) if the event is going to happen in the future, after t time. None if it is in the past. 61 | let t_event = heap 62 | .peek() 63 | .map(|e| e.ins.checked_duration_since(Instant::now())); 64 | 65 | match t_event { 66 | // soonest event must trigger in the future 67 | Some(Some(t)) => match event_rx.recv_timeout(t) { 68 | Ok(e) => heap.push(rel_to_abs(e)), 69 | Err(RecvTimeoutError::Timeout) => {} 70 | Err(RecvTimeoutError::Disconnected) => break, 71 | }, 72 | // soonest event must trigger now 73 | Some(None) => {} 74 | // no events in heap 75 | None => match event_rx.recv() { 76 | Ok(e) => heap.push(rel_to_abs(e)), 77 | _ => break, 78 | }, 79 | }; 80 | 81 | // put any other events onto the heap 82 | for e in event_rx.try_iter() { 83 | heap.push(rel_to_abs(e)); 84 | } 85 | 86 | // predicate which is true if the heap has events that must trigger, false if it is empty or 87 | // if all the remaining events are in the future 88 | while heap 89 | .peek() 90 | .map(|e| e.ins <= Instant::now()) 91 | .unwrap_or(false) 92 | { 93 | // send the events that must fire through the message receiver 94 | msg_tx.send(heap.pop().unwrap().msg).unwrap(); 95 | } 96 | } 97 | }); 98 | 99 | (event_tx, msg_rx) 100 | } 101 | 102 | #[cfg(test)] 103 | mod tests { 104 | use super::*; 105 | 106 | #[test] 107 | fn compare() { 108 | use pitch_calc::Letter::C; 109 | let first_event = EventAbs { 110 | msg: Message::NoteOn(LetterOctave(C, 4)), 111 | ins: Instant::now(), 112 | }; 113 | let second_event = EventAbs { 114 | msg: Message::Stop, 115 | ins: Instant::now() + Duration::from_millis(200), 116 | }; 117 | assert_eq!(first_event > second_event, true); 118 | } 119 | 120 | #[test] 121 | fn sequencer_basic() { 122 | use pitch_calc::Letter::C; 123 | 124 | let first_msg = Message::NoteOn(LetterOctave(C, 4)); 125 | let first_event = Event { 126 | msg: first_msg, 127 | del: Duration::from_millis(0), 128 | }; 129 | 130 | let second_msg = Message::Stop; 131 | let second_event = Event { 132 | msg: second_msg, 133 | del: Duration::from_millis(200), 134 | }; 135 | 136 | let (tx, rx) = start(); 137 | 138 | tx.send(first_event).unwrap(); 139 | tx.send(second_event).unwrap(); 140 | 141 | assert_eq!(rx.recv().unwrap(), first_msg); 142 | assert!(rx.try_recv().is_err()); 143 | 144 | std::thread::sleep(Duration::from_millis(300)); 145 | assert_eq!(rx.try_recv().unwrap(), second_msg); 146 | } 147 | 148 | #[test] 149 | fn sequencer_order_of_operations() { 150 | use pitch_calc::Letter::{A, B}; 151 | let msg_1 = Message::NoteOn(LetterOctave(A, 4)); 152 | let msg_2 = Message::NoteOn(LetterOctave(B, 4)); 153 | let msg_3 = Message::NoteOff(LetterOctave(A, 4)); 154 | let msg_4 = Message::NoteOff(LetterOctave(B, 4)); 155 | 156 | let zero = Duration::from_millis(0); 157 | let del = Duration::from_millis(200); 158 | 159 | let ev_1 = Event { 160 | msg: msg_1, 161 | del: zero, 162 | }; 163 | let ev_2 = Event { 164 | msg: msg_3, 165 | del: del, 166 | }; 167 | let ev_3 = Event { 168 | msg: msg_2, 169 | del: zero, 170 | }; 171 | let ev_4 = Event { 172 | msg: msg_4, 173 | del: del, 174 | }; 175 | 176 | let (tx, rx) = start(); 177 | tx.send(ev_1).unwrap(); 178 | tx.send(ev_2).unwrap(); 179 | 180 | std::thread::sleep(Duration::from_millis(10)); 181 | tx.send(ev_3).unwrap(); 182 | tx.send(ev_4).unwrap(); 183 | 184 | assert_eq!(rx.recv().unwrap(), msg_1); 185 | assert_eq!(rx.recv().unwrap(), msg_2); 186 | assert_eq!(rx.recv().unwrap(), msg_3); 187 | assert_eq!(rx.recv().unwrap(), msg_4); 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /src/bin/repl/main.rs: -------------------------------------------------------------------------------- 1 | extern crate diesel; 2 | extern crate diesel_migrations; 3 | extern crate nom; 4 | 5 | extern crate dasp; 6 | extern crate enum_primitive_derive; 7 | extern crate find_folder; // For easily finding the assets folder. 8 | extern crate harmony_explorer; 9 | extern crate num_traits; 10 | extern crate pitch_calc as pitch; // To work with musical notes. 11 | extern crate portaudio as pa; // For audio I/O 12 | extern crate rustyline; // To convert portaudio sample buffers to frames. 13 | // extern crate sampler; 14 | 15 | use diesel::SqliteConnection; 16 | use std::error::Error; 17 | use std::sync::mpsc; 18 | // use std::time::Duration; 19 | 20 | use rustyline::error::ReadlineError; 21 | use rustyline::Editor; 22 | 23 | use harmony_explorer::parser::{parse_command, Command}; 24 | use harmony_explorer::{chord_library, database, music_theory, sequencer}; 25 | // use sampler::Sampler; 26 | 27 | /* 28 | const CHANNELS: i32 = 2; 29 | const SAMPLE_RATE: f64 = 44_100.0; 30 | const FRAMES_PER_BUFFER: u32 = 1024; 31 | //const THUMB_PIANO: &'static str = "thumbpiano A#3.wav"; 32 | const CASIO_PIANO: &'static str = "Casio Piano C5.wav"; 33 | const NOTE_VELOCITY: f32 = 0.6; 34 | const CHORD_LENGTH: Duration = Duration::from_millis(1000); 35 | */ 36 | 37 | fn main() -> Result<(), Box> { 38 | // Initialise audio plumbing and sampler. 39 | /* 40 | 41 | // We'll create a sample map that maps a single sample to the entire note range. 42 | let assets = find_folder::Search::ParentsThenKids(5, 5).for_folder("assets")?; 43 | let sample = sampler::Sample::from_wav_file(assets.join(CASIO_PIANO), SAMPLE_RATE)?; 44 | let sample_map = sampler::Map::from_single_sample(sample); 45 | // Create atomic RC pointer to a mutex protecting the polyphonic sampler 46 | 47 | let mut sampler = Sampler::poly((), sample_map).num_voices(12).release(20.0); 48 | // Initialise PortAudio and create an output stream. 49 | let pa = pa::PortAudio::new()?; 50 | let settings = 51 | pa.default_output_stream_settings::(CHANNELS, SAMPLE_RATE, FRAMES_PER_BUFFER)?; 52 | 53 | let (tx, rx) = sequencer::start(); 54 | 55 | // Callback is frequently called by PortAudio to fill the audio buffer with samples, 56 | // which generates sound. Do not do expensive or blocking things in this function! 57 | let callback = move |pa::OutputStreamCallbackArgs { buffer, .. }| { 58 | for m in rx.try_iter() { 59 | match m { 60 | sequencer::Message::NoteOn(n) => sampler.note_on(n.to_hz(), NOTE_VELOCITY), 61 | sequencer::Message::NoteOff(n) => sampler.note_off(n.to_hz()), 62 | sequencer::Message::Stop => sampler.stop(), 63 | } 64 | } 65 | 66 | let buffer: &mut [[f32; CHANNELS as usize]] = 67 | sample::slice::to_frame_slice_mut(buffer).unwrap(); 68 | sample::slice::equilibrium(buffer); 69 | 70 | sampler.fill_slice(buffer, SAMPLE_RATE); 71 | 72 | pa::Continue 73 | }; 74 | 75 | let mut stream = pa.open_non_blocking_stream(settings, callback)?; 76 | stream.start()?; 77 | */ 78 | // Audio initialisation is complete. Start processing keyboard input. 79 | 80 | // Duplicate definition, use one above when adding sound back in 81 | let (tx, _) = sequencer::start(); 82 | 83 | let mut rl = Editor::<()>::new(); 84 | if let Err(_) = rl.load_history(".music_repl_history") { 85 | // No previous history - that's okay! 86 | } 87 | 88 | // In-memory SQLite database of chords 89 | let db = database::initialise_database().unwrap(); 90 | chord_library::populate_database(&db); 91 | 92 | // The last non-empty command is stored here, to be executed again 93 | // based on user input. 94 | let mut last_command: Option = None; 95 | 96 | loop { 97 | let readline = rl.readline("♪♪♪ "); 98 | match readline { 99 | Ok(line) => { 100 | rl.add_history_entry(line.as_str()); 101 | match parse_command(&line) { 102 | Ok(("", command)) => { 103 | // Act based on the received command, and save it if it 104 | // is not empty. 105 | execute(&command, &last_command, &tx, &db); 106 | if command != Command::EmptyString { 107 | last_command = Some(command); 108 | } 109 | } 110 | Ok((remaining, _)) => { 111 | // Should not get here, the parser should consume all input 112 | println!("Could not process input: {}", remaining); 113 | } 114 | Err(e) => println!("Error encountered while parsing command: {:?}", e), 115 | } 116 | } 117 | Err(ReadlineError::Interrupted) => { 118 | println!("CTRL-C, exiting..."); 119 | break; 120 | } 121 | Err(ReadlineError::Eof) => { 122 | println!("CTRL-D, exiting..."); 123 | break; 124 | } 125 | Err(err) => { 126 | println!("Input Error: {:?}", err); 127 | break; 128 | } 129 | } 130 | } 131 | rl.save_history(".music_repl_history").unwrap(); 132 | //stream.close()?; 133 | Ok(()) 134 | } 135 | 136 | // Ideally this function should be as small as possible - 137 | // all the work should be done in the functional core, 138 | // the command parser. All this function must do is 139 | // glue the different modules together 140 | fn execute( 141 | command: &Command, 142 | last_command: &Option, 143 | tx: &mpsc::Sender, 144 | db: &SqliteConnection, 145 | ) { 146 | use music_theory::*; 147 | match command { 148 | // Look up the chord quality in the database, play it and 149 | // print its notes. 150 | Command::Chord(letter, quality) => { 151 | use database::*; 152 | match get_quality(&quality, &db) { 153 | Some(q) => { 154 | let chord = Chord { 155 | root: LetterOctave(*letter, 4), 156 | quality: q, 157 | }; 158 | 159 | /* 160 | chord 161 | .notes() 162 | .into_iter() 163 | .map(|n| { 164 | ( 165 | sequencer::Message::NoteOn(n), 166 | sequencer::Message::NoteOff(n), 167 | ) 168 | }) 169 | .for_each(|(on, off)| { 170 | let zero = Duration::from_millis(0); 171 | tx.send(sequencer::Event { msg: on, del: zero }).unwrap(); 172 | tx.send(sequencer::Event { 173 | msg: off, 174 | del: CHORD_LENGTH, 175 | }) 176 | .unwrap(); 177 | }); 178 | */ 179 | 180 | println!("Playing {}", chord); 181 | } 182 | None => { 183 | println!("Could not find chord!"); 184 | } 185 | } 186 | } 187 | // Re-do the last command. 188 | Command::EmptyString => match last_command { 189 | Some(Command::EmptyString) => (), 190 | Some(c) => execute(&c, &None, tx, db), 191 | None => (), 192 | }, 193 | 194 | Command::Flats => { 195 | set_use_flats(true); 196 | println!("Notating accidentals using flats."); 197 | } 198 | 199 | Command::Sharps => { 200 | set_use_flats(false); 201 | println!("Notating accidentals using sharps."); 202 | } 203 | 204 | Command::Transpose(distance, letter, quality) => { 205 | let new_letter = *letter + *distance; 206 | let new_command = Command::Chord(new_letter, quality.to_string()); 207 | 208 | println!("{}{}", letter_to_string(new_letter), quality.to_string()); 209 | execute(&new_command, last_command, tx, db); 210 | } 211 | }; 212 | } 213 | -------------------------------------------------------------------------------- /src/music_theory.rs: -------------------------------------------------------------------------------- 1 | pub use pitch_calc::{Letter, LetterOctave, Step}; 2 | use std::fmt; 3 | 4 | use pitch_calc::letter_octave_from_step; 5 | 6 | // TODO: implement Display for Degree 7 | pub type Degree = i32; 8 | #[allow(dead_code)] 9 | /// Helper module exporting constants for roman numeral chord and scale degrees. 10 | pub mod degrees { 11 | use super::*; 12 | pub const I: Degree = 1; 13 | pub const II: Degree = 2; 14 | pub const III: Degree = 3; 15 | pub const IV: Degree = 4; 16 | pub const V: Degree = 5; 17 | pub const VI: Degree = 6; 18 | pub const VII: Degree = 7; 19 | } 20 | 21 | // TODO: implement Display for interval 22 | pub type Interval = i32; 23 | #[allow(dead_code, non_upper_case_globals)] 24 | /// Helper module containing intervals, and their size in semitones. 25 | pub mod intervals { 26 | use super::*; 27 | pub const Root: Interval = 0; 28 | pub const Min2nd: Interval = 1; 29 | pub const Maj2nd: Interval = 2; 30 | pub const Min3rd: Interval = 3; 31 | pub const Maj3rd: Interval = 4; 32 | pub const Per4th: Interval = 5; 33 | 34 | pub const Dim5th: Interval = 6; 35 | pub const Per5th: Interval = 7; 36 | pub const Aug5th: Interval = 8; 37 | 38 | pub const Min6th: Interval = 8; 39 | pub const Maj6th: Interval = 9; 40 | pub const Dim7th: Interval = 9; 41 | pub const Min7th: Interval = 10; 42 | pub const Maj7th: Interval = 11; 43 | 44 | pub const Octave: Interval = 12; 45 | } 46 | 47 | #[allow(dead_code, non_upper_case_globals)] 48 | // Contains constants of the type (Degree, Interval). Useful mostly for building Chords. 49 | pub mod degree_intervals { 50 | use super::*; 51 | use degrees::*; 52 | pub const Root: (Degree, Interval) = (I, intervals::Root); 53 | 54 | pub const Min2nd: (Degree, Interval) = (II, intervals::Min2nd); 55 | pub const Maj2nd: (Degree, Interval) = (II, intervals::Maj2nd); 56 | 57 | pub const Min3rd: (Degree, Interval) = (III, intervals::Min3rd); 58 | pub const Maj3rd: (Degree, Interval) = (III, intervals::Maj3rd); 59 | 60 | pub const Per4th: (Degree, Interval) = (IV, intervals::Per4th); 61 | 62 | pub const Dim5th: (Degree, Interval) = (V, intervals::Dim5th); 63 | pub const Per5th: (Degree, Interval) = (V, intervals::Per5th); 64 | pub const Aug5th: (Degree, Interval) = (V, intervals::Aug5th); 65 | 66 | pub const Min6th: (Degree, Interval) = (VI, intervals::Min6th); 67 | pub const Maj6th: (Degree, Interval) = (VI, intervals::Maj6th); 68 | 69 | pub const Dim7th: (Degree, Interval) = (VII, intervals::Dim7th); 70 | pub const Min7th: (Degree, Interval) = (VII, intervals::Min7th); 71 | pub const Maj7th: (Degree, Interval) = (VII, intervals::Maj7th); 72 | 73 | pub const Octave: (Degree, Interval) = (I, intervals::Octave); 74 | } 75 | 76 | pub type Quality = Vec<(Degree, Interval)>; 77 | 78 | /// Chords are composed of the root tone, followed by a list of notes 79 | /// and their scale degrees. 80 | #[derive(PartialEq, Eq, Debug)] 81 | pub struct Chord { 82 | pub root: LetterOctave, 83 | pub quality: Quality, 84 | } 85 | 86 | /// Transpose the note by the number of semitones in `interval`. 87 | pub fn transpose(note: LetterOctave, interval: Interval) -> LetterOctave { 88 | let interval_s = interval as pitch_calc::calc::Step; 89 | let (letter, octave) = letter_octave_from_step(note.step() + interval_s); 90 | LetterOctave(letter, octave) 91 | } 92 | 93 | #[allow(dead_code)] 94 | impl Chord { 95 | /// Returns a new chord, transposed by `interval`. 96 | pub fn transposed(&self, interval: Interval) -> Chord { 97 | Chord { 98 | root: transpose(self.root, interval), 99 | quality: self.quality.clone(), 100 | } 101 | } 102 | 103 | /// Returns the root note of the chord. 104 | pub fn root(&self) -> LetterOctave { 105 | self.root 106 | } 107 | 108 | /// Returns a new chord with the same quality but with a different root, 109 | /// determined by `new_root`. 110 | pub fn with_root(&self, new_root: LetterOctave) -> Chord { 111 | Chord { 112 | root: new_root, 113 | quality: self.quality.clone(), 114 | } 115 | } 116 | 117 | /// Returns the `Letter` of the root. 118 | pub fn root_letter(&self) -> Letter { 119 | self.root.letter() 120 | } 121 | 122 | /// Returns a new chord with the same quality but with a different root, 123 | /// determined by `new_root`. The new chord will be voiced in the same 124 | /// octave as the old one. 125 | pub fn with_root_letter(&self, new_root: Letter) -> Chord { 126 | Chord { 127 | root: LetterOctave(new_root, self.root.octave()), 128 | quality: self.quality.clone(), 129 | } 130 | } 131 | 132 | /// Returns a copy of the chord's Quality. 133 | pub fn quality(&self) -> Quality { 134 | self.quality.clone() 135 | } 136 | 137 | /// Returns a new chord with the same root, but a different `quality`. 138 | pub fn with_quality(&self, quality: Quality) -> Chord { 139 | Chord { 140 | root: self.root, 141 | quality: quality, 142 | } 143 | } 144 | 145 | /// Returns all of the notes that make up the chord. 146 | pub fn notes(&self) -> Vec { 147 | self.quality 148 | .clone() 149 | .into_iter() 150 | .map(|(_, i)| transpose(self.root, i)) 151 | .fold(vec![self.root()], |mut ns, n| { 152 | ns.push(n); 153 | ns 154 | }) 155 | } 156 | } 157 | 158 | static mut USE_FLATS: bool = false; 159 | 160 | pub fn set_use_flats(flats: bool) { 161 | unsafe { 162 | USE_FLATS = flats; 163 | } 164 | } 165 | 166 | pub fn get_use_flats() -> bool { 167 | unsafe { USE_FLATS } 168 | } 169 | 170 | pub fn letter_to_string(letter: Letter) -> String { 171 | use pitch_calc::Letter::*; 172 | let flats = get_use_flats(); 173 | let s = match letter { 174 | C => "C", 175 | D => "D", 176 | E => "E", 177 | F => "F", 178 | G => "G", 179 | A => "A", 180 | B => "B", 181 | 182 | Db | Csh => { 183 | if flats { 184 | "Db" 185 | } else { 186 | "C#" 187 | } 188 | } 189 | 190 | Eb | Dsh => { 191 | if flats { 192 | "Eb" 193 | } else { 194 | "D#" 195 | } 196 | } 197 | 198 | Gb | Fsh => { 199 | if flats { 200 | "Gb" 201 | } else { 202 | "F#" 203 | } 204 | } 205 | 206 | Ab | Gsh => { 207 | if flats { 208 | "Ab" 209 | } else { 210 | "G#" 211 | } 212 | } 213 | 214 | Bb | Ash => { 215 | if flats { 216 | "Bb" 217 | } else { 218 | "A#" 219 | } 220 | } 221 | }; 222 | s.to_string() 223 | } 224 | 225 | impl fmt::Display for Chord { 226 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 227 | let notes = self.notes(); 228 | 229 | let output: String = notes 230 | .into_iter() 231 | .map(|t| format!("{}{} ", letter_to_string(t.letter()), t.octave())) 232 | .collect(); 233 | 234 | write!(f, "{}", output.trim()) 235 | } 236 | } 237 | 238 | #[cfg(test)] 239 | mod tests { 240 | use super::*; 241 | 242 | #[allow(unused_imports)] 243 | use pitch_calc::Letter::*; 244 | 245 | #[test] 246 | fn interval_maths() { 247 | use intervals::*; 248 | assert_eq!(C, F + Per5th); 249 | assert_eq!(C, F + 7); 250 | assert_eq!(C, C + Octave); 251 | assert_eq!(C, C + Octave + Octave); 252 | 253 | assert_eq!(C, G - Per5th); 254 | assert_eq!(C, C - Octave); 255 | assert_eq!(C, C - Per4th - Per5th); 256 | assert_eq!(C - Min2nd, C + Maj7th); 257 | } 258 | 259 | #[test] 260 | #[allow(non_snake_case)] 261 | fn chord_maths() { 262 | use degree_intervals::*; 263 | let Cmaj = Chord { 264 | root: LetterOctave(Letter::C, 4), 265 | quality: vec![Maj3rd, Per5th], 266 | }; 267 | 268 | let Gmaj = Chord { 269 | root: LetterOctave(Letter::G, 4), 270 | quality: vec![Maj3rd, Per5th], 271 | }; 272 | 273 | let Amin = Chord { 274 | root: LetterOctave(Letter::A, 4), 275 | quality: vec![Min3rd, Per5th], 276 | }; 277 | 278 | assert_eq!(Gmaj, Cmaj.transposed(intervals::Per5th)); 279 | assert_eq!(Gmaj, Cmaj.with_root(LetterOctave(Letter::G, 4))); 280 | assert_eq!(Gmaj, Cmaj.with_root_letter(Letter::G)); 281 | 282 | let Amin_generated = Cmaj 283 | .transposed(intervals::Maj6th) 284 | .with_quality(vec![Min3rd, Per5th]); 285 | assert_eq!(Amin, Amin_generated); 286 | 287 | let notes_of_c_major: Vec = [Letter::C, Letter::E, Letter::G] 288 | .iter() 289 | .map(|l| LetterOctave(*l, 4)) 290 | .collect(); 291 | assert_eq!(Cmaj.notes(), notes_of_c_major); 292 | } 293 | } 294 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "arrayref" 7 | version = "0.3.5" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "0d382e583f07208808f6b1249e60848879ba3543f57c32277bf52d69c2f0f0ee" 10 | 11 | [[package]] 12 | name = "arrayvec" 13 | version = "0.4.12" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "cd9fd44efafa8690358b7408d253adf110036b88f55672a933f01d616ad9b1b9" 16 | dependencies = [ 17 | "nodrop", 18 | ] 19 | 20 | [[package]] 21 | name = "arrayvec" 22 | version = "0.5.2" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" 25 | 26 | [[package]] 27 | name = "atty" 28 | version = "0.2.14" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" 31 | dependencies = [ 32 | "hermit-abi", 33 | "libc", 34 | "winapi", 35 | ] 36 | 37 | [[package]] 38 | name = "autocfg" 39 | version = "1.0.1" 40 | source = "registry+https://github.com/rust-lang/crates.io-index" 41 | checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" 42 | 43 | [[package]] 44 | name = "backtrace" 45 | version = "0.3.40" 46 | source = "registry+https://github.com/rust-lang/crates.io-index" 47 | checksum = "924c76597f0d9ca25d762c25a4d369d51267536465dc5064bdf0eb073ed477ea" 48 | dependencies = [ 49 | "backtrace-sys", 50 | "cfg-if 0.1.10", 51 | "libc", 52 | "rustc-demangle", 53 | ] 54 | 55 | [[package]] 56 | name = "backtrace-sys" 57 | version = "0.1.32" 58 | source = "registry+https://github.com/rust-lang/crates.io-index" 59 | checksum = "5d6575f128516de27e3ce99689419835fce9643a9b215a14d2b5b685be018491" 60 | dependencies = [ 61 | "cc", 62 | "libc", 63 | ] 64 | 65 | [[package]] 66 | name = "base64" 67 | version = "0.10.1" 68 | source = "registry+https://github.com/rust-lang/crates.io-index" 69 | checksum = "0b25d992356d2eb0ed82172f5248873db5560c4721f564b13cb5193bda5e668e" 70 | dependencies = [ 71 | "byteorder", 72 | ] 73 | 74 | [[package]] 75 | name = "bitflags" 76 | version = "0.7.0" 77 | source = "registry+https://github.com/rust-lang/crates.io-index" 78 | checksum = "aad18937a628ec6abcd26d1489012cc0e18c21798210f491af69ded9b881106d" 79 | 80 | [[package]] 81 | name = "bitflags" 82 | version = "1.2.1" 83 | source = "registry+https://github.com/rust-lang/crates.io-index" 84 | checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" 85 | 86 | [[package]] 87 | name = "bitvec" 88 | version = "0.19.4" 89 | source = "registry+https://github.com/rust-lang/crates.io-index" 90 | checksum = "a7ba35e9565969edb811639dbebfe34edc0368e472c5018474c8eb2543397f81" 91 | dependencies = [ 92 | "funty", 93 | "radium", 94 | "tap", 95 | "wyz", 96 | ] 97 | 98 | [[package]] 99 | name = "blake2b_simd" 100 | version = "0.5.8" 101 | source = "registry+https://github.com/rust-lang/crates.io-index" 102 | checksum = "5850aeee1552f495dd0250014cf64b82b7c8879a89d83b33bbdace2cc4f63182" 103 | dependencies = [ 104 | "arrayref", 105 | "arrayvec 0.4.12", 106 | "constant_time_eq", 107 | ] 108 | 109 | [[package]] 110 | name = "byteorder" 111 | version = "1.3.2" 112 | source = "registry+https://github.com/rust-lang/crates.io-index" 113 | checksum = "a7c3dd8985a7111efc5c80b44e23ecdd8c007de8ade3b96595387e812b957cf5" 114 | 115 | [[package]] 116 | name = "cc" 117 | version = "1.0.46" 118 | source = "registry+https://github.com/rust-lang/crates.io-index" 119 | checksum = "0213d356d3c4ea2c18c40b037c3be23cd639825c18f25ee670ac7813beeef99c" 120 | 121 | [[package]] 122 | name = "cfg-if" 123 | version = "0.1.10" 124 | source = "registry+https://github.com/rust-lang/crates.io-index" 125 | checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" 126 | 127 | [[package]] 128 | name = "cfg-if" 129 | version = "1.0.0" 130 | source = "registry+https://github.com/rust-lang/crates.io-index" 131 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 132 | 133 | [[package]] 134 | name = "clap" 135 | version = "3.0.0-beta.2" 136 | source = "registry+https://github.com/rust-lang/crates.io-index" 137 | checksum = "4bd1061998a501ee7d4b6d449020df3266ca3124b941ec56cf2005c3779ca142" 138 | dependencies = [ 139 | "atty", 140 | "bitflags 1.2.1", 141 | "clap_derive", 142 | "indexmap", 143 | "lazy_static", 144 | "os_str_bytes", 145 | "strsim", 146 | "termcolor", 147 | "textwrap", 148 | "unicode-width", 149 | "vec_map", 150 | ] 151 | 152 | [[package]] 153 | name = "clap_derive" 154 | version = "3.0.0-beta.2" 155 | source = "registry+https://github.com/rust-lang/crates.io-index" 156 | checksum = "370f715b81112975b1b69db93e0b56ea4cd4e5002ac43b2da8474106a54096a1" 157 | dependencies = [ 158 | "heck", 159 | "proc-macro-error", 160 | "proc-macro2", 161 | "quote", 162 | "syn", 163 | ] 164 | 165 | [[package]] 166 | name = "cloudabi" 167 | version = "0.0.3" 168 | source = "registry+https://github.com/rust-lang/crates.io-index" 169 | checksum = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f" 170 | dependencies = [ 171 | "bitflags 1.2.1", 172 | ] 173 | 174 | [[package]] 175 | name = "constant_time_eq" 176 | version = "0.1.4" 177 | source = "registry+https://github.com/rust-lang/crates.io-index" 178 | checksum = "995a44c877f9212528ccc74b21a232f66ad69001e40ede5bcee2ac9ef2657120" 179 | 180 | [[package]] 181 | name = "crossbeam-utils" 182 | version = "0.6.6" 183 | source = "registry+https://github.com/rust-lang/crates.io-index" 184 | checksum = "04973fa96e96579258a5091af6003abde64af786b860f18622b82e026cca60e6" 185 | dependencies = [ 186 | "cfg-if 0.1.10", 187 | "lazy_static", 188 | ] 189 | 190 | [[package]] 191 | name = "dasp" 192 | version = "0.11.0" 193 | source = "registry+https://github.com/rust-lang/crates.io-index" 194 | checksum = "7381b67da416b639690ac77c73b86a7b5e64a29e31d1f75fb3b1102301ef355a" 195 | dependencies = [ 196 | "dasp_envelope", 197 | "dasp_frame", 198 | "dasp_interpolate", 199 | "dasp_peak", 200 | "dasp_ring_buffer", 201 | "dasp_rms", 202 | "dasp_sample", 203 | "dasp_signal", 204 | "dasp_slice", 205 | "dasp_window", 206 | ] 207 | 208 | [[package]] 209 | name = "dasp_envelope" 210 | version = "0.11.0" 211 | source = "registry+https://github.com/rust-lang/crates.io-index" 212 | checksum = "8ec617ce7016f101a87fe85ed44180839744265fae73bb4aa43e7ece1b7668b6" 213 | dependencies = [ 214 | "dasp_frame", 215 | "dasp_peak", 216 | "dasp_ring_buffer", 217 | "dasp_rms", 218 | "dasp_sample", 219 | ] 220 | 221 | [[package]] 222 | name = "dasp_frame" 223 | version = "0.11.0" 224 | source = "registry+https://github.com/rust-lang/crates.io-index" 225 | checksum = "b2a3937f5fe2135702897535c8d4a5553f8b116f76c1529088797f2eee7c5cd6" 226 | dependencies = [ 227 | "dasp_sample", 228 | ] 229 | 230 | [[package]] 231 | name = "dasp_interpolate" 232 | version = "0.11.0" 233 | source = "registry+https://github.com/rust-lang/crates.io-index" 234 | checksum = "7fc975a6563bb7ca7ec0a6c784ead49983a21c24835b0bc96eea11ee407c7486" 235 | dependencies = [ 236 | "dasp_frame", 237 | "dasp_ring_buffer", 238 | "dasp_sample", 239 | ] 240 | 241 | [[package]] 242 | name = "dasp_peak" 243 | version = "0.11.0" 244 | source = "registry+https://github.com/rust-lang/crates.io-index" 245 | checksum = "5cf88559d79c21f3d8523d91250c397f9a15b5fc72fbb3f87fdb0a37b79915bf" 246 | dependencies = [ 247 | "dasp_frame", 248 | "dasp_sample", 249 | ] 250 | 251 | [[package]] 252 | name = "dasp_ring_buffer" 253 | version = "0.11.0" 254 | source = "registry+https://github.com/rust-lang/crates.io-index" 255 | checksum = "07d79e19b89618a543c4adec9c5a347fe378a19041699b3278e616e387511ea1" 256 | 257 | [[package]] 258 | name = "dasp_rms" 259 | version = "0.11.0" 260 | source = "registry+https://github.com/rust-lang/crates.io-index" 261 | checksum = "a6c5dcb30b7e5014486e2822537ea2beae50b19722ffe2ed7549ab03774575aa" 262 | dependencies = [ 263 | "dasp_frame", 264 | "dasp_ring_buffer", 265 | "dasp_sample", 266 | ] 267 | 268 | [[package]] 269 | name = "dasp_sample" 270 | version = "0.11.0" 271 | source = "registry+https://github.com/rust-lang/crates.io-index" 272 | checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f" 273 | 274 | [[package]] 275 | name = "dasp_signal" 276 | version = "0.11.0" 277 | source = "registry+https://github.com/rust-lang/crates.io-index" 278 | checksum = "aa1ab7d01689c6ed4eae3d38fe1cea08cba761573fbd2d592528d55b421077e7" 279 | dependencies = [ 280 | "dasp_envelope", 281 | "dasp_frame", 282 | "dasp_interpolate", 283 | "dasp_peak", 284 | "dasp_ring_buffer", 285 | "dasp_rms", 286 | "dasp_sample", 287 | "dasp_window", 288 | ] 289 | 290 | [[package]] 291 | name = "dasp_slice" 292 | version = "0.11.0" 293 | source = "registry+https://github.com/rust-lang/crates.io-index" 294 | checksum = "4e1c7335d58e7baedafa516cb361360ff38d6f4d3f9d9d5ee2a2fc8e27178fa1" 295 | dependencies = [ 296 | "dasp_frame", 297 | "dasp_sample", 298 | ] 299 | 300 | [[package]] 301 | name = "dasp_window" 302 | version = "0.11.1" 303 | source = "registry+https://github.com/rust-lang/crates.io-index" 304 | checksum = "99ded7b88821d2ce4e8b842c9f1c86ac911891ab89443cc1de750cae764c5076" 305 | dependencies = [ 306 | "dasp_sample", 307 | ] 308 | 309 | [[package]] 310 | name = "diesel" 311 | version = "1.4.3" 312 | source = "registry+https://github.com/rust-lang/crates.io-index" 313 | checksum = "9d7cc03b910de9935007861dce440881f69102aaaedfd4bc5a6f40340ca5840c" 314 | dependencies = [ 315 | "byteorder", 316 | "diesel_derives", 317 | "libsqlite3-sys", 318 | ] 319 | 320 | [[package]] 321 | name = "diesel_derives" 322 | version = "1.4.1" 323 | source = "registry+https://github.com/rust-lang/crates.io-index" 324 | checksum = "45f5098f628d02a7a0f68ddba586fb61e80edec3bdc1be3b921f4ceec60858d3" 325 | dependencies = [ 326 | "proc-macro2", 327 | "quote", 328 | "syn", 329 | ] 330 | 331 | [[package]] 332 | name = "diesel_migrations" 333 | version = "1.4.0" 334 | source = "registry+https://github.com/rust-lang/crates.io-index" 335 | checksum = "bf3cde8413353dc7f5d72fa8ce0b99a560a359d2c5ef1e5817ca731cd9008f4c" 336 | dependencies = [ 337 | "migrations_internals", 338 | "migrations_macros", 339 | ] 340 | 341 | [[package]] 342 | name = "dirs" 343 | version = "2.0.2" 344 | source = "registry+https://github.com/rust-lang/crates.io-index" 345 | checksum = "13aea89a5c93364a98e9b37b2fa237effbb694d5cfe01c5b70941f7eb087d5e3" 346 | dependencies = [ 347 | "cfg-if 0.1.10", 348 | "dirs-sys", 349 | ] 350 | 351 | [[package]] 352 | name = "dirs-sys" 353 | version = "0.3.4" 354 | source = "registry+https://github.com/rust-lang/crates.io-index" 355 | checksum = "afa0b23de8fd801745c471deffa6e12d248f962c9fd4b4c33787b055599bde7b" 356 | dependencies = [ 357 | "cfg-if 0.1.10", 358 | "libc", 359 | "redox_users", 360 | "winapi", 361 | ] 362 | 363 | [[package]] 364 | name = "enum-primitive-derive" 365 | version = "0.2.1" 366 | source = "registry+https://github.com/rust-lang/crates.io-index" 367 | checksum = "5f52288f9a7ebb08959188872b58e7eaa12af9cb47da8e94158e16da7e143340" 368 | dependencies = [ 369 | "num-traits", 370 | "quote", 371 | "syn", 372 | ] 373 | 374 | [[package]] 375 | name = "failure" 376 | version = "0.1.6" 377 | source = "registry+https://github.com/rust-lang/crates.io-index" 378 | checksum = "f8273f13c977665c5db7eb2b99ae520952fe5ac831ae4cd09d80c4c7042b5ed9" 379 | dependencies = [ 380 | "backtrace", 381 | "failure_derive", 382 | ] 383 | 384 | [[package]] 385 | name = "failure_derive" 386 | version = "0.1.6" 387 | source = "registry+https://github.com/rust-lang/crates.io-index" 388 | checksum = "0bc225b78e0391e4b8683440bf2e63c2deeeb2ce5189eab46e2b68c6d3725d08" 389 | dependencies = [ 390 | "proc-macro2", 391 | "quote", 392 | "syn", 393 | "synstructure", 394 | ] 395 | 396 | [[package]] 397 | name = "find_folder" 398 | version = "0.3.0" 399 | source = "registry+https://github.com/rust-lang/crates.io-index" 400 | checksum = "9f6d018fb95a0b59f854aed68ecd96ce2b80af7911b92b1fed3c4b1fa516b91b" 401 | 402 | [[package]] 403 | name = "fuchsia-cprng" 404 | version = "0.1.1" 405 | source = "registry+https://github.com/rust-lang/crates.io-index" 406 | checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" 407 | 408 | [[package]] 409 | name = "funty" 410 | version = "1.1.0" 411 | source = "registry+https://github.com/rust-lang/crates.io-index" 412 | checksum = "fed34cd105917e91daa4da6b3728c47b068749d6a62c59811f06ed2ac71d9da7" 413 | 414 | [[package]] 415 | name = "harmony_explorer" 416 | version = "0.1.0" 417 | dependencies = [ 418 | "clap", 419 | "dasp", 420 | "diesel", 421 | "diesel_migrations", 422 | "enum-primitive-derive", 423 | "find_folder", 424 | "lexical-core", 425 | "libsqlite3-sys", 426 | "nom", 427 | "num 0.4.0", 428 | "num-traits", 429 | "pitch_calc", 430 | "portaudio", 431 | "rustyline", 432 | ] 433 | 434 | [[package]] 435 | name = "hashbrown" 436 | version = "0.11.2" 437 | source = "registry+https://github.com/rust-lang/crates.io-index" 438 | checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" 439 | 440 | [[package]] 441 | name = "heck" 442 | version = "0.3.3" 443 | source = "registry+https://github.com/rust-lang/crates.io-index" 444 | checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" 445 | dependencies = [ 446 | "unicode-segmentation", 447 | ] 448 | 449 | [[package]] 450 | name = "hermit-abi" 451 | version = "0.1.19" 452 | source = "registry+https://github.com/rust-lang/crates.io-index" 453 | checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" 454 | dependencies = [ 455 | "libc", 456 | ] 457 | 458 | [[package]] 459 | name = "indexmap" 460 | version = "1.7.0" 461 | source = "registry+https://github.com/rust-lang/crates.io-index" 462 | checksum = "bc633605454125dec4b66843673f01c7df2b89479b32e0ed634e43a91cff62a5" 463 | dependencies = [ 464 | "autocfg", 465 | "hashbrown", 466 | ] 467 | 468 | [[package]] 469 | name = "lazy_static" 470 | version = "1.4.0" 471 | source = "registry+https://github.com/rust-lang/crates.io-index" 472 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 473 | 474 | [[package]] 475 | name = "lexical-core" 476 | version = "0.7.6" 477 | source = "registry+https://github.com/rust-lang/crates.io-index" 478 | checksum = "6607c62aa161d23d17a9072cc5da0be67cdfc89d3afb1e8d9c842bebc2525ffe" 479 | dependencies = [ 480 | "arrayvec 0.5.2", 481 | "bitflags 1.2.1", 482 | "cfg-if 1.0.0", 483 | "ryu", 484 | "static_assertions", 485 | ] 486 | 487 | [[package]] 488 | name = "libc" 489 | version = "0.2.65" 490 | source = "registry+https://github.com/rust-lang/crates.io-index" 491 | checksum = "1a31a0627fdf1f6a39ec0dd577e101440b7db22672c0901fe00a9a6fbb5c24e8" 492 | 493 | [[package]] 494 | name = "libsqlite3-sys" 495 | version = "0.9.1" 496 | source = "registry+https://github.com/rust-lang/crates.io-index" 497 | checksum = "0e9eb7b8e152b6a01be6a4a2917248381875758250dc3df5d46caf9250341dda" 498 | dependencies = [ 499 | "cc", 500 | "pkg-config", 501 | "vcpkg", 502 | ] 503 | 504 | [[package]] 505 | name = "log" 506 | version = "0.4.8" 507 | source = "registry+https://github.com/rust-lang/crates.io-index" 508 | checksum = "14b6052be84e6b71ab17edffc2eeabf5c2c3ae1fdb464aae35ac50c67a44e1f7" 509 | dependencies = [ 510 | "cfg-if 0.1.10", 511 | ] 512 | 513 | [[package]] 514 | name = "memchr" 515 | version = "2.2.1" 516 | source = "registry+https://github.com/rust-lang/crates.io-index" 517 | checksum = "88579771288728879b57485cc7d6b07d648c9f0141eb955f8ab7f9d45394468e" 518 | 519 | [[package]] 520 | name = "migrations_internals" 521 | version = "1.4.0" 522 | source = "registry+https://github.com/rust-lang/crates.io-index" 523 | checksum = "8089920229070f914b9ce9b07ef60e175b2b9bc2d35c3edd8bf4433604e863b9" 524 | dependencies = [ 525 | "diesel", 526 | ] 527 | 528 | [[package]] 529 | name = "migrations_macros" 530 | version = "1.4.1" 531 | source = "registry+https://github.com/rust-lang/crates.io-index" 532 | checksum = "719ef0bc7f531428764c9b70661c14abd50a7f3d21f355752d9985aa21251c9e" 533 | dependencies = [ 534 | "migrations_internals", 535 | "proc-macro2", 536 | "quote", 537 | "syn", 538 | ] 539 | 540 | [[package]] 541 | name = "nix" 542 | version = "0.14.1" 543 | source = "registry+https://github.com/rust-lang/crates.io-index" 544 | checksum = "6c722bee1037d430d0f8e687bbdbf222f27cc6e4e68d5caf630857bb2b6dbdce" 545 | dependencies = [ 546 | "bitflags 1.2.1", 547 | "cc", 548 | "cfg-if 0.1.10", 549 | "libc", 550 | "void", 551 | ] 552 | 553 | [[package]] 554 | name = "nodrop" 555 | version = "0.1.14" 556 | source = "registry+https://github.com/rust-lang/crates.io-index" 557 | checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" 558 | 559 | [[package]] 560 | name = "nom" 561 | version = "6.2.1" 562 | source = "registry+https://github.com/rust-lang/crates.io-index" 563 | checksum = "9c5c51b9083a3c620fa67a2a635d1ce7d95b897e957d6b28ff9a5da960a103a6" 564 | dependencies = [ 565 | "bitvec", 566 | "funty", 567 | "lexical-core", 568 | "memchr", 569 | "version_check", 570 | ] 571 | 572 | [[package]] 573 | name = "num" 574 | version = "0.1.42" 575 | source = "registry+https://github.com/rust-lang/crates.io-index" 576 | checksum = "4703ad64153382334aa8db57c637364c322d3372e097840c72000dabdcf6156e" 577 | dependencies = [ 578 | "num-bigint 0.1.44", 579 | "num-complex 0.1.43", 580 | "num-integer", 581 | "num-iter", 582 | "num-rational 0.1.42", 583 | "num-traits", 584 | ] 585 | 586 | [[package]] 587 | name = "num" 588 | version = "0.4.0" 589 | source = "registry+https://github.com/rust-lang/crates.io-index" 590 | checksum = "43db66d1170d347f9a065114077f7dccb00c1b9478c89384490a3425279a4606" 591 | dependencies = [ 592 | "num-bigint 0.4.0", 593 | "num-complex 0.4.0", 594 | "num-integer", 595 | "num-iter", 596 | "num-rational 0.4.0", 597 | "num-traits", 598 | ] 599 | 600 | [[package]] 601 | name = "num-bigint" 602 | version = "0.1.44" 603 | source = "registry+https://github.com/rust-lang/crates.io-index" 604 | checksum = "e63899ad0da84ce718c14936262a41cee2c79c981fc0a0e7c7beb47d5a07e8c1" 605 | dependencies = [ 606 | "num-integer", 607 | "num-traits", 608 | "rand 0.4.6", 609 | "rustc-serialize", 610 | ] 611 | 612 | [[package]] 613 | name = "num-bigint" 614 | version = "0.4.0" 615 | source = "registry+https://github.com/rust-lang/crates.io-index" 616 | checksum = "4e0d047c1062aa51e256408c560894e5251f08925980e53cf1aa5bd00eec6512" 617 | dependencies = [ 618 | "autocfg", 619 | "num-integer", 620 | "num-traits", 621 | ] 622 | 623 | [[package]] 624 | name = "num-complex" 625 | version = "0.1.43" 626 | source = "registry+https://github.com/rust-lang/crates.io-index" 627 | checksum = "b288631d7878aaf59442cffd36910ea604ecd7745c36054328595114001c9656" 628 | dependencies = [ 629 | "num-traits", 630 | "rustc-serialize", 631 | ] 632 | 633 | [[package]] 634 | name = "num-complex" 635 | version = "0.4.0" 636 | source = "registry+https://github.com/rust-lang/crates.io-index" 637 | checksum = "26873667bbbb7c5182d4a37c1add32cdf09f841af72da53318fdb81543c15085" 638 | dependencies = [ 639 | "num-traits", 640 | ] 641 | 642 | [[package]] 643 | name = "num-integer" 644 | version = "0.1.44" 645 | source = "registry+https://github.com/rust-lang/crates.io-index" 646 | checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" 647 | dependencies = [ 648 | "autocfg", 649 | "num-traits", 650 | ] 651 | 652 | [[package]] 653 | name = "num-iter" 654 | version = "0.1.42" 655 | source = "registry+https://github.com/rust-lang/crates.io-index" 656 | checksum = "b2021c8337a54d21aca0d59a92577a029af9431cb59b909b03252b9c164fad59" 657 | dependencies = [ 658 | "autocfg", 659 | "num-integer", 660 | "num-traits", 661 | ] 662 | 663 | [[package]] 664 | name = "num-rational" 665 | version = "0.1.42" 666 | source = "registry+https://github.com/rust-lang/crates.io-index" 667 | checksum = "ee314c74bd753fc86b4780aa9475da469155f3848473a261d2d18e35245a784e" 668 | dependencies = [ 669 | "num-bigint 0.1.44", 670 | "num-integer", 671 | "num-traits", 672 | "rustc-serialize", 673 | ] 674 | 675 | [[package]] 676 | name = "num-rational" 677 | version = "0.4.0" 678 | source = "registry+https://github.com/rust-lang/crates.io-index" 679 | checksum = "d41702bd167c2df5520b384281bc111a4b5efcf7fbc4c9c222c815b07e0a6a6a" 680 | dependencies = [ 681 | "autocfg", 682 | "num-bigint 0.4.0", 683 | "num-integer", 684 | "num-traits", 685 | ] 686 | 687 | [[package]] 688 | name = "num-traits" 689 | version = "0.2.14" 690 | source = "registry+https://github.com/rust-lang/crates.io-index" 691 | checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" 692 | dependencies = [ 693 | "autocfg", 694 | ] 695 | 696 | [[package]] 697 | name = "os_str_bytes" 698 | version = "2.4.0" 699 | source = "registry+https://github.com/rust-lang/crates.io-index" 700 | checksum = "afb2e1c3ee07430c2cf76151675e583e0f19985fa6efae47d6848a3e2c824f85" 701 | 702 | [[package]] 703 | name = "pitch_calc" 704 | version = "0.12.0" 705 | source = "registry+https://github.com/rust-lang/crates.io-index" 706 | checksum = "387005d7ff9e9970f954ffd33e258f9b755d5f27f11a4b57df3e5c6eab5a46f8" 707 | dependencies = [ 708 | "num 0.1.42", 709 | "rand 0.3.23", 710 | ] 711 | 712 | [[package]] 713 | name = "pkg-config" 714 | version = "0.3.16" 715 | source = "registry+https://github.com/rust-lang/crates.io-index" 716 | checksum = "72d5370d90f49f70bd033c3d75e87fc529fbfff9d6f7cccef07d6170079d91ea" 717 | 718 | [[package]] 719 | name = "portaudio" 720 | version = "0.7.0" 721 | source = "registry+https://github.com/rust-lang/crates.io-index" 722 | checksum = "0d289315f6155a7608b6d8757786c79ed2243afeab8a5eda8989effda3fdc5c3" 723 | dependencies = [ 724 | "bitflags 0.7.0", 725 | "libc", 726 | "num 0.1.42", 727 | "pkg-config", 728 | ] 729 | 730 | [[package]] 731 | name = "proc-macro-error" 732 | version = "1.0.4" 733 | source = "registry+https://github.com/rust-lang/crates.io-index" 734 | checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" 735 | dependencies = [ 736 | "proc-macro-error-attr", 737 | "proc-macro2", 738 | "quote", 739 | "syn", 740 | "version_check", 741 | ] 742 | 743 | [[package]] 744 | name = "proc-macro-error-attr" 745 | version = "1.0.4" 746 | source = "registry+https://github.com/rust-lang/crates.io-index" 747 | checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" 748 | dependencies = [ 749 | "proc-macro2", 750 | "quote", 751 | "version_check", 752 | ] 753 | 754 | [[package]] 755 | name = "proc-macro2" 756 | version = "1.0.6" 757 | source = "registry+https://github.com/rust-lang/crates.io-index" 758 | checksum = "9c9e470a8dc4aeae2dee2f335e8f533e2d4b347e1434e5671afc49b054592f27" 759 | dependencies = [ 760 | "unicode-xid", 761 | ] 762 | 763 | [[package]] 764 | name = "quote" 765 | version = "1.0.2" 766 | source = "registry+https://github.com/rust-lang/crates.io-index" 767 | checksum = "053a8c8bcc71fcce321828dc897a98ab9760bef03a4fc36693c231e5b3216cfe" 768 | dependencies = [ 769 | "proc-macro2", 770 | ] 771 | 772 | [[package]] 773 | name = "radium" 774 | version = "0.5.3" 775 | source = "registry+https://github.com/rust-lang/crates.io-index" 776 | checksum = "941ba9d78d8e2f7ce474c015eea4d9c6d25b6a3327f9832ee29a4de27f91bbb8" 777 | 778 | [[package]] 779 | name = "rand" 780 | version = "0.3.23" 781 | source = "registry+https://github.com/rust-lang/crates.io-index" 782 | checksum = "64ac302d8f83c0c1974bf758f6b041c6c8ada916fbb44a609158ca8b064cc76c" 783 | dependencies = [ 784 | "libc", 785 | "rand 0.4.6", 786 | ] 787 | 788 | [[package]] 789 | name = "rand" 790 | version = "0.4.6" 791 | source = "registry+https://github.com/rust-lang/crates.io-index" 792 | checksum = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293" 793 | dependencies = [ 794 | "fuchsia-cprng", 795 | "libc", 796 | "rand_core 0.3.1", 797 | "rdrand", 798 | "winapi", 799 | ] 800 | 801 | [[package]] 802 | name = "rand_core" 803 | version = "0.3.1" 804 | source = "registry+https://github.com/rust-lang/crates.io-index" 805 | checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" 806 | dependencies = [ 807 | "rand_core 0.4.2", 808 | ] 809 | 810 | [[package]] 811 | name = "rand_core" 812 | version = "0.4.2" 813 | source = "registry+https://github.com/rust-lang/crates.io-index" 814 | checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" 815 | 816 | [[package]] 817 | name = "rand_os" 818 | version = "0.1.3" 819 | source = "registry+https://github.com/rust-lang/crates.io-index" 820 | checksum = "7b75f676a1e053fc562eafbb47838d67c84801e38fc1ba459e8f180deabd5071" 821 | dependencies = [ 822 | "cloudabi", 823 | "fuchsia-cprng", 824 | "libc", 825 | "rand_core 0.4.2", 826 | "rdrand", 827 | "winapi", 828 | ] 829 | 830 | [[package]] 831 | name = "rdrand" 832 | version = "0.4.0" 833 | source = "registry+https://github.com/rust-lang/crates.io-index" 834 | checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" 835 | dependencies = [ 836 | "rand_core 0.3.1", 837 | ] 838 | 839 | [[package]] 840 | name = "redox_syscall" 841 | version = "0.1.56" 842 | source = "registry+https://github.com/rust-lang/crates.io-index" 843 | checksum = "2439c63f3f6139d1b57529d16bc3b8bb855230c8efcc5d3a896c8bea7c3b1e84" 844 | 845 | [[package]] 846 | name = "redox_users" 847 | version = "0.3.1" 848 | source = "registry+https://github.com/rust-lang/crates.io-index" 849 | checksum = "4ecedbca3bf205f8d8f5c2b44d83cd0690e39ee84b951ed649e9f1841132b66d" 850 | dependencies = [ 851 | "failure", 852 | "rand_os", 853 | "redox_syscall", 854 | "rust-argon2", 855 | ] 856 | 857 | [[package]] 858 | name = "rust-argon2" 859 | version = "0.5.1" 860 | source = "registry+https://github.com/rust-lang/crates.io-index" 861 | checksum = "4ca4eaef519b494d1f2848fc602d18816fed808a981aedf4f1f00ceb7c9d32cf" 862 | dependencies = [ 863 | "base64", 864 | "blake2b_simd", 865 | "crossbeam-utils", 866 | ] 867 | 868 | [[package]] 869 | name = "rustc-demangle" 870 | version = "0.1.16" 871 | source = "registry+https://github.com/rust-lang/crates.io-index" 872 | checksum = "4c691c0e608126e00913e33f0ccf3727d5fc84573623b8d65b2df340b5201783" 873 | 874 | [[package]] 875 | name = "rustc-serialize" 876 | version = "0.3.24" 877 | source = "registry+https://github.com/rust-lang/crates.io-index" 878 | checksum = "dcf128d1287d2ea9d80910b5f1120d0b8eede3fbf1abe91c40d39ea7d51e6fda" 879 | 880 | [[package]] 881 | name = "rustyline" 882 | version = "5.0.3" 883 | source = "registry+https://github.com/rust-lang/crates.io-index" 884 | checksum = "4795e277e6e57dec9df62b515cd4991371daa80e8dc8d80d596e58722b89c417" 885 | dependencies = [ 886 | "dirs", 887 | "libc", 888 | "log", 889 | "memchr", 890 | "nix", 891 | "unicode-segmentation", 892 | "unicode-width", 893 | "utf8parse", 894 | "winapi", 895 | ] 896 | 897 | [[package]] 898 | name = "ryu" 899 | version = "1.0.2" 900 | source = "registry+https://github.com/rust-lang/crates.io-index" 901 | checksum = "bfa8506c1de11c9c4e4c38863ccbe02a305c8188e85a05a784c9e11e1c3910c8" 902 | 903 | [[package]] 904 | name = "static_assertions" 905 | version = "1.1.0" 906 | source = "registry+https://github.com/rust-lang/crates.io-index" 907 | checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" 908 | 909 | [[package]] 910 | name = "strsim" 911 | version = "0.10.0" 912 | source = "registry+https://github.com/rust-lang/crates.io-index" 913 | checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" 914 | 915 | [[package]] 916 | name = "syn" 917 | version = "1.0.5" 918 | source = "registry+https://github.com/rust-lang/crates.io-index" 919 | checksum = "66850e97125af79138385e9b88339cbcd037e3f28ceab8c5ad98e64f0f1f80bf" 920 | dependencies = [ 921 | "proc-macro2", 922 | "quote", 923 | "unicode-xid", 924 | ] 925 | 926 | [[package]] 927 | name = "synstructure" 928 | version = "0.12.1" 929 | source = "registry+https://github.com/rust-lang/crates.io-index" 930 | checksum = "3f085a5855930c0441ca1288cf044ea4aecf4f43a91668abdb870b4ba546a203" 931 | dependencies = [ 932 | "proc-macro2", 933 | "quote", 934 | "syn", 935 | "unicode-xid", 936 | ] 937 | 938 | [[package]] 939 | name = "tap" 940 | version = "1.0.0" 941 | source = "registry+https://github.com/rust-lang/crates.io-index" 942 | checksum = "36474e732d1affd3a6ed582781b3683df3d0563714c59c39591e8ff707cf078e" 943 | 944 | [[package]] 945 | name = "termcolor" 946 | version = "1.1.2" 947 | source = "registry+https://github.com/rust-lang/crates.io-index" 948 | checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4" 949 | dependencies = [ 950 | "winapi-util", 951 | ] 952 | 953 | [[package]] 954 | name = "textwrap" 955 | version = "0.12.1" 956 | source = "registry+https://github.com/rust-lang/crates.io-index" 957 | checksum = "203008d98caf094106cfaba70acfed15e18ed3ddb7d94e49baec153a2b462789" 958 | dependencies = [ 959 | "unicode-width", 960 | ] 961 | 962 | [[package]] 963 | name = "unicode-segmentation" 964 | version = "1.3.0" 965 | source = "registry+https://github.com/rust-lang/crates.io-index" 966 | checksum = "1967f4cdfc355b37fd76d2a954fb2ed3871034eb4f26d60537d88795cfc332a9" 967 | 968 | [[package]] 969 | name = "unicode-width" 970 | version = "0.1.6" 971 | source = "registry+https://github.com/rust-lang/crates.io-index" 972 | checksum = "7007dbd421b92cc6e28410fe7362e2e0a2503394908f417b68ec8d1c364c4e20" 973 | 974 | [[package]] 975 | name = "unicode-xid" 976 | version = "0.2.0" 977 | source = "registry+https://github.com/rust-lang/crates.io-index" 978 | checksum = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c" 979 | 980 | [[package]] 981 | name = "utf8parse" 982 | version = "0.1.1" 983 | source = "registry+https://github.com/rust-lang/crates.io-index" 984 | checksum = "8772a4ccbb4e89959023bc5b7cb8623a795caa7092d99f3aa9501b9484d4557d" 985 | 986 | [[package]] 987 | name = "vcpkg" 988 | version = "0.2.7" 989 | source = "registry+https://github.com/rust-lang/crates.io-index" 990 | checksum = "33dd455d0f96e90a75803cfeb7f948768c08d70a6de9a8d2362461935698bf95" 991 | 992 | [[package]] 993 | name = "vec_map" 994 | version = "0.8.2" 995 | source = "registry+https://github.com/rust-lang/crates.io-index" 996 | checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" 997 | 998 | [[package]] 999 | name = "version_check" 1000 | version = "0.9.2" 1001 | source = "registry+https://github.com/rust-lang/crates.io-index" 1002 | checksum = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed" 1003 | 1004 | [[package]] 1005 | name = "void" 1006 | version = "1.0.2" 1007 | source = "registry+https://github.com/rust-lang/crates.io-index" 1008 | checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" 1009 | 1010 | [[package]] 1011 | name = "winapi" 1012 | version = "0.3.8" 1013 | source = "registry+https://github.com/rust-lang/crates.io-index" 1014 | checksum = "8093091eeb260906a183e6ae1abdba2ef5ef2257a21801128899c3fc699229c6" 1015 | dependencies = [ 1016 | "winapi-i686-pc-windows-gnu", 1017 | "winapi-x86_64-pc-windows-gnu", 1018 | ] 1019 | 1020 | [[package]] 1021 | name = "winapi-i686-pc-windows-gnu" 1022 | version = "0.4.0" 1023 | source = "registry+https://github.com/rust-lang/crates.io-index" 1024 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 1025 | 1026 | [[package]] 1027 | name = "winapi-util" 1028 | version = "0.1.5" 1029 | source = "registry+https://github.com/rust-lang/crates.io-index" 1030 | checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" 1031 | dependencies = [ 1032 | "winapi", 1033 | ] 1034 | 1035 | [[package]] 1036 | name = "winapi-x86_64-pc-windows-gnu" 1037 | version = "0.4.0" 1038 | source = "registry+https://github.com/rust-lang/crates.io-index" 1039 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 1040 | 1041 | [[package]] 1042 | name = "wyz" 1043 | version = "0.2.0" 1044 | source = "registry+https://github.com/rust-lang/crates.io-index" 1045 | checksum = "85e60b0d1b5f99db2556934e21937020776a5d31520bf169e851ac44e6420214" 1046 | --------------------------------------------------------------------------------