music 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,5 @@
1
+ lib/**/*.rb
2
+ bin/*
3
+ -
4
+ features/**/*.feature
5
+ LICENSE.txt
@@ -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
@@ -0,0 +1,9 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.8.7
4
+ - 1.9.2
5
+ - 1.9.3
6
+ - jruby-18mode # JRuby in 1.8 mode
7
+ - jruby-19mode # JRuby in 1.9 mode
8
+ - rbx-18mode
9
+ - rbx-19mode
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source "http://rubygems.org"
2
+ gemspec
@@ -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
@@ -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.
@@ -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
+
@@ -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
+
@@ -0,0 +1,3 @@
1
+ require 'music/chord'
2
+ require 'music/note'
3
+
@@ -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
@@ -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
@@ -0,0 +1,5 @@
1
+ class Hash
2
+ def flip
3
+ Hash[*self.to_a.collect(&:reverse).flatten]
4
+ end
5
+ end
@@ -0,0 +1,3 @@
1
+ module Music
2
+ VERSION = '0.5.0'
3
+ end
@@ -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
@@ -0,0 +1 @@
1
+ require 'music'
@@ -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
@@ -0,0 +1,7 @@
1
+ require 'helper'
2
+
3
+ class TestMusic < Test::Unit::TestCase
4
+ should "probably rename this file and start testing for real" do
5
+ flunk "hey buddy, you should probably rename this file and start testing for real"
6
+ end
7
+ end
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: