music 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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:
|