rb-music 0.0.3 → 0.0.4

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 3fc617a723dd833ba0f1000693c41eb7b268b6ea
4
- data.tar.gz: 19a307ef2a1fd82f13eb930c205744adbd547f9f
3
+ metadata.gz: 64717c99b5fbd105422fc99c79fd9d137abd80b0
4
+ data.tar.gz: b61cfe61e4b3c562480d2f5b201d4d0636af8e69
5
5
  SHA512:
6
- metadata.gz: d99d234ff1650ec22574dedacf1afb3624e9e71144405931ff283d64b3abd70cf0944347955f65b053434113ca48bb25fb12db0afb6450a026b66a761faf0dc7
7
- data.tar.gz: 9f01db156a66dc104f237397dd9e93f421aa51d655a4ac0473f1914852458c1f49a9e925a935e98bdfc93d7f15f1a45fffd79f6f2cc524dd65430d4005655a6d
6
+ metadata.gz: cdc92e5119be32c2a4ac88869d8a48903e50992458035265096ef4efaef1a0a6f4a8f79c89237dc2ef6458998a9d689bb4d6c7feb30768d3394c5871709b6cbf
7
+ data.tar.gz: ba46377b0db3961a66a2ae7a5c0f82da8e8c3e8fa11e389204de82d87e165888a2370ee6920ffd8d0bafea289471dff5fcdb7f314fc80271d38dc0c83e36b716
@@ -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
@@ -0,0 +1,3 @@
1
+ module RBMusic
2
+ VERSION = "0.0.4" unless defined? RBMusic::VERSION
3
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rb-music
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.3
4
+ version: 0.0.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mark Wise
@@ -102,6 +102,12 @@ files:
102
102
  - LICENSE
103
103
  - README.md
104
104
  - lib/rb-music.rb
105
+ - lib/rb-music/constants.rb
106
+ - lib/rb-music/interval.rb
107
+ - lib/rb-music/note.rb
108
+ - lib/rb-music/note_set.rb
109
+ - lib/rb-music/scale.rb
110
+ - lib/rb-music/version.rb
105
111
  - spec/rb-music/constants_spec.rb
106
112
  - spec/rb-music/interval_spec.rb
107
113
  - spec/rb-music/note_set_spec.rb