rb-music 0.0.1
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.
- checksums.yaml +7 -0
- data/LICENSE +23 -0
- data/README.md +53 -0
- data/lib/motion-music/version.rb +3 -0
- data/lib/motion-music.rb +9 -0
- data/lib/rb-music/constants.rb +105 -0
- data/lib/rb-music/interval.rb +50 -0
- data/lib/rb-music/note.rb +107 -0
- data/lib/rb-music/note_set.rb +61 -0
- data/lib/rb-music/scale.rb +30 -0
- data/lib/rb-music/version.rb +3 -0
- data/lib/rb-music.rb +8 -0
- data/spec/rb-music/constants_spec.rb +27 -0
- data/spec/rb-music/interval_spec.rb +90 -0
- data/spec/rb-music/note_set_spec.rb +191 -0
- data/spec/rb-music/note_spec.rb +318 -0
- data/spec/rb-music/scale_spec.rb +88 -0
- data/spec/spec_helper.rb +14 -0
- metadata +125 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 53f397b83b876fe6811b1244eb54176e4235abdb
|
4
|
+
data.tar.gz: ae41a88a15cf8aa76fb0fff1c955b29408d7daa4
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 0aeaace498dede1b9c25c28094d7b94ab0f4dec3f37435844fc3f9f1338e278924d0f0543615e9859344d876323b94c01fc0b8e26066cb22044a60ddb2a3cbda
|
7
|
+
data.tar.gz: 3831325e305d9df64dca46d01809ae8d9c52c65ec31f52a2ce4912af6b5a572dbed3a3702d50596762500709f98f8db3d8ab293d763e7aabd140a81d015299c3
|
data/LICENSE
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
LICENSE
|
2
|
+
|
3
|
+
The MIT License
|
4
|
+
|
5
|
+
Copyright (c) 2014 Mark Wise
|
6
|
+
|
7
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
8
|
+
of this software and associated documentation files (the "Software"), to deal
|
9
|
+
in the Software without restriction, including without limitation the rights
|
10
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
11
|
+
copies of the Software, and to permit persons to whom the Software is
|
12
|
+
furnished to do so, subject to the following conditions:
|
13
|
+
|
14
|
+
The above copyright notice and this permission notice shall be included in
|
15
|
+
all copies or substantial portions of the Software.
|
16
|
+
|
17
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
18
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
19
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
20
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
21
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
22
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
23
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
# rb-music
|
2
|
+
|
3
|
+
**rb-music** is a Ruby gem for working with musical notes, scales and intervals. It is basically a direct port of the wonderful [music.js](https://github.com/gregjopa/music.js) library by Greg Jopa.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
In your Gemfile:
|
8
|
+
|
9
|
+
```
|
10
|
+
gem 'rb-music', git: 'https://github.com/mwise/rb-music', branch: 'master'
|
11
|
+
```
|
12
|
+
|
13
|
+
In your Ruby code:
|
14
|
+
|
15
|
+
```
|
16
|
+
require 'rb-music'
|
17
|
+
```
|
18
|
+
|
19
|
+
## Overview
|
20
|
+
|
21
|
+
### Note
|
22
|
+
|
23
|
+
`Note.from_latin(name)`: Note by latin name and octave
|
24
|
+
|
25
|
+
```ruby
|
26
|
+
n = Note.from_latin('A4'); # single note
|
27
|
+
n.frequency # 440
|
28
|
+
n.latin # "A"
|
29
|
+
n.octave # 4
|
30
|
+
n.midi_note_number # 69
|
31
|
+
|
32
|
+
n = Note.from_latin('C4') # base note for scale
|
33
|
+
n.scale('major') # NoteSet built from the given note and scale
|
34
|
+
```
|
35
|
+
|
36
|
+
### Interval
|
37
|
+
|
38
|
+
`Interval.from_name(name)`: Interval by name
|
39
|
+
|
40
|
+
`Interval.from_semitones(num)`: Interval by semitones
|
41
|
+
|
42
|
+
```ruby
|
43
|
+
Interval.from_name('fifth') # define by name
|
44
|
+
whole_step = Interval.from_semitones(2) # define by # of semitones
|
45
|
+
|
46
|
+
c = Note.from_latin('C3')
|
47
|
+
|
48
|
+
# use intervals to transpose notes
|
49
|
+
d = c.add(whole_step)
|
50
|
+
|
51
|
+
# use intervals to define chords
|
52
|
+
cmaj = c.add(['unison','major third','fifth'])
|
53
|
+
```
|
data/lib/motion-music.rb
ADDED
@@ -0,0 +1,9 @@
|
|
1
|
+
unless defined?(Motion::Project::Config)
|
2
|
+
raise "This file must be required within a RubyMotion project Rakefile."
|
3
|
+
end
|
4
|
+
|
5
|
+
Motion::Project::App.setup do |app|
|
6
|
+
Dir.glob(File.join(File.dirname(__FILE__), 'rb-music/*.rb')).each do |file|
|
7
|
+
app.files.unshift(file)
|
8
|
+
end
|
9
|
+
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
module RBMusic
|
2
|
+
Error = Class.new(StandardError)
|
3
|
+
ArgumentError = Class.new(ArgumentError)
|
4
|
+
|
5
|
+
NOTE_NAMES = ["F", "C", "G", "D", "A", "E", "B"]
|
6
|
+
ACCIDENTALS = ["bb", "b", "", "#", "x"]
|
7
|
+
|
8
|
+
# notes - two dimensional [octave, fifth] - relative to the 'main' note
|
9
|
+
NOTES = {
|
10
|
+
"Fbb" => [10, -17],
|
11
|
+
"Cbb" => [10, -16],
|
12
|
+
"Gbb" => [9, -15],
|
13
|
+
"Dbb" => [8, -14],
|
14
|
+
"Abb" => [8, -13],
|
15
|
+
"Ebb" => [7, -12],
|
16
|
+
"Bbb" => [7, -11],
|
17
|
+
|
18
|
+
"Fb" => [6, -10],
|
19
|
+
"Cb" => [5, -9],
|
20
|
+
"Gb" => [5, -8],
|
21
|
+
"Db" => [4, -7],
|
22
|
+
"Ab" => [4, -6],
|
23
|
+
"Eb" => [3, -5],
|
24
|
+
"Bb" => [3, -4],
|
25
|
+
|
26
|
+
"F" => [2, -3],
|
27
|
+
"C" => [1, -2],
|
28
|
+
"G" => [1, -1],
|
29
|
+
"D" => [0, 0],
|
30
|
+
"A" => [0, 1],
|
31
|
+
"E" => [-1, 2],
|
32
|
+
"B" => [-1, 3],
|
33
|
+
|
34
|
+
"F#" => [-2, 4],
|
35
|
+
"C#" => [-3, 5],
|
36
|
+
"G#" => [-3, 6],
|
37
|
+
"D#" => [-4, 7],
|
38
|
+
"A#" => [-4, 8],
|
39
|
+
"E#" => [-5, 9],
|
40
|
+
"B#" => [-5, 10],
|
41
|
+
|
42
|
+
"Fx" => [-6, 11],
|
43
|
+
"Cx" => [-7, 12],
|
44
|
+
"Gx" => [-7, 13],
|
45
|
+
"Dx" => [-8, 14],
|
46
|
+
"Ax" => [-8, 15],
|
47
|
+
"Ex" => [-9, 16],
|
48
|
+
"Bx" => [-10, 17]
|
49
|
+
}
|
50
|
+
|
51
|
+
BASE_FREQ = 440 # A4 'main' note
|
52
|
+
BASE_OFFSET = [4, 1] # offset of base note from D0
|
53
|
+
|
54
|
+
# intervals - two dimensional [octave, fifth] - relative to the 'main' note
|
55
|
+
INTERVALS = {
|
56
|
+
unison: [0, 0],
|
57
|
+
minor_second: [3, -5],
|
58
|
+
major_second: [-1, 2],
|
59
|
+
minor_third: [2, -3],
|
60
|
+
major_third: [-2, 4],
|
61
|
+
fourth: [1, -1],
|
62
|
+
augmented_fourth: [-3, 6],
|
63
|
+
tritone: [-3, 6],
|
64
|
+
diminished_fifth: [4, -6],
|
65
|
+
fifth: [0, 1],
|
66
|
+
minor_sixth: [3, -4],
|
67
|
+
major_sixth: [-1, 3],
|
68
|
+
minor_seventh: [2, -2],
|
69
|
+
major_seventh: [-2, 5],
|
70
|
+
octave: [1, 0]
|
71
|
+
}
|
72
|
+
|
73
|
+
INTERVALS_SEMITONES = {
|
74
|
+
0 => [0, 0],
|
75
|
+
1 => [3, -5],
|
76
|
+
2 => [-1, 2],
|
77
|
+
3 => [2, -3],
|
78
|
+
4 => [-2, 4],
|
79
|
+
5 => [1, -1],
|
80
|
+
6 => [-3, 6],
|
81
|
+
7 => [0, 1],
|
82
|
+
8 => [3, -4],
|
83
|
+
9 => [-1, 3],
|
84
|
+
10 => [2, -2],
|
85
|
+
11 => [-2, 5],
|
86
|
+
12 => [1, 0]
|
87
|
+
}
|
88
|
+
|
89
|
+
SCALES = {
|
90
|
+
major: [:major_second, :major_third, :fourth, :fifth, :major_sixth, :major_seventh],
|
91
|
+
natural_minor: [:major_second, :minor_third, :fourth, :fifth, :minor_sixth, :minor_seventh],
|
92
|
+
harmonic_minor: [:major_second, :minor_third, :fourth, :fifth, :minor_sixth, :major_seventh],
|
93
|
+
major_pentatonic: [:major_second, :major_third, :fifth, :major_sixth],
|
94
|
+
minor_pentatonic: [:minor_third, :fourth, :fifth, :minor_seventh],
|
95
|
+
blues: [:minor_third, :fourth, :augmented_fourth, :fifth, :minor_seventh],
|
96
|
+
dorian: [:major_second, :minor_third, :fourth, :fifth, :major_sixth, :minor_seventh],
|
97
|
+
phrygian: [:minor_second, :minor_third, :fourth, :fifth, :major_sixth, :minor_seventh],
|
98
|
+
lydian: [:major_second, :major_third, :augmented_fourth, :fifth, :major_sixth, :major_seventh],
|
99
|
+
mixolydian: [:major_second, :major_third, :fourth, :fifth, :major_sixth, :minor_seventh],
|
100
|
+
locrian: [:minor_second, :minor_third, :fourth, :diminished_fifth, :minor_sixth, :minor_seventh],
|
101
|
+
}
|
102
|
+
SCALES[:ionian] = SCALES[:major]
|
103
|
+
SCALES[:aeolian] = SCALES[:natural_minor]
|
104
|
+
|
105
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module RBMusic
|
2
|
+
|
3
|
+
class Interval
|
4
|
+
attr_accessor :coord
|
5
|
+
|
6
|
+
def initialize(coord)
|
7
|
+
self.coord = coord
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.from_name(name)
|
11
|
+
Interval.new(INTERVALS[name.to_sym])
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.from_semitones(num)
|
15
|
+
Interval.new(INTERVALS_SEMITONES[num])
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.from_tones_semitones(tone_semitone)
|
19
|
+
# multiply [tones, semitones] vector with [-1 2;3 -5] to get coordinate from tones and semitones
|
20
|
+
Interval.new([tone_semitone[0] * -1 + tone_semitone[1] * 3, tone_semitone[0] * 2 + tone_semitone[1] * -5])
|
21
|
+
end
|
22
|
+
|
23
|
+
def tone_semitone
|
24
|
+
# multiply coord vector with [5 2;3 1] to get coordinate in tones and semitones
|
25
|
+
# [5 2;3 1] is the inverse of [-1 2;3 -5], which is the coordinates of [tone; semitone]
|
26
|
+
@tone_semitone ||= [coord[0] * 5 + coord[1] * 3, coord[0] * 2 + coord[1] * 1]
|
27
|
+
end
|
28
|
+
|
29
|
+
def semitone
|
30
|
+
# number of semitones of interval = tones * 2 + semitones
|
31
|
+
tone_semitone[0] * 2 + tone_semitone[1]
|
32
|
+
end
|
33
|
+
|
34
|
+
def add(interval)
|
35
|
+
if interval.is_a?(String)
|
36
|
+
interval = Interval.from_name(interval)
|
37
|
+
end
|
38
|
+
Interval.new([coord[0] + interval.coord[0], coord[1] + interval.coord[1]])
|
39
|
+
end
|
40
|
+
|
41
|
+
def subtract(interval)
|
42
|
+
if interval.is_a?(String)
|
43
|
+
interval = Interval.from_name(interval)
|
44
|
+
end
|
45
|
+
Interval.new([coord[0] - interval.coord[0], coord[1] - interval.coord[1]])
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
module RBMusic
|
2
|
+
|
3
|
+
class Note
|
4
|
+
attr_accessor :coord
|
5
|
+
|
6
|
+
def initialize(coord)
|
7
|
+
self.coord = coord
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.from_latin(name)
|
11
|
+
raise ArgumentError unless name.is_a?(String)
|
12
|
+
|
13
|
+
note_parts = name.split(/(\d+)/)
|
14
|
+
note_name = note_parts.first
|
15
|
+
octave = note_parts.last.to_i
|
16
|
+
|
17
|
+
unless NOTES.has_key?(note_name) && note_parts.size < 3
|
18
|
+
raise ArgumentError
|
19
|
+
end
|
20
|
+
|
21
|
+
coordinate = [NOTES[note_name][0] + octave, NOTES[note_name][1]]
|
22
|
+
|
23
|
+
coordinate[0] -= BASE_OFFSET[0]
|
24
|
+
coordinate[1] -= BASE_OFFSET[1]
|
25
|
+
|
26
|
+
Note.new(coordinate)
|
27
|
+
end
|
28
|
+
|
29
|
+
def frequency
|
30
|
+
BASE_FREQ * (2.0 ** ((coord[0] * 1200 + coord[1] * 700.0) / 1200.0))
|
31
|
+
end
|
32
|
+
|
33
|
+
def accidental
|
34
|
+
@accidental ||= ((coord[1] + BASE_OFFSET[1]) / 7.0).round
|
35
|
+
end
|
36
|
+
|
37
|
+
def octave
|
38
|
+
# calculate octave of base note without accidentals
|
39
|
+
@octave ||= coord[0] + BASE_OFFSET[0] + 4 * accidental + ((coord[1] + BASE_OFFSET[1] - 7 * accidental) / 2).floor
|
40
|
+
end
|
41
|
+
|
42
|
+
def latin
|
43
|
+
return @latin if @latin
|
44
|
+
accidentalName = ACCIDENTALS[accidental + 2]
|
45
|
+
@latin ||= base_note_name + accidentalName
|
46
|
+
end
|
47
|
+
|
48
|
+
def ==(other)
|
49
|
+
other.is_a?(Note) && other.latin == latin && other.octave == octave
|
50
|
+
end
|
51
|
+
|
52
|
+
def enharmonic?(other)
|
53
|
+
raise ArgumentError unless other.is_a?(Note)
|
54
|
+
|
55
|
+
other.frequency == frequency
|
56
|
+
end
|
57
|
+
alias_method :enharmonically_equivalent_to?, :enharmonic?
|
58
|
+
|
59
|
+
def midi_note_number
|
60
|
+
# see http://www.phys.unsw.edu.au/jw/notes.html
|
61
|
+
12 * Math.log2(frequency / 440) + 69
|
62
|
+
end
|
63
|
+
|
64
|
+
def scale(name, octaves = 1)
|
65
|
+
NoteSet.from_scale(Scale.new(latin, name), octave, octaves)
|
66
|
+
end
|
67
|
+
|
68
|
+
def add(that)
|
69
|
+
# if input is an array return an array
|
70
|
+
if that.is_a?(Array)
|
71
|
+
notes = that.map { |thing| add(thing) }
|
72
|
+
return NoteSet.new(notes)
|
73
|
+
end
|
74
|
+
|
75
|
+
# if input is string/symbol try to parse it as interval
|
76
|
+
if that.is_a?(String) || that.is_a?(Symbol)
|
77
|
+
that = Interval.from_name(that)
|
78
|
+
end
|
79
|
+
|
80
|
+
Note.new([coord[0] + that.coord[0], coord[1] + that.coord[1]])
|
81
|
+
end
|
82
|
+
|
83
|
+
def subtract(that)
|
84
|
+
if that.is_a?(Array)
|
85
|
+
notes = that.map { |thing| subtract(thing) }
|
86
|
+
return NoteSet.new(notes)
|
87
|
+
end
|
88
|
+
|
89
|
+
# if input is string try to parse it as interval
|
90
|
+
if that.is_a?(String) || that.is_a?(Symbol)
|
91
|
+
that = Interval.from_name(that)
|
92
|
+
end
|
93
|
+
|
94
|
+
coordinate = [coord[0] - that.coord[0], coord[1] - that.coord[1]]
|
95
|
+
|
96
|
+
# if input is another note return the difference as an Interval
|
97
|
+
that.is_a?(Note) ? Interval.new(coordinate) : Note.new(coordinate)
|
98
|
+
end
|
99
|
+
|
100
|
+
private
|
101
|
+
|
102
|
+
def base_note_name
|
103
|
+
@base_note_name ||= NOTE_NAMES[coord[1] + BASE_OFFSET[1] - accidental * 7 + 3]
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
module RBMusic
|
2
|
+
|
3
|
+
class NoteSet
|
4
|
+
include Enumerable
|
5
|
+
|
6
|
+
attr_accessor :notes
|
7
|
+
|
8
|
+
def initialize(notes = [])
|
9
|
+
@notes = notes
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.from_scale(scale, octave=0, octaves=1)
|
13
|
+
raise ArgumentError unless scale.is_a?(Scale) && octaves > 0
|
14
|
+
|
15
|
+
root_note = Note.from_latin("#{scale.key}#{octave}")
|
16
|
+
notes = []
|
17
|
+
octaves.times do |i|
|
18
|
+
notes += scale.degrees.map do |interval_name|
|
19
|
+
note = root_note.add(interval_name)
|
20
|
+
i.times do |octave_offset|
|
21
|
+
note = note.add(:octave)
|
22
|
+
end
|
23
|
+
note
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
self.new(notes)
|
28
|
+
end
|
29
|
+
|
30
|
+
def each(&block)
|
31
|
+
@notes.each(&block)
|
32
|
+
end
|
33
|
+
|
34
|
+
def [](index)
|
35
|
+
@notes[index]
|
36
|
+
end
|
37
|
+
|
38
|
+
def <<(other)
|
39
|
+
@notes << other
|
40
|
+
end
|
41
|
+
|
42
|
+
def map(&block)
|
43
|
+
@notes.map(&block)
|
44
|
+
end
|
45
|
+
|
46
|
+
def ==(other)
|
47
|
+
@notes == other.notes
|
48
|
+
end
|
49
|
+
alias_method :eql?, :==
|
50
|
+
|
51
|
+
def add(that)
|
52
|
+
NoteSet.new(@notes.map { |note| note.add(that) })
|
53
|
+
end
|
54
|
+
|
55
|
+
def subtract(that)
|
56
|
+
NoteSet.new(@notes.map { |note| note.subtract(that) })
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module RBMusic
|
2
|
+
|
3
|
+
class Scale
|
4
|
+
attr_reader :key
|
5
|
+
attr_reader :degrees
|
6
|
+
|
7
|
+
def initialize(key, name)
|
8
|
+
@scale_name = name.to_sym
|
9
|
+
raise ArgumentError unless NOTES.has_key?(key)
|
10
|
+
raise ArgumentError unless SCALES.has_key?(@scale_name)
|
11
|
+
@key = key
|
12
|
+
@degrees = [:unison] + SCALES[@scale_name]
|
13
|
+
end
|
14
|
+
|
15
|
+
def degree_count
|
16
|
+
@degree_count ||= @degrees.size
|
17
|
+
end
|
18
|
+
alias_method :size, :degree_count
|
19
|
+
|
20
|
+
def name
|
21
|
+
@name ||= "#{key} #{human_scale_name}"
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
def human_scale_name
|
26
|
+
@scale_name.to_s.split("_").map { |word| word.capitalize }.join(" ")
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
data/lib/rb-music.rb
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
require_relative '../spec_helper'
|
2
|
+
|
3
|
+
describe "RBMusic Constants" do
|
4
|
+
|
5
|
+
describe RBMusic::NOTE_NAMES do
|
6
|
+
it "is correct" do
|
7
|
+
subject.should == ["F", "C", "G", "D", "A", "E", "B"]
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
describe RBMusic::ACCIDENTALS do
|
12
|
+
it "is correct" do
|
13
|
+
subject.should == ["bb", "b", "", "#", "x"]
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
describe RBMusic::NOTES do
|
18
|
+
it "contains a key for each note/accidental combination" do
|
19
|
+
RBMusic::NOTE_NAMES.each do |note_name|
|
20
|
+
ACCIDENTALS.each do |accidental|
|
21
|
+
NOTES.should have_key("#{note_name}#{accidental}")
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
require_relative '../spec_helper'
|
2
|
+
|
3
|
+
describe RBMusic::Interval do
|
4
|
+
|
5
|
+
context "after initialization" do
|
6
|
+
let(:subject) { described_class.new("some coord") }
|
7
|
+
|
8
|
+
it "assigns the coord argument" do
|
9
|
+
subject.coord.should == "some coord"
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
describe "class methods" do
|
14
|
+
describe "#from_name" do
|
15
|
+
it "looks up the correct coordinates" do
|
16
|
+
subject = described_class.from_name("major_second")
|
17
|
+
|
18
|
+
subject.coord.should == described_class::INTERVALS[:major_second]
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
describe "#from_semitones" do
|
23
|
+
it "looks up the correct coordinates" do
|
24
|
+
subject = described_class.from_semitones(3)
|
25
|
+
|
26
|
+
subject.coord.should == described_class::INTERVALS_SEMITONES[3]
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
describe "#from_tones_semitones" do
|
31
|
+
it "looks up the correct coordinates" do
|
32
|
+
subject = described_class.from_tones_semitones([0, 3])
|
33
|
+
|
34
|
+
subject.coord.should == [9, -15]
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
describe "instance methods" do
|
40
|
+
let(:subject) { described_class.from_name("major_second") }
|
41
|
+
|
42
|
+
describe "#tone_semitone" do
|
43
|
+
it "returns the tone / semitone vector" do
|
44
|
+
subject.tone_semitone.should == [1, 0]
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
describe "#semitone" do
|
49
|
+
it "returns number of semitones" do
|
50
|
+
subject.semitone.should == 2
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
describe "#add" do
|
55
|
+
context "when given a string" do
|
56
|
+
it "adds the string as an Interval" do
|
57
|
+
result = subject.add("major_second")
|
58
|
+
result.coord.should == Interval.from_name("major_third").coord
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
context "when given an Interval" do
|
63
|
+
it "adds the given Interval's coordinates and returns a new Interval" do
|
64
|
+
result = subject.add(described_class.from_name("minor_second"))
|
65
|
+
result.coord.should == Interval.from_name("minor_third").coord
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
describe "#subtract" do
|
71
|
+
context "when given a string" do
|
72
|
+
it "returns a Interval with the given string as Interval subtracted" do
|
73
|
+
result = subject.subtract("major_second")
|
74
|
+
result.coord.should == Interval.from_name("unison").coord
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
context "when given an Interval" do
|
79
|
+
it "subtracts the coordinates of the given Interval and returns a new Interval" do
|
80
|
+
subject = described_class.from_name("major_third")
|
81
|
+
result = subject.subtract(described_class.from_name("major_second"))
|
82
|
+
|
83
|
+
result.coord.should == Interval.from_name("major_second").coord
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
end
|
89
|
+
|
90
|
+
end
|
@@ -0,0 +1,191 @@
|
|
1
|
+
require_relative '../spec_helper'
|
2
|
+
|
3
|
+
describe RBMusic::NoteSet do
|
4
|
+
|
5
|
+
describe "class methods" do
|
6
|
+
describe "#from_scale" do
|
7
|
+
let(:scale) { RBMusic::Scale.new("C", "major") }
|
8
|
+
|
9
|
+
context "without any arguments" do
|
10
|
+
it "raises an ArgumentError" do
|
11
|
+
lambda {
|
12
|
+
described_class.from_scale
|
13
|
+
}.should raise_error(ArgumentError)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
context "without a valid RBMusic::Scale" do
|
18
|
+
it "raises an ArgumentError" do
|
19
|
+
lambda {
|
20
|
+
described_class.from_scale("foo")
|
21
|
+
}.should raise_error(RBMusic::ArgumentError)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
context "with a RBMusic::Scale only" do
|
26
|
+
let(:subject) { described_class.from_scale(scale) }
|
27
|
+
|
28
|
+
it "returns a #{described_class}" do
|
29
|
+
subject.should be_a(described_class)
|
30
|
+
end
|
31
|
+
|
32
|
+
it "builds a note for each scale degree in the default octave ranage (1)" do
|
33
|
+
subject.notes.length.should == scale.degree_count
|
34
|
+
end
|
35
|
+
|
36
|
+
it "builds notes from the default octave (0)" do
|
37
|
+
subject.notes[0].should == Note.from_latin("#{scale.key}0")
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
context "with a RBMusic::Scale and an octave" do
|
42
|
+
let(:octave) { 3 }
|
43
|
+
let(:subject) { described_class.from_scale(scale, octave) }
|
44
|
+
|
45
|
+
it "builds notes for the scale from the given octave" do
|
46
|
+
subject.notes[0].should == Note.from_latin("#{scale.key}#{octave}")
|
47
|
+
subject.notes.length.should == scale.degree_count
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
context "with a RBMusic::Scale, octave and octave range" do
|
52
|
+
let(:octave) { 3 }
|
53
|
+
let(:octaves) { 2 }
|
54
|
+
let(:subject) { described_class.from_scale(scale, octave, octaves) }
|
55
|
+
|
56
|
+
it "builds notes for the given octave range" do
|
57
|
+
degrees = scale.degree_count
|
58
|
+
|
59
|
+
subject.notes[0].should == Note.from_latin("#{scale.key}#{octave}")
|
60
|
+
subject.notes[degrees].should == Note.from_latin("#{scale.key}#{octave + 1}")
|
61
|
+
subject.notes.length.should == scale.degree_count * octaves
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
context "with an invalid octave range" do
|
66
|
+
it "raises an ArgumentError" do
|
67
|
+
lambda {
|
68
|
+
described_class.from_scale(scale, 3, -1)
|
69
|
+
}.should raise_exception(RBMusic::ArgumentError)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
describe "instance methods" do
|
76
|
+
|
77
|
+
describe "#initialize" do
|
78
|
+
let(:notes_array) { ["foo", "bar"] }
|
79
|
+
|
80
|
+
it "assigns the notes array" do
|
81
|
+
subject = described_class.new(notes_array)
|
82
|
+
|
83
|
+
subject.notes.should == notes_array
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
describe "#==" do
|
88
|
+
let(:notes) { ["foo", "bar"] }
|
89
|
+
|
90
|
+
context "when all the notes are equal" do
|
91
|
+
it "is true" do
|
92
|
+
described_class.new(notes).should == described_class.new(notes)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
context "when note all the notes are equal" do
|
97
|
+
it "is false" do
|
98
|
+
described_class.new(notes).should_not == described_class.new([1, 2])
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
describe "#add" do
|
104
|
+
let(:f4) { Note.from_latin("F4") }
|
105
|
+
let(:g4) { Note.from_latin("G4") }
|
106
|
+
let(:subject) { NoteSet.new([f4, g4]) }
|
107
|
+
|
108
|
+
context "when adding an interval string" do
|
109
|
+
let(:operand) { "major_second" }
|
110
|
+
|
111
|
+
it "retuns a new NoteSet with the operand added to each element of the original" do
|
112
|
+
result = subject.add(operand)
|
113
|
+
|
114
|
+
result.should be_a(NoteSet)
|
115
|
+
result[0].frequency.should == subject[0].add(operand).frequency
|
116
|
+
result[1].frequency.should == subject[1].add(operand).frequency
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
context "when adding an interval symbol" do
|
121
|
+
let(:operand) { :major_second }
|
122
|
+
|
123
|
+
it "retuns a new NoteSet with the operand added to each element of the original" do
|
124
|
+
result = subject.add(operand)
|
125
|
+
|
126
|
+
result.should be_a(NoteSet)
|
127
|
+
result[0].frequency.should == subject[0].add(operand).frequency
|
128
|
+
result[1].frequency.should == subject[1].add(operand).frequency
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
context "when adding an note" do
|
133
|
+
let(:operand) { Note.from_latin("C4") }
|
134
|
+
|
135
|
+
it "retuns a new NoteSet with the operand added to each element of the original" do
|
136
|
+
result = subject.add(operand)
|
137
|
+
|
138
|
+
result.should be_a(NoteSet)
|
139
|
+
result[0].frequency.should == subject[0].add(operand).frequency
|
140
|
+
result[1].frequency.should == subject[1].add(operand).frequency
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
end
|
145
|
+
|
146
|
+
describe "#subtract" do
|
147
|
+
let(:f4) { Note.from_latin("F4") }
|
148
|
+
let(:g4) { Note.from_latin("G4") }
|
149
|
+
let(:subject) { NoteSet.new([f4, g4]) }
|
150
|
+
|
151
|
+
context "when subtracting an interval string" do
|
152
|
+
let(:operand) { "major_second" }
|
153
|
+
|
154
|
+
it "retuns a new NoteSet with the operand subtracted to each element of the original" do
|
155
|
+
result = subject.subtract(operand)
|
156
|
+
|
157
|
+
result.should be_a(NoteSet)
|
158
|
+
result[0].frequency.should == subject[0].subtract(operand).frequency
|
159
|
+
result[1].frequency.should == subject[1].subtract(operand).frequency
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
context "when subtracting an interval symbol" do
|
164
|
+
let(:operand) { :major_second }
|
165
|
+
|
166
|
+
it "retuns a new NoteSet with the operand subtracted to each element of the original" do
|
167
|
+
result = subject.subtract(operand)
|
168
|
+
|
169
|
+
result.should be_a(NoteSet)
|
170
|
+
result[0].frequency.should == subject[0].subtract(operand).frequency
|
171
|
+
result[1].frequency.should == subject[1].subtract(operand).frequency
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
context "when subtracting an note" do
|
176
|
+
let(:operand) { Note.from_latin("C4") }
|
177
|
+
|
178
|
+
it "retuns a new NoteSet with the operand subtracted to each element of the original" do
|
179
|
+
result = subject.subtract(operand)
|
180
|
+
|
181
|
+
result.should be_a(NoteSet)
|
182
|
+
result[0].coord.should == subject[0].subtract(operand).coord
|
183
|
+
result[1].coord.should == subject[1].subtract(operand).coord
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
end
|
188
|
+
|
189
|
+
end
|
190
|
+
|
191
|
+
end
|
@@ -0,0 +1,318 @@
|
|
1
|
+
require_relative '../spec_helper'
|
2
|
+
|
3
|
+
describe RBMusic::Note do
|
4
|
+
|
5
|
+
describe "class methods" do
|
6
|
+
|
7
|
+
describe "#from_latin" do
|
8
|
+
|
9
|
+
context "when given a single-character valid note name" do
|
10
|
+
let(:note_name) { RBMusic::NOTE_NAMES[0] }
|
11
|
+
let(:subject) { described_class.from_latin(note_name) }
|
12
|
+
|
13
|
+
it "returns a #{described_class}" do
|
14
|
+
subject.should be_a(described_class)
|
15
|
+
end
|
16
|
+
|
17
|
+
it "assigns the correct latin name" do
|
18
|
+
subject.latin.should == note_name
|
19
|
+
end
|
20
|
+
|
21
|
+
it "assigns a default octave of 0" do
|
22
|
+
subject.octave.should == 0
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
context "when given a single-character valid note name with octave" do
|
27
|
+
let(:note_name) { RBMusic::NOTE_NAMES[0] }
|
28
|
+
let(:octave) { 2 }
|
29
|
+
let(:subject) { described_class.from_latin("#{note_name}#{octave}") }
|
30
|
+
|
31
|
+
it "returns a #{described_class}" do
|
32
|
+
subject.should be_a(described_class)
|
33
|
+
end
|
34
|
+
|
35
|
+
it "assigns the correct latin name" do
|
36
|
+
subject.latin.should == note_name
|
37
|
+
end
|
38
|
+
|
39
|
+
it "assigns the correct octave" do
|
40
|
+
subject.octave.should == 2
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
context "when given a two-character valid note name with octave" do
|
45
|
+
let(:note_name) { "C#" }
|
46
|
+
let(:octave) { 3 }
|
47
|
+
let(:subject) { described_class.from_latin("#{note_name}#{octave}") }
|
48
|
+
|
49
|
+
it "returns a #{described_class}" do
|
50
|
+
subject.should be_a(described_class)
|
51
|
+
end
|
52
|
+
|
53
|
+
it "assigns the correct latin name" do
|
54
|
+
subject.latin.should == note_name
|
55
|
+
end
|
56
|
+
|
57
|
+
it "assigns the correct octave" do
|
58
|
+
subject.octave.should == 3
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
context "when given a single-character invalid note name" do
|
63
|
+
let(:note_name) { "Z" }
|
64
|
+
|
65
|
+
it "raises an exception" do
|
66
|
+
lambda {
|
67
|
+
described_class.from_latin(note_name)
|
68
|
+
}.should raise_error(RBMusic::ArgumentError)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
context "when given an invalid note name / octave string" do
|
73
|
+
it "raises an exception" do
|
74
|
+
lambda {
|
75
|
+
described_class.from_latin("C0E3")
|
76
|
+
}.should raise_error(RBMusic::ArgumentError)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
context "when given an empty string" do
|
81
|
+
it "raises an exception" do
|
82
|
+
lambda {
|
83
|
+
described_class.from_latin("")
|
84
|
+
}.should raise_error(RBMusic::ArgumentError)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
context "when a non-string" do
|
89
|
+
it "raises an exception" do
|
90
|
+
lambda {
|
91
|
+
described_class.from_latin(1)
|
92
|
+
}.should raise_error(RBMusic::ArgumentError)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
end
|
97
|
+
|
98
|
+
end
|
99
|
+
|
100
|
+
describe "instance methods" do
|
101
|
+
let(:subject) { Note.from_latin("A4") }
|
102
|
+
|
103
|
+
describe "#frequency" do
|
104
|
+
[
|
105
|
+
["A4", 440],
|
106
|
+
["A#4", 466.16],
|
107
|
+
["C4", 261.63],
|
108
|
+
["B3", 246.94],
|
109
|
+
["Ax3", 246.94]
|
110
|
+
].each do |pair|
|
111
|
+
|
112
|
+
it "is #{pair[1]} for #{pair[0]}" do
|
113
|
+
Note.from_latin(pair[0]).frequency.round(2).should == pair[1]
|
114
|
+
end
|
115
|
+
|
116
|
+
end
|
117
|
+
|
118
|
+
end
|
119
|
+
|
120
|
+
it "has a latin name" do
|
121
|
+
subject.latin.should == "A"
|
122
|
+
end
|
123
|
+
|
124
|
+
it "has an octave" do
|
125
|
+
subject.octave.should == 4
|
126
|
+
end
|
127
|
+
|
128
|
+
describe "#==" do
|
129
|
+
context "when the argument is a note with the same latin name and octave" do
|
130
|
+
it "is true" do
|
131
|
+
subject.should == Note.from_latin("A4")
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
context "when the argument is a note with a different latin name and same octave" do
|
136
|
+
it "is false" do
|
137
|
+
subject.should_not == Note.from_latin("B4")
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
context "when the argument is a note with the same latin name and a different octave" do
|
142
|
+
it "is false" do
|
143
|
+
subject.should_not == Note.from_latin("A5")
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
context "when the argument is not a note" do
|
148
|
+
it "is false" do
|
149
|
+
subject.should_not == "foo"
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
describe "#enharmonic?" do
|
155
|
+
context "when the argument is a note with the same latin name and octave" do
|
156
|
+
it "is true" do
|
157
|
+
subject.should be_enharmonic(Note.from_latin("A4"))
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
context "when the argument is a note with same frequency but a different latin name and/or octave" do
|
162
|
+
it "is true" do
|
163
|
+
Note.from_latin("E4").should be_enharmonic(Note.from_latin("Fb4"))
|
164
|
+
Note.from_latin("Fbb4").should be_enharmonic(Note.from_latin("Eb4"))
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
context "when the argument is a note with a different latin name and same octave" do
|
169
|
+
it "is false" do
|
170
|
+
subject.should_not be_enharmonic(Note.from_latin("B4"))
|
171
|
+
subject.should_not be_enharmonically_equivalent_to(Note.from_latin("B4"))
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
context "when the argument is a note with the same latin name and a different octave" do
|
176
|
+
it "is false" do
|
177
|
+
subject.should_not be_enharmonic(Note.from_latin("A5"))
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
context "when the argument is not a note" do
|
182
|
+
it "raises an exception" do
|
183
|
+
lambda {
|
184
|
+
subject.enharmonic?("foo")
|
185
|
+
}.should raise_exception(RBMusic::ArgumentError)
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
describe "#add" do
|
191
|
+
context "when given a string" do
|
192
|
+
it "adds an interval from the string" do
|
193
|
+
b4 = Note.from_latin("B4")
|
194
|
+
result = subject.add("major_second")
|
195
|
+
|
196
|
+
result.frequency.should == b4.frequency
|
197
|
+
result.latin.should == b4.latin
|
198
|
+
result.octave.should == b4.octave
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
context "when given a symbol" do
|
203
|
+
it "adds an interval from the symbol" do
|
204
|
+
c5 = Note.from_latin("C5")
|
205
|
+
result = subject.add(:minor_third)
|
206
|
+
|
207
|
+
result.frequency.should == c5.frequency
|
208
|
+
result.latin.should == c5.latin
|
209
|
+
result.octave.should == c5.octave
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
context "when given an array" do
|
214
|
+
it "returns an NoteSet" do
|
215
|
+
b4 = Note.from_latin("B4")
|
216
|
+
c5 = Note.from_latin("C5")
|
217
|
+
result = subject.add(["major_second", :minor_third])
|
218
|
+
|
219
|
+
result.should be_a(NoteSet)
|
220
|
+
|
221
|
+
result[0].frequency.should == b4.frequency
|
222
|
+
result[0].latin.should == b4.latin
|
223
|
+
result[0].octave.should == b4.octave
|
224
|
+
|
225
|
+
result[1].frequency.should == c5.frequency
|
226
|
+
result[1].latin.should == c5.latin
|
227
|
+
result[1].octave.should == c5.octave
|
228
|
+
end
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
232
|
+
describe "#subtract" do
|
233
|
+
|
234
|
+
context "when given a string" do
|
235
|
+
it "returns a note with an interval from the string subtracted" do
|
236
|
+
g4 = Note.from_latin("G4")
|
237
|
+
result = subject.subtract("major_second")
|
238
|
+
|
239
|
+
result.frequency.should == g4.frequency
|
240
|
+
result.latin.should == g4.latin
|
241
|
+
result.octave.should == g4.octave
|
242
|
+
end
|
243
|
+
end
|
244
|
+
|
245
|
+
context "when given a symbol" do
|
246
|
+
it "returns a note with an interval from the symbol subtracted" do
|
247
|
+
f4 = Note.from_latin("F4")
|
248
|
+
result = subject.subtract(:major_third)
|
249
|
+
|
250
|
+
result.frequency.should == f4.frequency
|
251
|
+
result.latin.should == f4.latin
|
252
|
+
result.octave.should == f4.octave
|
253
|
+
end
|
254
|
+
end
|
255
|
+
|
256
|
+
context "when given an array" do
|
257
|
+
it "returns a NoteSet" do
|
258
|
+
f4 = Note.from_latin("F4")
|
259
|
+
g4 = Note.from_latin("G4")
|
260
|
+
result = subject.subtract(["major_third", :major_second])
|
261
|
+
|
262
|
+
result.should be_a(NoteSet)
|
263
|
+
result[0].frequency.should == f4.frequency
|
264
|
+
result[0].latin.should == f4.latin
|
265
|
+
result[0].octave.should == f4.octave
|
266
|
+
|
267
|
+
result[1].frequency.should == g4.frequency
|
268
|
+
result[1].latin.should == g4.latin
|
269
|
+
result[1].octave.should == g4.octave
|
270
|
+
end
|
271
|
+
end
|
272
|
+
|
273
|
+
context "when given a Note" do
|
274
|
+
it "returns the difference as an Interval" do
|
275
|
+
f4 = Note.from_latin("F4")
|
276
|
+
result = subject.subtract(f4)
|
277
|
+
|
278
|
+
result.coord.should == Interval.from_name("major_third").coord
|
279
|
+
end
|
280
|
+
end
|
281
|
+
end
|
282
|
+
end
|
283
|
+
|
284
|
+
describe "#midi_note_number" do
|
285
|
+
[
|
286
|
+
["C3", 48],
|
287
|
+
["B2", 47],
|
288
|
+
["Ax2", 47],
|
289
|
+
["C4", 60],
|
290
|
+
["Cb4", 59],
|
291
|
+
["C#4", 61],
|
292
|
+
["Db4", 61]
|
293
|
+
].each do |pair|
|
294
|
+
it "maps #{pair[0]} to #{pair[1]}" do
|
295
|
+
Note.from_latin(pair[0]).midi_note_number.should == pair[1]
|
296
|
+
end
|
297
|
+
end
|
298
|
+
end
|
299
|
+
|
300
|
+
describe "#scale" do
|
301
|
+
let(:subject) { Note.from_latin("C4") }
|
302
|
+
let(:scale_name) { "major" }
|
303
|
+
let(:scale) { Scale.new(subject.latin, scale_name) }
|
304
|
+
|
305
|
+
it "is a note set with the default octave and range" do
|
306
|
+
result = subject.scale(scale_name)
|
307
|
+
|
308
|
+
result.should == NoteSet.from_scale(scale, subject.octave, 1)
|
309
|
+
end
|
310
|
+
|
311
|
+
it "accepts an octave range" do
|
312
|
+
result = subject.scale(scale_name, 2)
|
313
|
+
|
314
|
+
result.should == NoteSet.from_scale(scale, subject.octave, 2)
|
315
|
+
end
|
316
|
+
end
|
317
|
+
|
318
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
require_relative '../spec_helper'
|
2
|
+
|
3
|
+
describe RBMusic::Scale do
|
4
|
+
|
5
|
+
describe "initializer" do
|
6
|
+
|
7
|
+
context "when called with no arguments" do
|
8
|
+
it "raises an ArgumentError" do
|
9
|
+
lambda {
|
10
|
+
described_class.new
|
11
|
+
}.should raise_error(ArgumentError)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
context "when called without a valid note name as key" do
|
16
|
+
it "raises an ArgumentError" do
|
17
|
+
lambda {
|
18
|
+
described_class.new("foo", "bar")
|
19
|
+
}.should raise_error(RBMusic::ArgumentError)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
context "when called without a valid key and scale name" do
|
24
|
+
it "raises an ArgumentError" do
|
25
|
+
lambda {
|
26
|
+
described_class.new("C", "bar")
|
27
|
+
}.should raise_error(RBMusic::ArgumentError)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
context "when called with a valid key and scale" do
|
32
|
+
let(:key) { "C" }
|
33
|
+
let(:name) { "major" }
|
34
|
+
let(:subject) { described_class.new(key, name) }
|
35
|
+
|
36
|
+
it "assigns the key attribute" do
|
37
|
+
subject.key.should == key
|
38
|
+
end
|
39
|
+
|
40
|
+
it "assigns the degrees based on the name" do
|
41
|
+
subject.degrees.should == [:unison] + SCALES[name.to_sym]
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
describe "instance methods" do
|
47
|
+
let(:subject) { described_class.new("C", "major") }
|
48
|
+
|
49
|
+
describe "#degree_count" do
|
50
|
+
it "is the number of scale degrees" do
|
51
|
+
subject.degree_count.should == subject.degrees.size
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
describe "#name" do
|
56
|
+
it "is the human-readable name" do
|
57
|
+
subject.name.should == "C Major"
|
58
|
+
Scale.new("D#", "harmonic_minor").name.should == "D# Harmonic Minor"
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
describe "scale types" do
|
64
|
+
let(:note) { Note.from_latin("C4") }
|
65
|
+
|
66
|
+
{
|
67
|
+
"major" => ["C", "D", "E", "F", "G", "A", "B"],
|
68
|
+
"natural_minor" => ["C", "D", "Eb", "F", "G", "Ab", "Bb"],
|
69
|
+
"natural_minor" => ["C", "D", "Eb", "F", "G", "Ab", "Bb"],
|
70
|
+
"harmonic_minor" => ["C", "D", "Eb", "F", "G", "Ab", "B"],
|
71
|
+
"major_pentatonic" => ["C", "D", "E", "G", "A"],
|
72
|
+
"minor_pentatonic" => ["C", "Eb", "F", "G", "Bb"],
|
73
|
+
"blues" => ["C", "Eb", "F", "F#", "G", "Bb"],
|
74
|
+
"ionian" => ["C", "D", "E", "F", "G", "A", "B"],
|
75
|
+
"dorian" => ["C", "D", "Eb", "F", "G", "A", "Bb"],
|
76
|
+
"phrygian" => ["C", "Db", "Eb", "F", "G", "A", "Bb"],
|
77
|
+
"lydian" => ["C", "D", "E", "F#", "G", "A", "B"],
|
78
|
+
"mixolydian" => ["C", "D", "E", "F", "G", "A", "Bb"],
|
79
|
+
"aeolian" => ["C", "D", "Eb", "F", "G", "Ab", "Bb"],
|
80
|
+
"locrian" => ["C", "Db", "Eb", "F", "Gb", "Ab", "Bb"],
|
81
|
+
}.each_pair do |key, value|
|
82
|
+
it "calculates a #{key} scale" do
|
83
|
+
note.scale(key).map(&:latin).should == value
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
$TESTING=true
|
2
|
+
$:.push File.join(File.dirname(__FILE__), '..', 'lib')
|
3
|
+
|
4
|
+
if ENV["COVERAGE"]
|
5
|
+
require 'simplecov'
|
6
|
+
SimpleCov.start
|
7
|
+
end
|
8
|
+
|
9
|
+
RSpec.configure do |c|
|
10
|
+
c.filter_run focus: true
|
11
|
+
c.run_all_when_everything_filtered = true
|
12
|
+
end
|
13
|
+
|
14
|
+
require 'rb-music'
|
metadata
ADDED
@@ -0,0 +1,125 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: rb-music
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Mark Wise
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-05-18 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: guard-rspec
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rspec
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: simplecov
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
description: This gem provides Ruby classes for working with musical notes, scales
|
70
|
+
and intervals.
|
71
|
+
email:
|
72
|
+
- markmediadude@mgail.comm
|
73
|
+
executables: []
|
74
|
+
extensions: []
|
75
|
+
extra_rdoc_files:
|
76
|
+
- README.md
|
77
|
+
files:
|
78
|
+
- LICENSE
|
79
|
+
- README.md
|
80
|
+
- lib/motion-music.rb
|
81
|
+
- lib/motion-music/version.rb
|
82
|
+
- lib/rb-music.rb
|
83
|
+
- lib/rb-music/constants.rb
|
84
|
+
- lib/rb-music/interval.rb
|
85
|
+
- lib/rb-music/note.rb
|
86
|
+
- lib/rb-music/note_set.rb
|
87
|
+
- lib/rb-music/scale.rb
|
88
|
+
- lib/rb-music/version.rb
|
89
|
+
- spec/rb-music/constants_spec.rb
|
90
|
+
- spec/rb-music/interval_spec.rb
|
91
|
+
- spec/rb-music/note_set_spec.rb
|
92
|
+
- spec/rb-music/note_spec.rb
|
93
|
+
- spec/rb-music/scale_spec.rb
|
94
|
+
- spec/spec_helper.rb
|
95
|
+
homepage: https://rubygems.org/mwise/rb-music
|
96
|
+
licenses:
|
97
|
+
- MIT
|
98
|
+
metadata: {}
|
99
|
+
post_install_message:
|
100
|
+
rdoc_options: []
|
101
|
+
require_paths:
|
102
|
+
- lib
|
103
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
104
|
+
requirements:
|
105
|
+
- - ">="
|
106
|
+
- !ruby/object:Gem::Version
|
107
|
+
version: 1.8.6
|
108
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
109
|
+
requirements:
|
110
|
+
- - ">="
|
111
|
+
- !ruby/object:Gem::Version
|
112
|
+
version: '0'
|
113
|
+
requirements: []
|
114
|
+
rubyforge_project:
|
115
|
+
rubygems_version: 2.2.2
|
116
|
+
signing_key:
|
117
|
+
specification_version: 4
|
118
|
+
summary: Music theory library for Ruby
|
119
|
+
test_files:
|
120
|
+
- spec/rb-music/constants_spec.rb
|
121
|
+
- spec/rb-music/interval_spec.rb
|
122
|
+
- spec/rb-music/note_set_spec.rb
|
123
|
+
- spec/rb-music/note_spec.rb
|
124
|
+
- spec/rb-music/scale_spec.rb
|
125
|
+
- spec/spec_helper.rb
|