head_music 0.1.5 → 0.3.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.
- checksums.yaml +4 -4
- data/lib/head_music/accidental.rb +2 -1
- data/lib/head_music/interval.rb +2 -2
- data/lib/head_music/letter.rb +1 -1
- data/lib/head_music/octave.rb +2 -1
- data/lib/head_music/pitch.rb +26 -3
- data/lib/head_music/pitch_class.rb +2 -0
- data/lib/head_music/scale.rb +41 -18
- data/lib/head_music/scale_type.rb +23 -15
- data/lib/head_music/spelling.rb +17 -2
- data/lib/head_music/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 67e0581fd0dc08759395cc97286a1e96b538147d
|
4
|
+
data.tar.gz: dc985557ff8d0e1aa4720a4732a8b29a643251d8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 09fcb0a4675636f67580e095b73e5640d122d7ad72df17b0677456f0234778df1a43137eb1d3c7c5e2852a0f8acada492a179bb0e8da3e6b64252164cc554fe2
|
7
|
+
data.tar.gz: b1b1809ed935b1a03688220039cdce9ecd8c98a55d31d71a818077ce2b2a4e416d8a00ae6dbbba7fe742cf0dc4f507ce173b4a395d4638b4d0adef497f64fb18
|
@@ -9,15 +9,16 @@ class HeadMusic::Accidental
|
|
9
9
|
attr_reader :string
|
10
10
|
|
11
11
|
def self.get(identifier)
|
12
|
-
@accidentals ||= {}
|
13
12
|
for_symbol(identifier) || for_interval(identifier)
|
14
13
|
end
|
15
14
|
|
16
15
|
def self.for_symbol(identifier)
|
16
|
+
@accidentals ||= {}
|
17
17
|
@accidentals[identifier.to_s] ||= new(identifier.to_s) if ACCIDENTAL_SEMITONES.keys.include?(identifier.to_s)
|
18
18
|
end
|
19
19
|
|
20
20
|
def self.for_interval(semitones)
|
21
|
+
@accidentals ||= {}
|
21
22
|
@accidentals[semitones.to_i] ||= ACCIDENTAL_SEMITONES.key(semitones.to_i)
|
22
23
|
end
|
23
24
|
|
data/lib/head_music/interval.rb
CHANGED
@@ -8,8 +8,8 @@ class HeadMusic::Interval
|
|
8
8
|
attr_reader :semitones
|
9
9
|
|
10
10
|
def self.get(semitones)
|
11
|
-
@
|
12
|
-
@
|
11
|
+
@intervals ||= {}
|
12
|
+
@intervals[semitones.to_i] ||= new(semitones.to_i)
|
13
13
|
end
|
14
14
|
|
15
15
|
def self.named(name)
|
data/lib/head_music/letter.rb
CHANGED
@@ -18,11 +18,11 @@ class HeadMusic::Letter
|
|
18
18
|
end
|
19
19
|
|
20
20
|
def self.get(identifier)
|
21
|
-
@letters ||= {}
|
22
21
|
from_name(identifier) || from_pitch_class(identifier)
|
23
22
|
end
|
24
23
|
|
25
24
|
def self.from_name(name)
|
25
|
+
@letters ||= {}
|
26
26
|
name = name.to_s.first.upcase
|
27
27
|
@letters[name] ||= new(name) if NAMES.include?(name)
|
28
28
|
end
|
data/lib/head_music/octave.rb
CHANGED
@@ -6,19 +6,20 @@ class HeadMusic::Octave
|
|
6
6
|
MATCHER = /(-?\d+)$/
|
7
7
|
|
8
8
|
def self.get(identifier)
|
9
|
-
@octaves ||= {}
|
10
9
|
from_number(identifier) || from_name(identifier) || default
|
11
10
|
end
|
12
11
|
|
13
12
|
def self.from_number(identifier)
|
14
13
|
return nil unless identifier.to_s == identifier.to_i.to_s
|
15
14
|
return nil unless (-2..12).include?(identifier.to_i)
|
15
|
+
@octaves ||= {}
|
16
16
|
@octaves[identifier.to_i] ||= new(identifier.to_i)
|
17
17
|
end
|
18
18
|
|
19
19
|
def self.from_name(string)
|
20
20
|
if string.to_s.match(HeadMusic::Spelling::MATCHER)
|
21
21
|
_letter, _accidental, octave_string = string.to_s.match(HeadMusic::Spelling::MATCHER).captures
|
22
|
+
@octaves ||= {}
|
22
23
|
@octaves[octave_string.to_i] ||= new(octave_string.to_i) if octave_string
|
23
24
|
end
|
24
25
|
end
|
data/lib/head_music/pitch.rb
CHANGED
@@ -4,11 +4,14 @@ class HeadMusic::Pitch
|
|
4
4
|
attr_reader :spelling
|
5
5
|
attr_reader :octave
|
6
6
|
|
7
|
-
delegate :letter,
|
7
|
+
delegate :letter, to: :spelling
|
8
|
+
delegate :accidental, :sharp?, :flat?, to: :spelling
|
9
|
+
delegate :pitch_class, to: :spelling
|
10
|
+
|
11
|
+
delegate :smallest_interval_to, to: :pitch_class
|
8
12
|
|
9
13
|
def self.get(value)
|
10
|
-
|
11
|
-
@pitches[value] ||= from_name(value) || from_number(value)
|
14
|
+
from_name(value) || from_number(value)
|
12
15
|
end
|
13
16
|
|
14
17
|
def self.from_name(name)
|
@@ -23,7 +26,19 @@ class HeadMusic::Pitch
|
|
23
26
|
fetch_or_create(spelling, octave)
|
24
27
|
end
|
25
28
|
|
29
|
+
def self.from_number_and_letter(number, letter)
|
30
|
+
letter = HeadMusic::Letter.get(letter)
|
31
|
+
natural_letter_pitch = get(HeadMusic::Letter.get(letter).pitch_class)
|
32
|
+
natural_letter_pitch += 12 while (number - natural_letter_pitch.to_i) >= 12
|
33
|
+
natural_letter_pitch = get(natural_letter_pitch)
|
34
|
+
accidental_interval = natural_letter_pitch.smallest_interval_to(HeadMusic::PitchClass.get(number))
|
35
|
+
accidental = HeadMusic::Accidental.for_interval(accidental_interval)
|
36
|
+
spelling = HeadMusic::Spelling.fetch_or_create(letter, accidental)
|
37
|
+
fetch_or_create(spelling, natural_letter_pitch.octave)
|
38
|
+
end
|
39
|
+
|
26
40
|
def self.fetch_or_create(spelling, octave)
|
41
|
+
@pitches ||= {}
|
27
42
|
if spelling && (-1..9).include?(octave)
|
28
43
|
key = [spelling, octave].join
|
29
44
|
@pitches[key] ||= new(spelling, octave)
|
@@ -57,6 +72,14 @@ class HeadMusic::Pitch
|
|
57
72
|
self.midi_note_number == other.midi_note_number
|
58
73
|
end
|
59
74
|
|
75
|
+
def +(value)
|
76
|
+
Pitch.get(self.to_i + value.to_i)
|
77
|
+
end
|
78
|
+
|
79
|
+
def -(value)
|
80
|
+
Pitch.get(self.to_i - value.to_i)
|
81
|
+
end
|
82
|
+
|
60
83
|
def ==(value)
|
61
84
|
to_s == value.to_s
|
62
85
|
end
|
data/lib/head_music/scale.rb
CHANGED
@@ -1,29 +1,52 @@
|
|
1
1
|
class HeadMusic::Scale
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
}
|
7
|
-
|
8
|
-
attr_reader :pattern
|
9
|
-
|
10
|
-
def self.major
|
2
|
+
def self.get(root_pitch, scale_type_name = nil)
|
3
|
+
root_pitch = HeadMusic::Pitch.get(root_pitch)
|
4
|
+
scale_type_name ||= :major
|
5
|
+
scale_type ||= HeadMusic::ScaleType.get(scale_type_name)
|
11
6
|
@scales ||= {}
|
12
|
-
@scales[
|
7
|
+
@scales[root_pitch.to_s] ||= {}
|
8
|
+
@scales[root_pitch.to_s][scale_type.name] ||= new(root_pitch, scale_type)
|
13
9
|
end
|
14
10
|
|
15
|
-
|
16
|
-
|
17
|
-
|
11
|
+
attr_reader :root_pitch, :scale_type
|
12
|
+
|
13
|
+
def initialize(root_pitch, scale_type)
|
14
|
+
@root_pitch = HeadMusic::Pitch.get(root_pitch)
|
15
|
+
@scale_type = HeadMusic::ScaleType.get(scale_type)
|
18
16
|
end
|
19
17
|
|
20
|
-
def
|
21
|
-
@
|
22
|
-
|
18
|
+
def pitches
|
19
|
+
@pitches ||= begin
|
20
|
+
pitches = [root_pitch]
|
21
|
+
letters_cycle = HeadMusic::Letter::NAMES
|
22
|
+
letters_cycle = letters_cycle.rotate while letters_cycle.first != root_pitch.letter.to_s
|
23
|
+
semitones_from_root = 0
|
24
|
+
if scale_type.parent
|
25
|
+
parent_scale_pitches = HeadMusic::Scale.get(root_pitch, scale_type.parent_name).pitches
|
26
|
+
end
|
27
|
+
scale_type.intervals.each_with_index do |semitones, i|
|
28
|
+
semitones_from_root += semitones
|
29
|
+
pitch_number = root_pitch.pitch_class.to_i + semitones_from_root
|
30
|
+
if scale_type.intervals.length == 7
|
31
|
+
current_letter = letters_cycle[(i + 1) % 7]
|
32
|
+
elsif scale_type.intervals.length < 7 && scale_type.parent
|
33
|
+
current_letter = parent_scale_pitches.detect { |parent_scale_pitches|
|
34
|
+
parent_scale_pitches.pitch_class == (root_pitch + semitones_from_root).to_i % 12
|
35
|
+
}.letter
|
36
|
+
elsif root_pitch.flat?
|
37
|
+
current_letter = HeadMusic::PitchClass::FLAT_SPELLINGS[pitch_number % 12]
|
38
|
+
else
|
39
|
+
current_letter = HeadMusic::PitchClass::SHARP_SPELLINGS[pitch_number % 12]
|
40
|
+
end
|
41
|
+
pitch = HeadMusic::Pitch.from_number_and_letter(pitch_number, current_letter)
|
42
|
+
pitches << pitch
|
43
|
+
end
|
44
|
+
pitches
|
45
|
+
end
|
23
46
|
end
|
24
47
|
|
25
|
-
def
|
26
|
-
|
48
|
+
def pitch_names
|
49
|
+
pitches.map(&:spelling).map(&:to_s)
|
27
50
|
end
|
28
51
|
|
29
52
|
def in(spelling)
|
@@ -16,17 +16,13 @@ class HeadMusic::ScaleType
|
|
16
16
|
HARMONIC_MINOR = [W, H, W, W, H, WH, H]
|
17
17
|
MELODIC_MINOR_ASCENDING = [W, H, W, W, W, W, H]
|
18
18
|
|
19
|
-
CHROMATIC = [H, H, H, H, H, H, H, H, H, H, H, H]
|
20
|
-
MINOR_PENTATONIC = [3, 2, 2, 3, 2]
|
21
|
-
MAJOR_PENTATONIC = MINOR_PENTATONIC.rotate
|
22
|
-
|
23
19
|
MODE_NAMES = {
|
24
|
-
i: [:ionian, :major
|
20
|
+
i: [:ionian, :major],
|
25
21
|
ii: [:dorian],
|
26
22
|
iii: [:phrygian],
|
27
23
|
iv: [:lydian],
|
28
24
|
v: [:mixolydian],
|
29
|
-
vi: [:aeolian, :minor, :natural_minor
|
25
|
+
vi: [:aeolian, :minor, :natural_minor],
|
30
26
|
vii: [:locrian],
|
31
27
|
}
|
32
28
|
SCALE_TYPES = {}
|
@@ -40,14 +36,19 @@ class HeadMusic::ScaleType
|
|
40
36
|
SCALE_TYPES[:harmonic_minor] = { ascending: HARMONIC_MINOR }
|
41
37
|
SCALE_TYPES[:melodic_minor] = { ascending: MELODIC_MINOR_ASCENDING, descending: VI.reverse }
|
42
38
|
|
39
|
+
CHROMATIC = [H, H, H, H, H, H, H, H, H, H, H, H]
|
43
40
|
SCALE_TYPES[:chromatic] = { ascending: CHROMATIC }
|
44
41
|
|
45
|
-
|
46
|
-
SCALE_TYPES[:
|
42
|
+
MINOR_PENTATONIC = [3, 2, 2, 3, 2]
|
43
|
+
SCALE_TYPES[:minor_pentatonic] = { ascending: MINOR_PENTATONIC, parent_name: :minor }
|
44
|
+
SCALE_TYPES[:major_pentatonic] = { ascending: MINOR_PENTATONIC.rotate, parent_name: :major }
|
45
|
+
SCALE_TYPES[:egyptian_pentatonic] = { ascending: MINOR_PENTATONIC.rotate(2), parent_name: :minor }
|
46
|
+
SCALE_TYPES[:blues_minor_pentatonic] = { ascending: MINOR_PENTATONIC.rotate(3), parent_name: :minor }
|
47
|
+
SCALE_TYPES[:blues_major_pentatonic] = { ascending: MINOR_PENTATONIC.rotate(4), parent_name: :major }
|
47
48
|
|
49
|
+
# exotic scales
|
48
50
|
SCALE_TYPES[:octatonic] = { ascending: [W, H, W, H, W, H, W, H] }
|
49
51
|
SCALE_TYPES[:whole_tone] = { ascending: [W, W, W, W, W, W] }
|
50
|
-
SCALE_TYPES[:monotonic] = { ascending: [12] }
|
51
52
|
|
52
53
|
class << self
|
53
54
|
SCALE_TYPES.keys.each do |name|
|
@@ -60,17 +61,20 @@ class HeadMusic::ScaleType
|
|
60
61
|
def self.get(name)
|
61
62
|
@scale_types ||= {}
|
62
63
|
name = name.to_s.to_sym
|
63
|
-
|
64
|
-
@scale_types[name] ||= new(name,
|
64
|
+
attributes = SCALE_TYPES[name]
|
65
|
+
@scale_types[name] ||= new(name, attributes)
|
65
66
|
end
|
66
67
|
|
67
|
-
attr_reader :name, :ascending_intervals, :descending_intervals
|
68
|
+
attr_reader :name, :ascending_intervals, :descending_intervals, :parent_name
|
69
|
+
alias_method :intervals, :ascending_intervals
|
70
|
+
|
68
71
|
delegate :to_s, to: :name
|
69
72
|
|
70
|
-
def initialize(name,
|
73
|
+
def initialize(name, attributes)
|
71
74
|
@name = name
|
72
|
-
@ascending_intervals =
|
73
|
-
@descending_intervals =
|
75
|
+
@ascending_intervals = attributes[:ascending]
|
76
|
+
@descending_intervals = attributes[:descending] || ascending_intervals.reverse
|
77
|
+
@parent_name = attributes[:parent_name]
|
74
78
|
end
|
75
79
|
|
76
80
|
def ==(other)
|
@@ -80,4 +84,8 @@ class HeadMusic::ScaleType
|
|
80
84
|
def state
|
81
85
|
[ascending_intervals, descending_intervals]
|
82
86
|
end
|
87
|
+
|
88
|
+
def parent
|
89
|
+
@parent ||= self.class.get(parent_name) if parent_name
|
90
|
+
end
|
83
91
|
end
|
data/lib/head_music/spelling.rb
CHANGED
@@ -10,7 +10,6 @@ class HeadMusic::Spelling
|
|
10
10
|
delegate :number, to: :pitch_class, prefix: true
|
11
11
|
|
12
12
|
def self.get(identifier)
|
13
|
-
@spellings ||= {}
|
14
13
|
from_name(identifier) || from_number(identifier)
|
15
14
|
end
|
16
15
|
|
@@ -32,11 +31,19 @@ class HeadMusic::Spelling
|
|
32
31
|
return nil unless number == number.to_i
|
33
32
|
pitch_class_number = number.to_i % 12
|
34
33
|
letter = HeadMusic::Letter.from_pitch_class(pitch_class_number)
|
35
|
-
|
34
|
+
from_number_and_letter(number, letter)
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.from_number_and_letter(number, letter)
|
38
|
+
letter = HeadMusic::Letter.get(letter)
|
39
|
+
natural_letter_pitch_class = HeadMusic::Letter.get(letter).pitch_class
|
40
|
+
accidental_interval = letter.pitch_class.smallest_interval_to(HeadMusic::PitchClass.get(number))
|
41
|
+
accidental = HeadMusic::Accidental.for_interval(accidental_interval)
|
36
42
|
fetch_or_create(letter, accidental)
|
37
43
|
end
|
38
44
|
|
39
45
|
def self.fetch_or_create(letter, accidental)
|
46
|
+
@spellings ||= {}
|
40
47
|
key = [letter, accidental].join
|
41
48
|
@spellings[key] ||= new(letter, accidental)
|
42
49
|
end
|
@@ -56,6 +63,14 @@ class HeadMusic::Spelling
|
|
56
63
|
name
|
57
64
|
end
|
58
65
|
|
66
|
+
def sharp?
|
67
|
+
accidental && accidental == '#'
|
68
|
+
end
|
69
|
+
|
70
|
+
def flat?
|
71
|
+
accidental && accidental == 'b'
|
72
|
+
end
|
73
|
+
|
59
74
|
def ==(value)
|
60
75
|
to_s == value.to_s
|
61
76
|
end
|
data/lib/head_music/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: head_music
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Rob Head
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2017-02-
|
11
|
+
date: 2017-02-19 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|