├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── shard.yml ├── spec ├── key_spec.cr ├── medley_spec.cr ├── note_spec.cr ├── scale_spec.cr └── spec_helper.cr └── src ├── medley.cr └── medley ├── errors.cr ├── key.cr ├── note.cr ├── scale.cr └── version.cr /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /doc/ 3 | /libs/ 4 | /.crystal/ 5 | /.shards/ 6 | 7 | 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: crystal 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Jeremy Woertink 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # medley 2 | 3 | A mixture of music related methods. 4 | 5 | ## Installation 6 | 7 | Install locally 8 | 9 | * `git clone git@github.com:jwoertink/medley.git` 10 | * `cd medley/` 11 | * `icr -r ./src/medley.cr` 12 | 13 | or add as a dependecy to your project 14 | 15 | ```yaml 16 | dependencies: 17 | medley: 18 | github: jwoertink/medley 19 | branch: master 20 | ``` 21 | 22 | ## Usage 23 | 24 | ```crystal 25 | require "medley" 26 | 27 | # Play with a note 28 | note = Medley::Note.new("G") 29 | puts note.halfstep_up # => "G#" 30 | puts note.wholestep_up # => "A" 31 | puts note.sharp? #=> false 32 | puts note.natural? #=> true 33 | puts note.next_root #=> "A" 34 | puts note.halfstep_down #=> "Gb" 35 | puts note.name #=> "G" 36 | 37 | other_note = Medley::Note.new("G#") 38 | other_note.root_matches?(note) #=> true 39 | 40 | # Play with a scale 41 | scale = Medley::Scale.new("Cmaj") 42 | scale.notes #=> ["C", "D", "E", "F", "G", "A", "B", "C"] 43 | 44 | # Get the flats or sharps from a key of a scale 45 | scale = Medley::Scale.new("Bbmaj") 46 | scale.key.flats #=> ["Bb", "Eb"] 47 | 48 | # Learn note patterns from a scale 49 | scale = Medley::Scale.new("Cmaj") 50 | scale.pattern(1, 6, 4, 5) #=> ["C", "A", "F", "G"] 51 | ``` 52 | 53 | ## Development 54 | 55 | * `crystal spec` 56 | 57 | ## Contributing 58 | 59 | 1. Fork it ( https://github.com/jwoertink/medley/fork ) 60 | 2. Create your feature branch (git checkout -b my-new-feature) 61 | 3. Commit your changes (git commit -am 'Add some feature') 62 | 4. Push to the branch (git push origin my-new-feature) 63 | 5. Create a new Pull Request 64 | 65 | ## Contributors 66 | 67 | - [jwoertink](https://github.com/jwoertink) Jeremy Woertink - creator, maintainer 68 | -------------------------------------------------------------------------------- /shard.yml: -------------------------------------------------------------------------------- 1 | name: medley 2 | version: 0.4.0 3 | 4 | authors: 5 | - Jeremy Woertink 6 | 7 | license: MIT 8 | -------------------------------------------------------------------------------- /spec/key_spec.cr: -------------------------------------------------------------------------------- 1 | require "./spec_helper" 2 | 3 | describe Medley::Key do 4 | 5 | describe ".flats" do 6 | it "returns an array with 3 flat notes" do 7 | scale = Medley::Scale.new("Ebmaj") 8 | key = Medley::Key.new(scale) 9 | key.flats.size.should eq 3 10 | key.flats.should eq(["Eb", "Ab", "Bb"]) 11 | key.sharps.empty?.should eq true 12 | end 13 | 14 | it "returns an array with 2 sharp notes" do 15 | scale = Medley::Scale.new("Bmin") 16 | key = Medley::Key.new(scale) 17 | key.sharps.size.should eq 2 18 | key.sharps.should eq(["C#", "F#"]) 19 | key.flats.empty?.should eq true 20 | end 21 | end 22 | 23 | describe ".name" do 24 | it "returns the Gmaj" do 25 | scale = Medley::Scale.new("Gmaj") 26 | key = Medley::Key.new(scale) 27 | key.name.should eq "Gmaj" 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /spec/medley_spec.cr: -------------------------------------------------------------------------------- 1 | require "./spec_helper" 2 | 3 | describe Medley do 4 | end 5 | -------------------------------------------------------------------------------- /spec/note_spec.cr: -------------------------------------------------------------------------------- 1 | require "./spec_helper" 2 | 3 | describe Medley::Note do 4 | 5 | describe ".halfstep_up" do 6 | it "returns G# when given G" do 7 | note = Medley::Note.new("G") 8 | note.halfstep_up.should eq "G#" 9 | end 10 | 11 | it "returns B# when given B" do 12 | note = Medley::Note.new("B") 13 | note.halfstep_up.should eq "B#" 14 | end 15 | 16 | it "returns D## when given D#" do 17 | note = Medley::Note.new("D#") 18 | note.halfstep_up.should eq "D##" 19 | end 20 | 21 | it "returns B when given Bb" do 22 | note = Medley::Note.new("Bb") 23 | note.halfstep_up.should eq "B" 24 | end 25 | 26 | it "returns F## when given E##" do 27 | note = Medley::Note.new("E##") 28 | note.halfstep_up.should eq "F##" 29 | end 30 | end 31 | 32 | describe ".halfstep_down" do 33 | it "returns G when given G#" do 34 | note = Medley::Note.new("G#") 35 | note.halfstep_down.should eq "G" 36 | end 37 | 38 | it "returns B when given B#" do 39 | note = Medley::Note.new("B#") 40 | note.halfstep_down.should eq "B" 41 | end 42 | 43 | it "returns Dbb when given Db" do 44 | note = Medley::Note.new("Db") 45 | note.halfstep_down.should eq "Dbb" 46 | end 47 | 48 | it "returns Cbb when given Cb" do 49 | note = Medley::Note.new("Cb") 50 | note.halfstep_down.should eq "Cbb" 51 | end 52 | 53 | it "returns Ebb when given Fbb" do 54 | note = Medley::Note.new("Fbb") 55 | note.halfstep_down.should eq "Ebb" 56 | end 57 | end 58 | 59 | describe ".wholestep_up" do 60 | it "returns E when given D" do 61 | note = Medley::Note.new("D") 62 | note.wholestep_up.should eq "E" 63 | end 64 | 65 | it "returns G when given F" do 66 | note = Medley::Note.new("F") 67 | note.wholestep_up.should eq "G" 68 | end 69 | 70 | it "returns D# when given C#" do 71 | note = Medley::Note.new("C#") 72 | note.wholestep_up.should eq "D#" 73 | end 74 | 75 | it "returns Bb when given Ab" do 76 | note = Medley::Note.new("Ab") 77 | note.wholestep_up.should eq "Bb" 78 | end 79 | 80 | it "returns C when given Bb" do 81 | note = Medley::Note.new("Bb") 82 | note.wholestep_up.should eq "C" 83 | end 84 | 85 | it "returns Dbb when given Cbb" do 86 | note = Medley::Note.new("Cbb") 87 | note.wholestep_up.should eq "Dbb" 88 | end 89 | end 90 | 91 | describe ".next_root" do 92 | it "returns C when given B" do 93 | note = Medley::Note.new("B") 94 | note.next_root.should eq "C" 95 | end 96 | 97 | it "returns F when given E" do 98 | note = Medley::Note.new("E") 99 | note.next_root.should eq "F" 100 | end 101 | end 102 | 103 | end 104 | -------------------------------------------------------------------------------- /spec/scale_spec.cr: -------------------------------------------------------------------------------- 1 | require "./spec_helper" 2 | 3 | describe Medley::Scale do 4 | 5 | describe ".notes" do 6 | it "returns an array of letters in C major scale order" do 7 | scale = Medley::Scale.new("Cmaj") 8 | scale.notes.should eq(["C", "D", "E", "F", "G", "A", "B", "C"]) 9 | end 10 | 11 | it "returns an array of letters in F major scale order" do 12 | scale = Medley::Scale.new("Fmaj") 13 | scale.notes.should eq(["F", "G", "A", "Bb", "C", "D", "E", "F"]) 14 | end 15 | 16 | it "returns an array of letters in D Major scale order" do 17 | scale = Medley::Scale.new("Dmaj") 18 | scale.notes.should eq(["D", "E", "F#", "G", "A", "B", "C#", "D"]) 19 | end 20 | 21 | it "returns an array of letters Bb Major scale order" do 22 | scale = Medley::Scale.new("Bbmaj") 23 | scale.notes.should eq(["Bb", "C", "D", "Eb", "F", "G", "A", "Bb"]) 24 | end 25 | 26 | it "returns an array of letters in the C minor scale order" do 27 | scale = Medley::Scale.new("Cmin") 28 | scale.notes.should eq(["C", "D", "Eb", "F", "G", "Ab", "Bb", "C"]) 29 | end 30 | end 31 | 32 | describe ".key" do 33 | it "returns the key with 2 flats" do 34 | scale = Medley::Scale.new("Bbmaj") 35 | scale.key.flats.size.should eq 2 36 | end 37 | end 38 | 39 | describe ".pattern" do 40 | it "returns an array of notes in the order of the pattern" do 41 | scale = Medley::Scale.new("Cmaj") 42 | scale.pattern(1, 6, 4, 5).should eq ["C", "A", "F", "G"] 43 | end 44 | it "raises an error when you pass 0 in the pattern" do 45 | scale = Medley::Scale.new("Cmaj") 46 | expect_raises(Medley::InvalidPatternError, "Note arrays are not 0 based index") do 47 | scale.pattern(0, 1) 48 | end 49 | end 50 | it "wraps around if the value is greater than 7" do 51 | scale = Medley::Scale.new("Cmaj") 52 | scale.pattern(8, 12, 4, 10).should eq ["C", "G", "F", "E"] 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /spec/spec_helper.cr: -------------------------------------------------------------------------------- 1 | require "spec" 2 | require "../src/medley" 3 | require "../src/medley/note" 4 | -------------------------------------------------------------------------------- /src/medley.cr: -------------------------------------------------------------------------------- 1 | require "./medley/*" 2 | 3 | module Medley 4 | end 5 | -------------------------------------------------------------------------------- /src/medley/errors.cr: -------------------------------------------------------------------------------- 1 | module Medley 2 | class InvalidPatternError < Exception 3 | def initialize(message) 4 | super(message) 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /src/medley/key.cr: -------------------------------------------------------------------------------- 1 | module Medley 2 | class Key 3 | 4 | def initialize(scale : Scale) 5 | @scale = scale 6 | end 7 | 8 | # returns an array of flat notes in a key 9 | def flats 10 | @scale.notes.select { |n| 11 | note = Note.new(n) 12 | note.flat? || note.double_flat? 13 | }.uniq 14 | end 15 | 16 | # returns an array of the sharp notes in a key 17 | def sharps 18 | @scale.notes.select { |n| 19 | note = Note.new(n) 20 | note.sharp? || note.double_sharp? 21 | }.uniq 22 | end 23 | 24 | def name 25 | @scale.name 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /src/medley/note.cr: -------------------------------------------------------------------------------- 1 | module Medley 2 | class Note 3 | NOTE_NAMES = %w(A B C D E F G) 4 | ALIASES = {"A##": "B", "B##": "C#", "C##": "D", "D##": "E", 5 | "E##": "F#", "F##": "G", "G##": "A", "Abb": "G", 6 | "Bbb": "A", "Cbb": "Bb", "Dbb": "C", "Ebb": "D", 7 | "Fbb": "Eb", "Gbb": "F", "A#": "Bb", "B#": "C", 8 | "C#": "Db", "D#": "Eb", "E#": "F", "F#": "G", 9 | "G#": "Ab"} 10 | 11 | def initialize(current_note : String) 12 | @current_note = current_note 13 | end 14 | 15 | # true if it's a valid note letter with no modifiers 16 | def natural? 17 | !!name.match(/\A[A-G]\Z/) 18 | end 19 | 20 | # true if it's a valid note letter with b 21 | def flat? 22 | !!name.match(/\A[A-G]b\Z/) 23 | end 24 | 25 | # true if it's a valid note letter with bb 26 | def double_flat? 27 | !!name.match(/\A[A-G]bb\Z/) 28 | end 29 | 30 | # true if it's a valid note letter with # 31 | def sharp? 32 | !!name.match(/\A[A-G]#\Z/) 33 | end 34 | 35 | # true if it's a valid note letter with ## 36 | def double_sharp? 37 | !!name.match(/\A[A-G]##\Z/) 38 | end 39 | 40 | # Returns the current note name 41 | def name 42 | @current_note 43 | end 44 | 45 | # Returns the root of the note 46 | def root 47 | @current_note[0].to_s 48 | end 49 | 50 | def next_root 51 | idx = NOTE_NAMES.index(root) || -1 52 | NOTE_NAMES[idx + 1] 53 | end 54 | 55 | # true if the root name is equal to the other note root 56 | def root_matches?(other_note : Note) 57 | root == other_note.root 58 | end 59 | 60 | # Returns the same note, but up a halfstep in most cases 61 | # Without context of the Key, there's no telling if 62 | # the note E should be E# or F. Since there's no triple sharps, 63 | # this is the only case we just go to the next note. 64 | def halfstep_up 65 | case self 66 | when .natural?, .sharp? 67 | return "#{@current_note}#" 68 | when .flat? 69 | return @current_note[0].to_s 70 | when .double_flat? 71 | @current_note[0..1].to_s 72 | when .double_sharp? 73 | aliased = ALIASES[@current_note] 74 | return "#{aliased}#" 75 | else "" 76 | end 77 | end 78 | 79 | def halfstep_down 80 | case self 81 | when .natural?, .flat? 82 | return "#{@current_note}b" 83 | when .sharp? 84 | return @current_note[0].to_s 85 | when .double_sharp? 86 | @current_note[0..1].to_s 87 | when .double_flat? 88 | aliased = ALIASES[@current_note] 89 | return "#{aliased}b" 90 | else "" 91 | end 92 | end 93 | 94 | def wholestep_up 95 | up_halfstep = Note.new(halfstep_up) 96 | new_note = Note.new(up_halfstep.halfstep_up) 97 | if new_note.double_sharp? || new_note.root_matches?(self) 98 | ALIASES[new_note.name]? || new_note.next_root + name[1..-1] 99 | else 100 | new_note.name 101 | end 102 | end 103 | end 104 | end 105 | -------------------------------------------------------------------------------- /src/medley/scale.cr: -------------------------------------------------------------------------------- 1 | module Medley 2 | class Scale 3 | PATTERNS = { 4 | "maj": %w(1 1 .5 1 1 1 .5), 5 | "min": %w(1 .5 1 1 .5 1 1) 6 | } 7 | 8 | getter :name 9 | 10 | @pattern : Array(String) 11 | @root : String 12 | 13 | def initialize(scale : String) 14 | @name = scale 15 | scale.match(/(\w+)(maj|min)/) 16 | @pattern = PATTERNS[$2] 17 | @root = $1 18 | @notes = [] of String 19 | build_scale 20 | end 21 | 22 | def notes 23 | @notes 24 | end 25 | 26 | def key 27 | Medley::Key.new(self) 28 | end 29 | 30 | def pattern(*values : Int32) 31 | raise Medley::InvalidPatternError.new("Note arrays are not 0 based index") if values.includes?(0) 32 | results = [] of String 33 | values.each do |v| 34 | v = v % 7 if v > 7 35 | results << notes[v - 1] 36 | end 37 | results 38 | end 39 | 40 | private def build_scale 41 | note = Medley::Note.new(@root) 42 | @notes << note.name 43 | @pattern.each do |step| 44 | new_note = case step 45 | when "1" 46 | Medley::Note.new(note.wholestep_up) 47 | when ".5" 48 | Medley::Note.new(note.halfstep_up) 49 | else Medley::Note.new("C") 50 | end 51 | if new_note.root_matches?(note) 52 | new_note = Medley::Note.new(Medley::Note::ALIASES[new_note.name]) 53 | end 54 | @notes << new_note.name 55 | note = Medley::Note.new(@notes.last) 56 | end 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /src/medley/version.cr: -------------------------------------------------------------------------------- 1 | module Medley 2 | VERSION = "0.4.0" 3 | end 4 | --------------------------------------------------------------------------------