music 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/.gitignore +48 -0
- data/.travis.yml +9 -0
- data/Gemfile +2 -0
- data/Gemfile.lock +37 -0
- data/LICENSE.txt +20 -0
- data/README.md +54 -0
- data/Rakefile +25 -0
- data/lib/music.rb +3 -0
- data/lib/music/chord.rb +36 -0
- data/lib/music/note.rb +148 -0
- data/lib/music/patches/hash.rb +5 -0
- data/lib/music/version.rb +3 -0
- data/music.gemspec +31 -0
- data/spec/classes/chord_spec.rb +55 -0
- data/spec/classes/hash_spec.rb +17 -0
- data/spec/classes/note_spec.rb +264 -0
- data/spec/spec_helper.rb +1 -0
- data/test/helper.rb +18 -0
- data/test/test_music.rb +7 -0
- metadata +150 -0
data/.document
ADDED
data/.gitignore
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
# rcov generated
|
2
|
+
coverage
|
3
|
+
|
4
|
+
# rdoc generated
|
5
|
+
rdoc
|
6
|
+
|
7
|
+
# yard generated
|
8
|
+
doc
|
9
|
+
.yardoc
|
10
|
+
|
11
|
+
# bundler
|
12
|
+
.bundle
|
13
|
+
|
14
|
+
# jeweler generated
|
15
|
+
pkg
|
16
|
+
|
17
|
+
# Have editor/IDE/OS specific files you need to ignore? Consider using a global gitignore:
|
18
|
+
#
|
19
|
+
# * Create a file at ~/.gitignore
|
20
|
+
# * Include files you want ignored
|
21
|
+
# * Run: git config --global core.excludesfile ~/.gitignore
|
22
|
+
#
|
23
|
+
# After doing this, these files will be ignored in all your git projects,
|
24
|
+
# saving you from having to 'pollute' every project you touch with them
|
25
|
+
#
|
26
|
+
# Not sure what to needs to be ignored for particular editors/OSes? Here's some ideas to get you started. (Remember, remove the leading # of the line)
|
27
|
+
#
|
28
|
+
# For MacOS:
|
29
|
+
#
|
30
|
+
#.DS_Store
|
31
|
+
|
32
|
+
# For TextMate
|
33
|
+
#*.tmproj
|
34
|
+
#tmtags
|
35
|
+
|
36
|
+
# For emacs:
|
37
|
+
#*~
|
38
|
+
#\#*
|
39
|
+
#.\#*
|
40
|
+
|
41
|
+
# For vim:
|
42
|
+
#*.swp
|
43
|
+
|
44
|
+
# For redcar:
|
45
|
+
#.redcar
|
46
|
+
|
47
|
+
# For rubinius:
|
48
|
+
#*.rbc
|
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
music-utils (0.5.0)
|
5
|
+
|
6
|
+
GEM
|
7
|
+
remote: http://rubygems.org/
|
8
|
+
specs:
|
9
|
+
activemodel (3.2.2)
|
10
|
+
activesupport (= 3.2.2)
|
11
|
+
builder (~> 3.0.0)
|
12
|
+
activesupport (3.2.2)
|
13
|
+
i18n (~> 0.6)
|
14
|
+
multi_json (~> 1.0)
|
15
|
+
builder (3.0.0)
|
16
|
+
diff-lcs (1.1.3)
|
17
|
+
i18n (0.6.0)
|
18
|
+
multi_json (1.1.0)
|
19
|
+
rake (0.9.2.2)
|
20
|
+
rspec (2.9.0)
|
21
|
+
rspec-core (~> 2.9.0)
|
22
|
+
rspec-expectations (~> 2.9.0)
|
23
|
+
rspec-mocks (~> 2.9.0)
|
24
|
+
rspec-core (2.9.0)
|
25
|
+
rspec-expectations (2.9.1)
|
26
|
+
diff-lcs (~> 1.1.3)
|
27
|
+
rspec-mocks (2.9.0)
|
28
|
+
|
29
|
+
PLATFORMS
|
30
|
+
ruby
|
31
|
+
|
32
|
+
DEPENDENCIES
|
33
|
+
activemodel (>= 3.0)
|
34
|
+
bundler (>= 1.1.3)
|
35
|
+
music-utils!
|
36
|
+
rake (>= 0.9)
|
37
|
+
rspec
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2012 Brian Underwood
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
music
|
2
|
+
=====
|
3
|
+
|
4
|
+
The *music* gem provides a means on calculating notes and chords.
|
5
|
+
|
6
|
+
Examples:
|
7
|
+
---------
|
8
|
+
|
9
|
+
Creating notes:
|
10
|
+
|
11
|
+
note = Note.new(698.46) # Creates a note object with the frequency 698.46
|
12
|
+
note = Note.new('F#6') # Creates an F sharp note in the 6th octave
|
13
|
+
|
14
|
+
Comparing notes:
|
15
|
+
|
16
|
+
Note.new(698.46) < Note.new(1975.53) # => true
|
17
|
+
Note.new(698.46) == Note.new('F5') # => true
|
18
|
+
|
19
|
+
Converting from frequency to note string and vice versa:
|
20
|
+
|
21
|
+
Note.new(698.46).note_string # => 'F5'
|
22
|
+
Note.new('Bb3').frequency # => 233.08
|
23
|
+
|
24
|
+
Find how many semitones two notes are apart:
|
25
|
+
|
26
|
+
Note.note_distance('A4', 'A#4') # => 1
|
27
|
+
Note.note_distance('B1', 'F1') # => -6
|
28
|
+
Note.note_distance('G#1', 'Ab1') # => 0
|
29
|
+
|
30
|
+
Adjust frequencies by semitones:
|
31
|
+
|
32
|
+
Note.frequency_adjustment(440.0, 15) # => 1046.50
|
33
|
+
|
34
|
+
Create chords:
|
35
|
+
|
36
|
+
chord = Chord.new(['C4', 'E4', 'G4']) # Create a C major chord
|
37
|
+
|
38
|
+
Describe chords:
|
39
|
+
|
40
|
+
Chord.new(['C4', 'Eb4', 'Gb4']) # => ['C', :diminished]
|
41
|
+
|
42
|
+
*For further usage, see the examples in the spec files*
|
43
|
+
|
44
|
+
Contributing to music
|
45
|
+
=====================
|
46
|
+
|
47
|
+
Contributions are welcome. Check out the issues list and feel free to fork and make a pull request if you have any fixes/additions.
|
48
|
+
|
49
|
+
Copyright
|
50
|
+
=========
|
51
|
+
|
52
|
+
Copyright (c) 2012 Brian Underwood. See LICENSE.txt for
|
53
|
+
further details.
|
54
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'rake'
|
2
|
+
require 'rspec/core/rake_task'
|
3
|
+
|
4
|
+
task :default => [:spec]
|
5
|
+
|
6
|
+
desc "Run all test"
|
7
|
+
task :test => [:spec]
|
8
|
+
|
9
|
+
# RSpec tasks
|
10
|
+
desc "Run all examples"
|
11
|
+
RSpec::Core::RakeTask.new(:spec) do |spec|
|
12
|
+
spec.ruby_opts = '-I lib'
|
13
|
+
spec.pattern = 'spec/**/*_spec.rb'
|
14
|
+
spec.rspec_opts = ['--color --format doc']
|
15
|
+
end
|
16
|
+
|
17
|
+
# Gems tasks
|
18
|
+
require "bundler"
|
19
|
+
Bundler::GemHelper.install_tasks
|
20
|
+
|
21
|
+
desc "Clean automatically generated files"
|
22
|
+
task :clean do
|
23
|
+
FileUtils.rm_rf "pkg"
|
24
|
+
end
|
25
|
+
|
data/lib/music.rb
ADDED
data/lib/music/chord.rb
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
module Music
|
2
|
+
class Chord
|
3
|
+
def initialize(notes)
|
4
|
+
@notes = notes.collect do |note|
|
5
|
+
if note.is_a?(Note)
|
6
|
+
note
|
7
|
+
else
|
8
|
+
Note.new(note)
|
9
|
+
end
|
10
|
+
end.sort
|
11
|
+
end
|
12
|
+
|
13
|
+
def note_strings
|
14
|
+
@notes.collect(&:note_string)
|
15
|
+
end
|
16
|
+
|
17
|
+
def describe
|
18
|
+
distances = (1...@notes.size).collect do |i|
|
19
|
+
@notes[0].distance_to(@notes[i])
|
20
|
+
end
|
21
|
+
|
22
|
+
quality = case distances
|
23
|
+
when [4, 7]
|
24
|
+
:major
|
25
|
+
when [3, 7]
|
26
|
+
:minor
|
27
|
+
when [3, 6]
|
28
|
+
:diminished
|
29
|
+
when [4, 8]
|
30
|
+
:augmented
|
31
|
+
end
|
32
|
+
|
33
|
+
[@notes.first.letter, quality]
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
data/lib/music/note.rb
ADDED
@@ -0,0 +1,148 @@
|
|
1
|
+
require 'active_model'
|
2
|
+
require 'music/patches/hash'
|
3
|
+
|
4
|
+
module Music
|
5
|
+
class Note
|
6
|
+
include ActiveModel::Validations
|
7
|
+
|
8
|
+
NOTES = ['C', 'C#/Db', 'D', 'D#/Eb', 'E', 'F', 'F#/Gb', 'G', 'G#/Ab', 'A', 'A#/Bb', 'B']
|
9
|
+
NOTE_STRINGS = ['Ab', 'A', 'A#', 'Bb', 'B', 'C', 'C#', 'Db', 'D', 'D#', 'Eb', 'E', 'F', 'Gb', 'G', 'G#']
|
10
|
+
|
11
|
+
attr_accessor :frequency
|
12
|
+
|
13
|
+
validates_presence_of :frequency
|
14
|
+
|
15
|
+
include Comparable
|
16
|
+
def <=>(other_note)
|
17
|
+
self.frequency <=> other_note.frequency
|
18
|
+
end
|
19
|
+
|
20
|
+
def initialize(descriptor, assumed_octave = nil)
|
21
|
+
self.frequency = if descriptor.is_a? Numeric
|
22
|
+
Note.nearest_note_frequency(descriptor)
|
23
|
+
else
|
24
|
+
Note.calculate_frequency(descriptor, assumed_octave)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def note_string(give_flat = false)
|
29
|
+
Note.calculate_note(self.frequency, give_flat).join
|
30
|
+
end
|
31
|
+
|
32
|
+
def letter(give_flat = false)
|
33
|
+
Note.calculate_note(self.frequency, give_flat)[0]
|
34
|
+
end
|
35
|
+
|
36
|
+
def accidental(give_flat = false)
|
37
|
+
Note.calculate_note(self.frequency, give_flat)[1]
|
38
|
+
end
|
39
|
+
|
40
|
+
def octave
|
41
|
+
Note.calculate_note(self.frequency)[2]
|
42
|
+
end
|
43
|
+
|
44
|
+
def pred
|
45
|
+
Note.new(Note.frequency_adjustment(self.frequency, -1))
|
46
|
+
end
|
47
|
+
|
48
|
+
def succ
|
49
|
+
Note.new(Note.frequency_adjustment(self.frequency, 1))
|
50
|
+
end
|
51
|
+
alias :next :succ
|
52
|
+
|
53
|
+
def distance_to(note)
|
54
|
+
Note.note_distance(self.note_string, note.note_string)
|
55
|
+
end
|
56
|
+
|
57
|
+
class << self
|
58
|
+
extend ActiveSupport::Memoizable
|
59
|
+
|
60
|
+
def parse_note_string(note_string, assumed_octave = nil)
|
61
|
+
match = note_string.match(/^([A-Ga-g])([#b]?)([0-8]?)$/)
|
62
|
+
|
63
|
+
raise ArgumentError, "Did not recognize note string: #{note_string}" if !match
|
64
|
+
raise ArgumentError, "No octave found or specified" if match[3].blank? && assumed_octave.nil?
|
65
|
+
raise ArgumentError if match[3].to_i > 8 || (assumed_octave && !(0..8).include?(assumed_octave))
|
66
|
+
|
67
|
+
octave = match[3].present? ? match[3] : assumed_octave
|
68
|
+
[match[1].upcase, match[2] == '' ? nil : match[2], octave.to_i]
|
69
|
+
end
|
70
|
+
|
71
|
+
def note_distance(note_string1, note_string2)
|
72
|
+
letter1, accidental1, octave1 = parse_note_string(note_string1)
|
73
|
+
letter2, accidental2, octave2 = parse_note_string(note_string2)
|
74
|
+
|
75
|
+
get_index = Proc.new do |letter, accidental|
|
76
|
+
NOTES.index do |note|
|
77
|
+
regex = case accidental
|
78
|
+
when '#' then
|
79
|
+
/^#{letter}#/
|
80
|
+
when 'b' then
|
81
|
+
/#{letter}b$/
|
82
|
+
else
|
83
|
+
/^#{letter}$/
|
84
|
+
end
|
85
|
+
note.match(regex)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
index1 = get_index.call(letter1, accidental1)
|
90
|
+
index2 = get_index.call(letter2, accidental2)
|
91
|
+
|
92
|
+
(index2 - index1) + ((octave2.to_i - octave1.to_i) * NOTES.size)
|
93
|
+
end
|
94
|
+
|
95
|
+
def frequency_adjustment(start_frequency, distance)
|
96
|
+
result = (start_frequency * (2.0 ** (distance.to_f / NOTES.size)))
|
97
|
+
|
98
|
+
# Would like to use #round(2), but want to support Ruby 1.8
|
99
|
+
(result * 100.0).round / 100.0
|
100
|
+
end
|
101
|
+
|
102
|
+
def calculate_frequency(*args)
|
103
|
+
case args.size
|
104
|
+
when 1
|
105
|
+
letter, accidental, octave = parse_note_string(args[0])
|
106
|
+
when 2
|
107
|
+
letter, accidental, octave = parse_note_string(args[0], args[1])
|
108
|
+
when 3
|
109
|
+
letter, accidental, octave = args
|
110
|
+
else
|
111
|
+
raise ArgumentError, "Invalid octave of arguments"
|
112
|
+
end
|
113
|
+
|
114
|
+
distance = note_distance('A4', "#{letter}#{accidental}#{octave}")
|
115
|
+
|
116
|
+
frequency_adjustment(440.0, distance)
|
117
|
+
end
|
118
|
+
|
119
|
+
def calculate_note(frequency, give_flat = false)
|
120
|
+
# Would like to use #log(frequency / 440.0, 2), but would like to support Ruby 1.8
|
121
|
+
frequency_log_base_2 = Math.log(frequency / 440.0) / Math.log(2)
|
122
|
+
|
123
|
+
distance = (NOTES.size * frequency_log_base_2).round
|
124
|
+
|
125
|
+
index = 9 + distance # 9 is index for A
|
126
|
+
|
127
|
+
octave = 4 + (index / NOTES.size) # 4 is because we're using A4
|
128
|
+
index = (index % NOTES.size)
|
129
|
+
|
130
|
+
parts = "#{NOTES[index]}".split('/')
|
131
|
+
note = if give_flat
|
132
|
+
parts.last
|
133
|
+
else
|
134
|
+
parts.first
|
135
|
+
end
|
136
|
+
|
137
|
+
"#{note}#{octave}"
|
138
|
+
note_parts = note.split('')
|
139
|
+
note_parts + (note_parts.size == 1 ? [nil] : []) + [octave.to_i]
|
140
|
+
end
|
141
|
+
|
142
|
+
def nearest_note_frequency(frequency)
|
143
|
+
Note.calculate_frequency(Note.calculate_note(frequency).join)
|
144
|
+
end
|
145
|
+
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
data/music.gemspec
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "music/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
# Basic Info
|
7
|
+
s.name = "music"
|
8
|
+
s.version = Music::VERSION
|
9
|
+
s.platform = Gem::Platform::RUBY
|
10
|
+
s.authors = ["Brian Underwood"]
|
11
|
+
s.email = ["ml+musicgem@semi-sentient.com"]
|
12
|
+
s.homepage = "http://github.com/cheerfulstoic/music"
|
13
|
+
s.summary = %q{Library for performing calculations on musical elements}
|
14
|
+
s.description = %q{Library for classifying notes and chords and performing calculations on them. See README.md}
|
15
|
+
s.license = %q{MIT}
|
16
|
+
|
17
|
+
# Files
|
18
|
+
s.files = `git ls-files`.split("\n")
|
19
|
+
s.test_files = `git ls-files -- {spec,factories}/*`.split("\n")
|
20
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
21
|
+
s.require_paths = ["lib"]
|
22
|
+
|
23
|
+
# Ruby Gems Version
|
24
|
+
s.required_rubygems_version = ">= 1.8"
|
25
|
+
|
26
|
+
# Dependencies
|
27
|
+
s.add_development_dependency "rake", ">= 0.9"
|
28
|
+
s.add_development_dependency "bundler", ">= 1.1.3"
|
29
|
+
s.add_development_dependency "rspec"
|
30
|
+
s.add_development_dependency "activemodel", '>= 3.2.0'
|
31
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Music::Chord do
|
4
|
+
before :all do
|
5
|
+
@standard_tuning_notes = [Note.new('E2'), Note.new('A2'), Note.new('D3'), Note.new('G3'), Note.new('B3'), Note.new('E4')]
|
6
|
+
end
|
7
|
+
|
8
|
+
describe '#new(notes)' do
|
9
|
+
it 'should take an array of Note objects' do
|
10
|
+
Chord.new(@standard_tuning_notes)
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'should take an array of note strings' do
|
14
|
+
Chord.new(@standard_tuning_notes.collect(&:note_string))
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'should sort the notes by frequency' do
|
18
|
+
Chord.new(['A2', 'E2']).note_strings.should == ['E2', 'A2']
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
describe '#note_strings' do
|
23
|
+
it 'should return an array of note strings' do
|
24
|
+
chord = Chord.new(@standard_tuning_notes)
|
25
|
+
|
26
|
+
chord.note_strings.should == @standard_tuning_notes.collect(&:note_string)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
describe '#describe' do
|
31
|
+
describe 'major chords' do
|
32
|
+
it 'should recognize C major' do
|
33
|
+
Chord.new(['C4', 'E4', 'G4']).describe.should == ['C', :major]
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
describe 'minor chords' do
|
38
|
+
it 'should recognize C minor' do
|
39
|
+
Chord.new(['C4', 'Eb4', 'G4']).describe.should == ['C', :minor]
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
describe 'diminished chords' do
|
44
|
+
it 'should recognize C diminished' do
|
45
|
+
Chord.new(['C4', 'Eb4', 'Gb4']).describe.should == ['C', :diminished]
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
describe 'augmented chords' do
|
50
|
+
it 'should recognize C augmented' do
|
51
|
+
Chord.new(['C4', 'E4', 'G#4']).describe.should == ['C', :augmented]
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'music/patches/hash'
|
3
|
+
|
4
|
+
describe Hash do
|
5
|
+
describe '#flip' do
|
6
|
+
it "Should reverse keys and values" do
|
7
|
+
{1 => 2, 3 => 4}.flip.should == {2 => 1, 4 => 3}
|
8
|
+
|
9
|
+
{'foo' => 'bar', 'baz' => 'bitz'}.flip.should == {'bitz' => 'baz', 'bar' => 'foo'}
|
10
|
+
end
|
11
|
+
|
12
|
+
it "Should return an empty hash from an empty hash" do
|
13
|
+
{}.flip.should == {}
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
@@ -0,0 +1,264 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
include Music
|
3
|
+
|
4
|
+
describe Music::Note do
|
5
|
+
|
6
|
+
describe "#new" do
|
7
|
+
it "should allow note strings to initalize notes" do
|
8
|
+
Note.new('E0').frequency.should == 20.60
|
9
|
+
Note.new('Bb3').frequency.should == 233.08
|
10
|
+
end
|
11
|
+
|
12
|
+
it "should allow frequencies to initalize notes" do
|
13
|
+
Note.new(698.46).note_string.should == 'F5'
|
14
|
+
Note.new(1975.53).note_string.should == 'B6'
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
describe "Comparing notes" do
|
19
|
+
it "should compare notes by their frequencies" do
|
20
|
+
Note.new(698.46).should < Note.new(1975.53)
|
21
|
+
Note.new('B5').should > Note.new('B2')
|
22
|
+
Note.new(698.46).should == Note.new('F5')
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
describe '#note_string' do
|
27
|
+
it 'Should return the letter, accidental, and octave as a string' do
|
28
|
+
Note.new(698.46).note_string.should == 'F5'
|
29
|
+
Note.new('C#6').note_string.should == 'C#6'
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'Should return the flat version if asked' do
|
33
|
+
Note.new('C#6').note_string(true).should == 'Db6'
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
describe '#letter' do
|
38
|
+
it 'Should return just the letter' do
|
39
|
+
Note.new('E0').letter.should == 'E'
|
40
|
+
Note.new(698.46).letter.should == 'F'
|
41
|
+
Note.new(1975.53).letter.should == 'B'
|
42
|
+
end
|
43
|
+
|
44
|
+
it 'Should return the letter based on if asked for the flat' do
|
45
|
+
Note.new('Bb3').letter.should == 'A'
|
46
|
+
Note.new('Bb3').letter(true).should == 'B'
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
describe '#accidental' do
|
51
|
+
it 'Should return just the accidental' do
|
52
|
+
Note.new('A#4').accidental.should == '#'
|
53
|
+
end
|
54
|
+
|
55
|
+
it 'Should return the flat if asked for' do
|
56
|
+
Note.new('Bb3').accidental.should == '#'
|
57
|
+
Note.new('Bb3').accidental(true).should == 'b'
|
58
|
+
end
|
59
|
+
|
60
|
+
it 'Should return nil for no accidental' do
|
61
|
+
Note.new('B2').accidental.should == nil
|
62
|
+
Note.new('G2').accidental.should == nil
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
describe '#octave' do
|
67
|
+
it 'Should return the octave' do
|
68
|
+
Note.new('A#4').octave.should == 4
|
69
|
+
Note.new('B7').octave.should == 7
|
70
|
+
Note.new('Gb1').octave.should == 1
|
71
|
+
Note.new('E0').octave.should == 0
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
describe ".parse_note_string(note_string)" do
|
76
|
+
it "Should split note letter, accidental, and octave" do
|
77
|
+
Note.parse_note_string('A#1').should == ['A', '#', 1]
|
78
|
+
Note.parse_note_string('Cb4').should == ['C', 'b', 4]
|
79
|
+
end
|
80
|
+
|
81
|
+
it "Should allow for lower case notes" do
|
82
|
+
Note.parse_note_string('g#1').should == ['G', '#', 1]
|
83
|
+
end
|
84
|
+
|
85
|
+
it "Should allow for an assumed octave" do
|
86
|
+
Note.parse_note_string('A', 4).should == ['A', nil, 4]
|
87
|
+
Note.parse_note_string('C#', 6).should == ['C', '#', 6]
|
88
|
+
end
|
89
|
+
|
90
|
+
it "Should ignore the assumed octave if there is a octave" do
|
91
|
+
Note.parse_note_string('B3', 4).should == ['B', nil, 3]
|
92
|
+
Note.parse_note_string('Gb6', 2).should == ['G', 'b', 6]
|
93
|
+
end
|
94
|
+
|
95
|
+
it "Should not allow notes above G" do
|
96
|
+
expect { Note.parse_note_string('HB1') }.to raise_error ArgumentError
|
97
|
+
end
|
98
|
+
|
99
|
+
it "Should not allow extraneous characters" do
|
100
|
+
expect { Note.parse_note_string(' Ab1') }.to raise_error ArgumentError
|
101
|
+
expect { Note.parse_note_string('%Hb1') }.to raise_error ArgumentError
|
102
|
+
expect { Note.parse_note_string('Hb1-') }.to raise_error ArgumentError
|
103
|
+
end
|
104
|
+
|
105
|
+
it "Should not allow for upper case flats" do
|
106
|
+
expect { Note.parse_note_string('AB1') }.to raise_error ArgumentError
|
107
|
+
expect { Note.parse_note_string('aB1') }.to raise_error ArgumentError
|
108
|
+
end
|
109
|
+
|
110
|
+
it "Should return nil when there is no accidental" do
|
111
|
+
Note.parse_note_string('C4').should == ['C', nil, 4]
|
112
|
+
end
|
113
|
+
|
114
|
+
it "Should not allow note above octave 8" do
|
115
|
+
Note.parse_note_string('G#8').should == ['G', '#', 8]
|
116
|
+
expect { Note.parse_note_string('Ab9') }.to raise_error ArgumentError
|
117
|
+
expect { Note.parse_note_string('Ab', 9) }.to raise_error ArgumentError
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
describe ".note_distance(note_string1, note_string2)" do
|
122
|
+
{
|
123
|
+
['A4', 'A#4'] => 1,
|
124
|
+
['A4', 'Ab4'] => -1,
|
125
|
+
['B0', 'B0'] => 0,
|
126
|
+
['G#1', 'Ab1'] => 0,
|
127
|
+
['Bb1', 'A#1'] => 0,
|
128
|
+
['A3', 'A4'] => 12,
|
129
|
+
['a3', 'A4'] => 12,
|
130
|
+
['B1', 'F1'] => -6,
|
131
|
+
['B1', 'f1'] => -6,
|
132
|
+
['A4', 'Eb0'] => -54,
|
133
|
+
['a4', 'eb0'] => -54,
|
134
|
+
['A2', 'C4'] => 15
|
135
|
+
}.each do |notes, distance|
|
136
|
+
it "Should return #{distance} between #{notes[0]} and #{notes[1]}" do
|
137
|
+
Note.note_distance(*notes).should == distance
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
it "Should not allow invalid note strings" do
|
142
|
+
expect { Note.note_distance('H0', 'A0') }.to raise_error ArgumentError
|
143
|
+
expect { Note.note_distance('A0', 'I#0') }.to raise_error ArgumentError
|
144
|
+
expect { Note.note_distance('A%0', 'A0') }.to raise_error ArgumentError
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
describe '#distance_to(note)' do
|
149
|
+
it 'Should find the distance between the subject note object and the note passed in' do
|
150
|
+
Note.new('A2').distance_to(Note.new('C4')).should == 15
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
describe ".calculate_frequency(letter, accidental, octave)" do
|
155
|
+
{
|
156
|
+
['C', nil, 0] => 16.35,
|
157
|
+
['E', 'b', 0] => 19.45,
|
158
|
+
['A', '#', 1] => 58.27,
|
159
|
+
['B', 'b', 1] => 58.27,
|
160
|
+
['A', 'b', 4] => 415.30,
|
161
|
+
['A', nil, 4] => 440.00,
|
162
|
+
['A', '#', 4] => 466.16,
|
163
|
+
['B', nil, 6] => 1975.53,
|
164
|
+
['C', nil, 7] => 2093.00,
|
165
|
+
['E', 'b', 8] => 4978.03
|
166
|
+
}.each do |note_array, frequency|
|
167
|
+
it "Should return #{frequency} for #{note_array.join}" do
|
168
|
+
Note.calculate_frequency(*note_array).should == frequency
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
it "Should take note strings as an argument" do
|
173
|
+
Note.calculate_frequency('A#1').should == 58.27
|
174
|
+
Note.calculate_frequency('A4').should == 440.00
|
175
|
+
Note.calculate_frequency('C7').should == 2093.00
|
176
|
+
end
|
177
|
+
|
178
|
+
it "Should allow lower case notes" do
|
179
|
+
Note.calculate_frequency('a#1').should == 58.27
|
180
|
+
Note.calculate_frequency('db2').should == 69.3
|
181
|
+
Note.calculate_frequency('e', nil, 3).should == 164.81
|
182
|
+
end
|
183
|
+
|
184
|
+
it "Should not take argument lengths above 3" do
|
185
|
+
expect { Note.calculate_frequency('A', nil, 0, 0) }.to raise_error ArgumentError
|
186
|
+
end
|
187
|
+
|
188
|
+
it "Should not allow invalid notes" do
|
189
|
+
expect { Note.calculate_frequency('H', nil, 0) }.to raise_error ArgumentError
|
190
|
+
expect { Note.calculate_frequency('I', nil, 0) }.to raise_error ArgumentError
|
191
|
+
end
|
192
|
+
|
193
|
+
it "Should not allow invalid accidentals" do
|
194
|
+
expect { Note.calculate_frequency('A', 5, 0) }.to raise_error ArgumentError
|
195
|
+
expect { Note.calculate_frequency('A', '&', 0) }.to raise_error ArgumentError
|
196
|
+
end
|
197
|
+
|
198
|
+
it "Should not allow invalid octaves" do
|
199
|
+
expect { Note.calculate_frequency('A', nil, -1) }.to raise_error ArgumentError
|
200
|
+
expect { Note.calculate_frequency('A', nil, 9) }.to raise_error ArgumentError
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
describe ".calculate_note(frequency)" do
|
205
|
+
test_frequencies = {
|
206
|
+
[16.35] => ['C', nil, 0],
|
207
|
+
[19.45, true] => ['E', 'b', 0],
|
208
|
+
[58.27] => ['A', '#', 1],
|
209
|
+
[58.27, true] => ['B', 'b', 1],
|
210
|
+
[415.30, true] => ['A', 'b', 4],
|
211
|
+
[440.00] => ['A', nil, 4],
|
212
|
+
[466.16] => ['A', '#', 4],
|
213
|
+
[1975.53] => ['B', nil, 6],
|
214
|
+
[2093.00] => ['C', nil, 7],
|
215
|
+
[4978.03, true] => ['E', 'b', 8]
|
216
|
+
}
|
217
|
+
|
218
|
+
test_frequencies.each do |args, note_string|
|
219
|
+
it "Should return #{note_string} for #{args[0]}#{args[1] && ' (giving flat)'}" do
|
220
|
+
Note.calculate_note(*args).should == note_string
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
it "Should calculate the closest note near to a frequency" do
|
225
|
+
Note.calculate_note(420, true).should == ['A', 'b', 4]
|
226
|
+
Note.calculate_note(430).should == ['A', nil, 4]
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
describe ".nearest_note_frequency(frequency)" do
|
231
|
+
it 'should return the frequency if it is the frequency of a note' do
|
232
|
+
Note.nearest_note_frequency(2093.00).should == 2093.00
|
233
|
+
end
|
234
|
+
|
235
|
+
it 'should return the nearest frequency which matches a note' do
|
236
|
+
Note.nearest_note_frequency(41.00).should == 41.20
|
237
|
+
Note.nearest_note_frequency(40.50).should == 41.20
|
238
|
+
Note.nearest_note_frequency(40.00).should == 38.89
|
239
|
+
Note.nearest_note_frequency(39.00).should == 38.89
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|
243
|
+
describe ".frequency_adjustment(start_frequency, distance)" do
|
244
|
+
it "Should find frequencies based on start frequency and distance" do
|
245
|
+
Note.frequency_adjustment(440.0, 0).should == 440.0
|
246
|
+
Note.frequency_adjustment(440.0, 15).should == 1046.50
|
247
|
+
Note.frequency_adjustment(440.0, -10).should == 246.94
|
248
|
+
|
249
|
+
Note.frequency_adjustment(1479.98, -6).should == 1046.50
|
250
|
+
Note.frequency_adjustment(1479.98, 7).should == 2217.46
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
254
|
+
describe "Getting adjacent notes" do
|
255
|
+
it "should allow getting of next note" do
|
256
|
+
Note.new(698.46).next.should == Note.new(739.99)
|
257
|
+
Note.new(698.46).succ.should == Note.new(739.99)
|
258
|
+
end
|
259
|
+
|
260
|
+
it "should allow getting of previous note" do
|
261
|
+
Note.new(739.99).pred.should == Note.new(698.46)
|
262
|
+
end
|
263
|
+
end
|
264
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'music'
|
data/test/helper.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'bundler'
|
3
|
+
begin
|
4
|
+
Bundler.setup(:default, :development)
|
5
|
+
rescue Bundler::BundlerError => e
|
6
|
+
$stderr.puts e.message
|
7
|
+
$stderr.puts "Run `bundle install` to install missing gems"
|
8
|
+
exit e.status_code
|
9
|
+
end
|
10
|
+
require 'test/unit'
|
11
|
+
require 'shoulda'
|
12
|
+
|
13
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
14
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
15
|
+
require 'music'
|
16
|
+
|
17
|
+
class Test::Unit::TestCase
|
18
|
+
end
|
data/test/test_music.rb
ADDED
metadata
ADDED
@@ -0,0 +1,150 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: music
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 11
|
5
|
+
prerelease:
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 5
|
9
|
+
- 0
|
10
|
+
version: 0.5.0
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Brian Underwood
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2012-04-15 00:00:00 Z
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
21
|
+
name: rake
|
22
|
+
prerelease: false
|
23
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
24
|
+
none: false
|
25
|
+
requirements:
|
26
|
+
- - ">="
|
27
|
+
- !ruby/object:Gem::Version
|
28
|
+
hash: 25
|
29
|
+
segments:
|
30
|
+
- 0
|
31
|
+
- 9
|
32
|
+
version: "0.9"
|
33
|
+
type: :development
|
34
|
+
version_requirements: *id001
|
35
|
+
- !ruby/object:Gem::Dependency
|
36
|
+
name: bundler
|
37
|
+
prerelease: false
|
38
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ">="
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
hash: 21
|
44
|
+
segments:
|
45
|
+
- 1
|
46
|
+
- 1
|
47
|
+
- 3
|
48
|
+
version: 1.1.3
|
49
|
+
type: :development
|
50
|
+
version_requirements: *id002
|
51
|
+
- !ruby/object:Gem::Dependency
|
52
|
+
name: rspec
|
53
|
+
prerelease: false
|
54
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
55
|
+
none: false
|
56
|
+
requirements:
|
57
|
+
- - ">="
|
58
|
+
- !ruby/object:Gem::Version
|
59
|
+
hash: 3
|
60
|
+
segments:
|
61
|
+
- 0
|
62
|
+
version: "0"
|
63
|
+
type: :development
|
64
|
+
version_requirements: *id003
|
65
|
+
- !ruby/object:Gem::Dependency
|
66
|
+
name: activemodel
|
67
|
+
prerelease: false
|
68
|
+
requirement: &id004 !ruby/object:Gem::Requirement
|
69
|
+
none: false
|
70
|
+
requirements:
|
71
|
+
- - ">="
|
72
|
+
- !ruby/object:Gem::Version
|
73
|
+
hash: 15
|
74
|
+
segments:
|
75
|
+
- 3
|
76
|
+
- 2
|
77
|
+
- 0
|
78
|
+
version: 3.2.0
|
79
|
+
type: :development
|
80
|
+
version_requirements: *id004
|
81
|
+
description: Library for classifying notes and chords and performing calculations on them. See README.md
|
82
|
+
email:
|
83
|
+
- ml+musicgem@semi-sentient.com
|
84
|
+
executables: []
|
85
|
+
|
86
|
+
extensions: []
|
87
|
+
|
88
|
+
extra_rdoc_files: []
|
89
|
+
|
90
|
+
files:
|
91
|
+
- .document
|
92
|
+
- .gitignore
|
93
|
+
- .travis.yml
|
94
|
+
- Gemfile
|
95
|
+
- Gemfile.lock
|
96
|
+
- LICENSE.txt
|
97
|
+
- README.md
|
98
|
+
- Rakefile
|
99
|
+
- lib/music.rb
|
100
|
+
- lib/music/chord.rb
|
101
|
+
- lib/music/note.rb
|
102
|
+
- lib/music/patches/hash.rb
|
103
|
+
- lib/music/version.rb
|
104
|
+
- music.gemspec
|
105
|
+
- spec/classes/chord_spec.rb
|
106
|
+
- spec/classes/hash_spec.rb
|
107
|
+
- spec/classes/note_spec.rb
|
108
|
+
- spec/spec_helper.rb
|
109
|
+
- test/helper.rb
|
110
|
+
- test/test_music.rb
|
111
|
+
homepage: http://github.com/cheerfulstoic/music
|
112
|
+
licenses:
|
113
|
+
- MIT
|
114
|
+
post_install_message:
|
115
|
+
rdoc_options: []
|
116
|
+
|
117
|
+
require_paths:
|
118
|
+
- lib
|
119
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
120
|
+
none: false
|
121
|
+
requirements:
|
122
|
+
- - ">="
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
hash: 3
|
125
|
+
segments:
|
126
|
+
- 0
|
127
|
+
version: "0"
|
128
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
129
|
+
none: false
|
130
|
+
requirements:
|
131
|
+
- - ">="
|
132
|
+
- !ruby/object:Gem::Version
|
133
|
+
hash: 31
|
134
|
+
segments:
|
135
|
+
- 1
|
136
|
+
- 8
|
137
|
+
version: "1.8"
|
138
|
+
requirements: []
|
139
|
+
|
140
|
+
rubyforge_project:
|
141
|
+
rubygems_version: 1.8.17
|
142
|
+
signing_key:
|
143
|
+
specification_version: 3
|
144
|
+
summary: Library for performing calculations on musical elements
|
145
|
+
test_files:
|
146
|
+
- spec/classes/chord_spec.rb
|
147
|
+
- spec/classes/hash_spec.rb
|
148
|
+
- spec/classes/note_spec.rb
|
149
|
+
- spec/spec_helper.rb
|
150
|
+
has_rdoc:
|