head_music 0.17.0 → 0.18.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +12 -1145
- data/Gemfile +8 -2
- data/Rakefile +7 -5
- data/bin/console +4 -3
- data/circle.yml +1 -1
- data/head_music.gemspec +19 -17
- data/lib/head_music/bar.rb +2 -0
- data/lib/head_music/chord.rb +24 -6
- data/lib/head_music/circle.rb +5 -0
- data/lib/head_music/clef.rb +5 -2
- data/lib/head_music/composition.rb +3 -0
- data/lib/head_music/consonance.rb +5 -2
- data/lib/head_music/functional_interval.rb +84 -47
- data/lib/head_music/grand_staff.rb +11 -11
- data/lib/head_music/harmonic_interval.rb +13 -7
- data/lib/head_music/instrument.rb +9 -6
- data/lib/head_music/interval.rb +13 -7
- data/lib/head_music/key_signature.rb +8 -5
- data/lib/head_music/language.rb +21 -14
- data/lib/head_music/letter_name.rb +13 -10
- data/lib/head_music/melodic_interval.rb +10 -4
- data/lib/head_music/meter.rb +12 -12
- data/lib/head_music/motion.rb +12 -9
- data/lib/head_music/named_rudiment.rb +22 -20
- data/lib/head_music/note.rb +7 -1
- data/lib/head_music/octave.rb +9 -7
- data/lib/head_music/pitch.rb +54 -27
- data/lib/head_music/pitch_class.rb +17 -12
- data/lib/head_music/placement.rb +22 -9
- data/lib/head_music/position.rb +8 -9
- data/lib/head_music/quality.rb +9 -6
- data/lib/head_music/rhythm.rb +2 -0
- data/lib/head_music/rhythmic_unit.rb +29 -19
- data/lib/head_music/rhythmic_value.rb +5 -2
- data/lib/head_music/scale.rb +65 -45
- data/lib/head_music/scale_degree.rb +9 -6
- data/lib/head_music/scale_type.rb +70 -30
- data/lib/head_music/sign.rb +18 -13
- data/lib/head_music/spelling.rb +14 -10
- data/lib/head_music/staff.rb +4 -1
- data/lib/head_music/style/analysis.rb +36 -34
- data/lib/head_music/style/annotation.rb +14 -13
- data/lib/head_music/style/annotations/always_move.rb +7 -6
- data/lib/head_music/style/annotations/approach_perfection_contrarily.rb +5 -2
- data/lib/head_music/style/annotations/at_least_eight_notes.rb +10 -8
- data/lib/head_music/style/annotations/avoid_crossing_voices.rb +11 -8
- data/lib/head_music/style/annotations/avoid_overlapping_voices.rb +17 -10
- data/lib/head_music/style/annotations/consonant_climax.rb +18 -15
- data/lib/head_music/style/annotations/consonant_downbeats.rb +6 -3
- data/lib/head_music/style/annotations/diatonic.rb +8 -5
- data/lib/head_music/style/annotations/direction_changes.rb +8 -6
- data/lib/head_music/style/annotations/end_on_perfect_consonance.rb +5 -5
- data/lib/head_music/style/annotations/end_on_tonic.rb +7 -6
- data/lib/head_music/style/annotations/frequent_direction_changes.rb +6 -3
- data/lib/head_music/style/annotations/limit_octave_leaps.rb +9 -7
- data/lib/head_music/style/annotations/moderate_direction_changes.rb +6 -3
- data/lib/head_music/style/annotations/mostly_conjunct.rb +8 -5
- data/lib/head_music/style/annotations/no_rests.rb +6 -3
- data/lib/head_music/style/annotations/no_unisons_in_middle.rb +6 -3
- data/lib/head_music/style/annotations/notes_same_length.rb +9 -6
- data/lib/head_music/style/annotations/one_to_one.rb +10 -7
- data/lib/head_music/style/annotations/prefer_contrary_motion.rb +6 -3
- data/lib/head_music/style/annotations/prefer_imperfect.rb +7 -6
- data/lib/head_music/style/annotations/prepare_octave_leaps.rb +21 -12
- data/lib/head_music/style/annotations/recover_large_leaps.rb +17 -12
- data/lib/head_music/style/annotations/singable_intervals.rb +8 -5
- data/lib/head_music/style/annotations/singable_range.rb +7 -6
- data/lib/head_music/style/annotations/single_large_leaps.rb +6 -3
- data/lib/head_music/style/annotations/start_on_perfect_consonance.rb +6 -5
- data/lib/head_music/style/annotations/start_on_tonic.rb +6 -5
- data/lib/head_music/style/annotations/step_down_to_final_note.rb +12 -12
- data/lib/head_music/style/annotations/step_out_of_unison.rb +10 -7
- data/lib/head_music/style/annotations/step_to_final_note.rb +6 -3
- data/lib/head_music/style/annotations/step_up_to_final_note.rb +12 -12
- data/lib/head_music/style/annotations/up_to_fourteen_notes.rb +6 -5
- data/lib/head_music/style/mark.rb +10 -4
- data/lib/head_music/style/rulesets/first_species_harmony.rb +6 -3
- data/lib/head_music/style/rulesets/first_species_melody.rb +6 -3
- data/lib/head_music/style/rulesets/fux_cantus_firmus.rb +6 -3
- data/lib/head_music/style/rulesets/modern_cantus_firmus.rb +6 -3
- data/lib/head_music/utilities/hash_key.rb +9 -7
- data/lib/head_music/version.rb +3 -1
- data/lib/head_music/voice.rb +7 -3
- data/lib/head_music.rb +2 -0
- metadata +4 -4
data/lib/head_music/quality.rb
CHANGED
@@ -1,3 +1,6 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# A quality is a categorization of an interval.
|
1
4
|
class HeadMusic::Quality
|
2
5
|
SHORTHAND = {
|
3
6
|
perfect: 'P',
|
@@ -7,7 +10,7 @@ class HeadMusic::Quality
|
|
7
10
|
augmented: 'A',
|
8
11
|
doubly_diminished: 'dd',
|
9
12
|
doubly_augmented: 'AA',
|
10
|
-
}
|
13
|
+
}.freeze
|
11
14
|
NAMES = SHORTHAND.keys
|
12
15
|
|
13
16
|
PERFECT_INTERVAL_MODIFICATION = {
|
@@ -15,16 +18,16 @@ class HeadMusic::Quality
|
|
15
18
|
-1 => :diminished,
|
16
19
|
0 => :perfect,
|
17
20
|
1 => :augmented,
|
18
|
-
2 => :doubly_augmented
|
19
|
-
}
|
21
|
+
2 => :doubly_augmented,
|
22
|
+
}.freeze
|
20
23
|
|
21
24
|
MAJOR_INTERVAL_MODIFICATION = {
|
22
25
|
-2 => :diminished,
|
23
26
|
-1 => :minor,
|
24
27
|
0 => :major,
|
25
28
|
1 => :augmented,
|
26
|
-
2 => :doubly_augmented
|
27
|
-
}
|
29
|
+
2 => :doubly_augmented,
|
30
|
+
}.freeze
|
28
31
|
|
29
32
|
def self.get(identifier)
|
30
33
|
@qualities ||= {}
|
@@ -48,7 +51,7 @@ class HeadMusic::Quality
|
|
48
51
|
end
|
49
52
|
|
50
53
|
def ==(other)
|
51
|
-
|
54
|
+
to_s == other.to_s
|
52
55
|
end
|
53
56
|
|
54
57
|
def shorthand
|
data/lib/head_music/rhythm.rb
CHANGED
@@ -1,11 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# A rhythmic unit is a rudiment of duration consisting of doublings and divisions of a whole note.
|
1
4
|
class HeadMusic::RhythmicUnit
|
2
5
|
include HeadMusic::NamedRudiment
|
3
6
|
|
4
|
-
MULTIPLES = ['whole', 'double whole', 'longa', 'maxima']
|
5
|
-
FRACTIONS = [
|
7
|
+
MULTIPLES = ['whole', 'double whole', 'longa', 'maxima'].freeze
|
8
|
+
FRACTIONS = [
|
9
|
+
'whole', 'half', 'quarter', 'eighth', 'sixteenth', 'thirty-second',
|
10
|
+
'sixty-fourth', 'hundred twenty-eighth', 'two hundred fifty-sixth',
|
11
|
+
].freeze
|
6
12
|
|
7
|
-
BRITISH_MULTIPLE_NAMES = %w[semibreve breve longa maxima]
|
8
|
-
BRITISH_DIVISION_NAMES = %w[
|
13
|
+
BRITISH_MULTIPLE_NAMES = %w[semibreve breve longa maxima].freeze
|
14
|
+
BRITISH_DIVISION_NAMES = %w[
|
15
|
+
semibreve minim crotchet quaver semiquaver demisemiquaver
|
16
|
+
hemidemisemiquaver semihemidemisemiquaver demisemihemidemisemiquaver
|
17
|
+
].freeze
|
9
18
|
|
10
19
|
def self.for_denominator_value(denominator)
|
11
20
|
get(FRACTIONS[Math.log2(denominator).to_i])
|
@@ -32,32 +41,25 @@ class HeadMusic::RhythmicUnit
|
|
32
41
|
end
|
33
42
|
|
34
43
|
def notehead
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
when 2
|
41
|
-
:breve
|
42
|
-
when 0.5, 1
|
43
|
-
:open
|
44
|
-
else
|
45
|
-
:closed
|
46
|
-
end
|
44
|
+
return :maxima if relative_value == 8
|
45
|
+
return :longa if relative_value == 4
|
46
|
+
return :breve if relative_value == 2
|
47
|
+
return :open if [0.5, 1].include? relative_value
|
48
|
+
:closed
|
47
49
|
end
|
48
50
|
|
49
51
|
def flags
|
50
52
|
FRACTIONS.include?(name) ? [FRACTIONS.index(name) - 2, 0].max : 0
|
51
53
|
end
|
52
54
|
|
53
|
-
def
|
55
|
+
def stemmed?
|
54
56
|
relative_value < 1
|
55
57
|
end
|
56
58
|
|
57
59
|
def british_name
|
58
|
-
if
|
60
|
+
if multiple?
|
59
61
|
BRITISH_MULTIPLE_NAMES[MULTIPLES.index(name)]
|
60
|
-
elsif
|
62
|
+
elsif fraction?
|
61
63
|
BRITISH_DIVISION_NAMES[FRACTIONS.index(name)]
|
62
64
|
elsif BRITISH_MULTIPLE_NAMES.include?(name) || BRITISH_DIVISION_NAMES.include?(name)
|
63
65
|
name
|
@@ -68,6 +70,14 @@ class HeadMusic::RhythmicUnit
|
|
68
70
|
|
69
71
|
private
|
70
72
|
|
73
|
+
def multiple?
|
74
|
+
MULTIPLES.include?(name)
|
75
|
+
end
|
76
|
+
|
77
|
+
def fraction?
|
78
|
+
FRACTIONS.include?(name)
|
79
|
+
end
|
80
|
+
|
71
81
|
def numerator_exponent
|
72
82
|
MULTIPLES.index(name) || BRITISH_MULTIPLE_NAMES.index(name) || 0
|
73
83
|
end
|
@@ -1,3 +1,6 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# A rhythmic value is a duration composed of a rhythmic unit, any number of dots, and a tied value.
|
1
4
|
class HeadMusic::RhythmicValue
|
2
5
|
attr_reader :unit, :dots, :tied_value
|
3
6
|
|
@@ -25,8 +28,8 @@ class HeadMusic::RhythmicValue
|
|
25
28
|
end
|
26
29
|
|
27
30
|
def self.dots_from_words(identifier)
|
28
|
-
return 0 unless identifier
|
29
|
-
modifier,
|
31
|
+
return 0 unless identifier.match?(/dotted/)
|
32
|
+
modifier, = identifier.split(/_*dotted_*/)
|
30
33
|
case modifier
|
31
34
|
when /tripl\w/
|
32
35
|
3
|
data/lib/head_music/scale.rb
CHANGED
@@ -1,10 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# A scale contains ordered pitches starting at a tonal center.
|
1
4
|
class HeadMusic::Scale
|
2
5
|
SCALE_REGEX = /^[A-G][#b]?\s+\w+$/
|
3
6
|
|
4
7
|
def self.get(root_pitch, scale_type = nil)
|
5
|
-
if root_pitch.is_a?(String) && scale_type =~ SCALE_REGEX
|
6
|
-
root_pitch, scale_type = root_pitch.split(/\s+/)
|
7
|
-
end
|
8
|
+
root_pitch, scale_type = root_pitch.split(/\s+/) if root_pitch.is_a?(String) && scale_type =~ SCALE_REGEX
|
8
9
|
root_pitch = HeadMusic::Pitch.get(root_pitch)
|
9
10
|
scale_type = HeadMusic::ScaleType.get(scale_type || :major)
|
10
11
|
@scales ||= {}
|
@@ -13,6 +14,8 @@ class HeadMusic::Scale
|
|
13
14
|
@scales[hash_key] ||= new(root_pitch, scale_type)
|
14
15
|
end
|
15
16
|
|
17
|
+
delegate :letter_name_cycle, to: :root_pitch
|
18
|
+
|
16
19
|
attr_reader :root_pitch, :scale_type
|
17
20
|
|
18
21
|
def initialize(root_pitch, scale_type)
|
@@ -26,30 +29,6 @@ class HeadMusic::Scale
|
|
26
29
|
@pitches[direction][octaves] ||= determine_scale_pitches(direction, octaves)
|
27
30
|
end
|
28
31
|
|
29
|
-
def determine_scale_pitches(direction, octaves)
|
30
|
-
semitones_from_root = 0
|
31
|
-
[root_pitch].tap do |pitches|
|
32
|
-
[:ascending, :descending].each do |single_direction|
|
33
|
-
if [single_direction, :both].include?(direction)
|
34
|
-
(1..octaves).each do
|
35
|
-
direction_intervals(single_direction).each_with_index do |semitones, i|
|
36
|
-
semitones_from_root += semitones * direction_sign(single_direction)
|
37
|
-
pitches << pitch_for_step(i+1, semitones_from_root, single_direction)
|
38
|
-
end
|
39
|
-
end
|
40
|
-
end
|
41
|
-
end
|
42
|
-
end
|
43
|
-
end
|
44
|
-
|
45
|
-
def direction_sign(direction)
|
46
|
-
direction == :descending ? -1 : 1
|
47
|
-
end
|
48
|
-
|
49
|
-
def direction_intervals(direction)
|
50
|
-
scale_type.send("#{direction}_intervals")
|
51
|
-
end
|
52
|
-
|
53
32
|
def spellings(direction: :ascending, octaves: 1)
|
54
33
|
pitches(direction: direction, octaves: octaves).map(&:spelling).map(&:to_s)
|
55
34
|
end
|
@@ -58,10 +37,6 @@ class HeadMusic::Scale
|
|
58
37
|
pitches(direction: direction, octaves: octaves).map(&:name)
|
59
38
|
end
|
60
39
|
|
61
|
-
def letter_name_cycle
|
62
|
-
@letter_name_cycle ||= root_pitch.letter_name_cycle
|
63
|
-
end
|
64
|
-
|
65
40
|
def root_pitch_number
|
66
41
|
@root_pitch_number ||= root_pitch.number
|
67
42
|
end
|
@@ -72,26 +47,41 @@ class HeadMusic::Scale
|
|
72
47
|
|
73
48
|
private
|
74
49
|
|
50
|
+
def determine_scale_pitches(direction, octaves)
|
51
|
+
semitones_from_root = 0
|
52
|
+
pitches = [root_pitch]
|
53
|
+
%i[ascending descending].each do |single_direction|
|
54
|
+
next unless [single_direction, :both].include?(direction)
|
55
|
+
(1..octaves).each do
|
56
|
+
pitches += octave_scale_pitches(single_direction, semitones_from_root)
|
57
|
+
semitones_from_root += 12 * direction_sign(single_direction)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
pitches
|
61
|
+
end
|
62
|
+
|
63
|
+
def octave_scale_pitches(direction, semitones_from_root)
|
64
|
+
direction_intervals(direction).map.with_index do |semitones, i|
|
65
|
+
semitones_from_root += semitones * direction_sign(direction)
|
66
|
+
pitch_for_step(i + 1, semitones_from_root, direction)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def direction_sign(direction)
|
71
|
+
direction == :descending ? -1 : 1
|
72
|
+
end
|
73
|
+
|
74
|
+
def direction_intervals(direction)
|
75
|
+
scale_type.send("#{direction}_intervals")
|
76
|
+
end
|
77
|
+
|
75
78
|
def parent_scale_pitches
|
76
79
|
HeadMusic::Scale.get(root_pitch, scale_type.parent_name).pitches if scale_type.parent
|
77
80
|
end
|
78
81
|
|
79
82
|
def parent_scale_pitch_for(semitones_from_root)
|
80
|
-
parent_scale_pitches.detect
|
83
|
+
parent_scale_pitches.detect do |parent_scale_pitch|
|
81
84
|
parent_scale_pitch.pitch_class == (root_pitch + semitones_from_root).to_i % 12
|
82
|
-
}
|
83
|
-
end
|
84
|
-
|
85
|
-
def letter_for_step(step, semitones_from_root, direction)
|
86
|
-
pitch_class_number = (root_pitch.pitch_class.to_i + semitones_from_root) % 12
|
87
|
-
if scale_type.intervals.length == 7
|
88
|
-
direction == :ascending ? letter_name_cycle[step % 7] : letter_name_cycle[-step % 7]
|
89
|
-
elsif scale_type.intervals.length < 7 && parent_scale_pitches
|
90
|
-
parent_scale_pitch_for(semitones_from_root).letter_name
|
91
|
-
elsif root_pitch.flat?
|
92
|
-
HeadMusic::PitchClass::FLAT_SPELLINGS[pitch_class_number]
|
93
|
-
else
|
94
|
-
HeadMusic::PitchClass::SHARP_SPELLINGS[pitch_class_number]
|
95
85
|
end
|
96
86
|
end
|
97
87
|
|
@@ -100,4 +90,34 @@ class HeadMusic::Scale
|
|
100
90
|
letter_name = letter_for_step(step, semitones_from_root, direction)
|
101
91
|
HeadMusic::Pitch.from_number_and_letter(pitch_number, letter_name)
|
102
92
|
end
|
93
|
+
|
94
|
+
def letter_for_step(step, semitones_from_root, direction)
|
95
|
+
diatonic_letter_for_step(direction, step) ||
|
96
|
+
child_scale_letter_for_step(semitones_from_root) ||
|
97
|
+
flat_letter_for_step(semitones_from_root) ||
|
98
|
+
sharp_letter_for_step(semitones_from_root)
|
99
|
+
end
|
100
|
+
|
101
|
+
def diatonic_letter_for_step(direction, step)
|
102
|
+
return unless scale_type.diatonic?
|
103
|
+
direction == :ascending ? letter_name_cycle[step % 7] : letter_name_cycle[-step % 7]
|
104
|
+
end
|
105
|
+
|
106
|
+
def child_scale_letter_for_step(semitones_from_root)
|
107
|
+
return unless scale_type.parent
|
108
|
+
parent_scale_pitch_for(semitones_from_root).letter_name
|
109
|
+
end
|
110
|
+
|
111
|
+
def flat_letter_for_step(semitones_from_root)
|
112
|
+
return unless root_pitch.flat?
|
113
|
+
HeadMusic::PitchClass::FLAT_SPELLINGS[pitch_class_number(semitones_from_root)]
|
114
|
+
end
|
115
|
+
|
116
|
+
def sharp_letter_for_step(semitones_from_root)
|
117
|
+
HeadMusic::PitchClass::SHARP_SPELLINGS[pitch_class_number(semitones_from_root)]
|
118
|
+
end
|
119
|
+
|
120
|
+
def pitch_class_number(semitones_from_root)
|
121
|
+
(root_pitch.pitch_class.to_i + semitones_from_root) % 12
|
122
|
+
end
|
103
123
|
end
|
@@ -1,7 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# A scale degree is a number indicating the ordinality of the spelling in the key signature.
|
4
|
+
# TODO: Rewrite to accept a tonal_center and a scale type.
|
1
5
|
class HeadMusic::ScaleDegree
|
2
6
|
include Comparable
|
3
7
|
|
4
|
-
NAME_FOR_DIATONIC_DEGREE = [nil, 'tonic', 'supertonic', 'mediant', 'subdominant', 'dominant', 'submediant']
|
8
|
+
NAME_FOR_DIATONIC_DEGREE = [nil, 'tonic', 'supertonic', 'mediant', 'subdominant', 'dominant', 'submediant'].freeze
|
5
9
|
|
6
10
|
attr_reader :key_signature, :spelling
|
7
11
|
delegate :scale, to: :key_signature
|
@@ -17,8 +21,8 @@ class HeadMusic::ScaleDegree
|
|
17
21
|
end
|
18
22
|
|
19
23
|
def sign
|
20
|
-
sign_semitones = spelling.sign
|
21
|
-
usual_sign_semitones = scale_degree_usual_spelling.sign
|
24
|
+
sign_semitones = spelling.sign&.semitones || 0
|
25
|
+
usual_sign_semitones = scale_degree_usual_spelling.sign&.semitones || 0
|
22
26
|
delta = sign_semitones - usual_sign_semitones
|
23
27
|
HeadMusic::Sign.by(:semitones, delta) if delta != 0
|
24
28
|
end
|
@@ -36,10 +40,9 @@ class HeadMusic::ScaleDegree
|
|
36
40
|
end
|
37
41
|
|
38
42
|
def name_for_degree
|
39
|
-
|
40
|
-
|
43
|
+
return unless scale_type.diatonic?
|
44
|
+
NAME_FOR_DIATONIC_DEGREE[degree] ||
|
41
45
|
(scale_type.intervals.last == 1 || sign == '#' ? 'leading tone' : 'subtonic')
|
42
|
-
end
|
43
46
|
end
|
44
47
|
|
45
48
|
private
|
@@ -1,10 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# A ScaleType represents a particular scale pattern, such as major, lydian, or minor pentatonic.
|
1
4
|
class HeadMusic::ScaleType
|
2
5
|
H = 1 # whole step
|
3
6
|
W = 2 # half step
|
4
|
-
WH = W + H # augmented second
|
5
7
|
|
6
8
|
# Modal
|
7
|
-
I = [W, W, H, W, W, W, H]
|
9
|
+
I = [W, W, H, W, W, W, H].freeze
|
8
10
|
II = I.rotate
|
9
11
|
III = I.rotate(2)
|
10
12
|
IV = I.rotate(3)
|
@@ -13,47 +15,73 @@ class HeadMusic::ScaleType
|
|
13
15
|
VII = I.rotate(6)
|
14
16
|
|
15
17
|
# Tonal
|
16
|
-
HARMONIC_MINOR = [W, H, W, W, H,
|
17
|
-
MELODIC_MINOR_ASCENDING = [W, H, W, W, W, W, H]
|
18
|
+
HARMONIC_MINOR = [W, H, W, W, H, 3, H].freeze
|
19
|
+
MELODIC_MINOR_ASCENDING = [W, H, W, W, W, W, H].freeze
|
18
20
|
|
19
21
|
MODE_NAMES = {
|
20
|
-
i: [
|
22
|
+
i: %i[ionian major],
|
21
23
|
ii: [:dorian],
|
22
24
|
iii: [:phrygian],
|
23
25
|
iv: [:lydian],
|
24
26
|
v: [:mixolydian],
|
25
|
-
vi: [
|
27
|
+
vi: %i[aeolian minor natural_minor],
|
26
28
|
vii: [:locrian],
|
27
|
-
}
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
29
|
+
}.freeze
|
30
|
+
|
31
|
+
CHROMATIC = [H, H, H, H, H, H, H, H, H, H, H, H].freeze
|
32
|
+
|
33
|
+
MINOR_PENTATONIC = [3, 2, 2, 3, 2].freeze
|
34
|
+
|
35
|
+
def self._modes
|
36
|
+
{}.tap do |modes|
|
37
|
+
MODE_NAMES.each do |roman_numeral, aliases|
|
38
|
+
intervals = { ascending: const_get(roman_numeral.upcase) }
|
39
|
+
modes[roman_numeral] = intervals
|
40
|
+
aliases.each { |name| modes[name] = intervals }
|
41
|
+
end
|
34
42
|
end
|
35
43
|
end
|
36
|
-
SCALE_TYPES[:harmonic_minor] = { ascending: HARMONIC_MINOR }
|
37
|
-
SCALE_TYPES[:melodic_minor] = { ascending: MELODIC_MINOR_ASCENDING, descending: VI.reverse }
|
38
44
|
|
39
|
-
|
40
|
-
|
45
|
+
def self._minor_scales
|
46
|
+
{
|
47
|
+
harmonic_minor: { ascending: HARMONIC_MINOR },
|
48
|
+
melodic_minor: { ascending: MELODIC_MINOR_ASCENDING, descending: VI.reverse },
|
49
|
+
}
|
50
|
+
end
|
41
51
|
|
42
|
-
|
43
|
-
|
44
|
-
|
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 }
|
52
|
+
def self._chromatic_scales
|
53
|
+
{ chromatic: { ascending: CHROMATIC } }
|
54
|
+
end
|
48
55
|
|
49
|
-
|
50
|
-
|
51
|
-
|
56
|
+
def self._pentatonic_scales
|
57
|
+
{
|
58
|
+
minor_pentatonic: { ascending: MINOR_PENTATONIC, parent_name: :minor },
|
59
|
+
major_pentatonic: { ascending: MINOR_PENTATONIC.rotate, parent_name: :major },
|
60
|
+
egyptian_pentatonic: { ascending: MINOR_PENTATONIC.rotate(2), parent_name: :minor },
|
61
|
+
blues_minor_pentatonic: { ascending: MINOR_PENTATONIC.rotate(3), parent_name: :minor },
|
62
|
+
blues_major_pentatonic: { ascending: MINOR_PENTATONIC.rotate(4), parent_name: :major },
|
63
|
+
}
|
64
|
+
end
|
65
|
+
|
66
|
+
def self._exotic_scales
|
67
|
+
{
|
68
|
+
octatonic: { ascending: [W, H, W, H, W, H, W, H] },
|
69
|
+
whole_tone: { ascending: [W, W, W, W, W, W] },
|
70
|
+
}
|
71
|
+
end
|
72
|
+
|
73
|
+
SCALE_TYPES = {}.tap do |scales|
|
74
|
+
scales.merge!(_modes)
|
75
|
+
scales.merge!(_minor_scales)
|
76
|
+
scales.merge!(_chromatic_scales)
|
77
|
+
scales.merge!(_pentatonic_scales)
|
78
|
+
scales.merge!(_exotic_scales)
|
79
|
+
end.freeze
|
52
80
|
|
53
81
|
class << self
|
54
|
-
SCALE_TYPES.
|
82
|
+
SCALE_TYPES.each_key do |name|
|
55
83
|
define_method(name) do
|
56
|
-
|
84
|
+
get(name)
|
57
85
|
end
|
58
86
|
end
|
59
87
|
end
|
@@ -70,7 +98,7 @@ class HeadMusic::ScaleType
|
|
70
98
|
end
|
71
99
|
|
72
100
|
attr_reader :name, :ascending_intervals, :descending_intervals, :parent_name
|
73
|
-
|
101
|
+
alias intervals ascending_intervals
|
74
102
|
|
75
103
|
delegate :to_s, to: :name
|
76
104
|
|
@@ -82,7 +110,7 @@ class HeadMusic::ScaleType
|
|
82
110
|
end
|
83
111
|
|
84
112
|
def ==(other)
|
85
|
-
|
113
|
+
state == other.state
|
86
114
|
end
|
87
115
|
|
88
116
|
def state
|
@@ -96,4 +124,16 @@ class HeadMusic::ScaleType
|
|
96
124
|
def diatonic?
|
97
125
|
intervals.length == 7
|
98
126
|
end
|
127
|
+
|
128
|
+
def whole_tone?
|
129
|
+
intervals.length == 6 && intervals.uniq == [2]
|
130
|
+
end
|
131
|
+
|
132
|
+
def pentatonic?
|
133
|
+
intervals.length == 5
|
134
|
+
end
|
135
|
+
|
136
|
+
def chromatic?
|
137
|
+
intervals.length == 12
|
138
|
+
end
|
99
139
|
end
|
data/lib/head_music/sign.rb
CHANGED
@@ -1,15 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# A Sign is a symbol that modifies pitch, such as a sharp, flat, or natural.
|
1
4
|
class HeadMusic::Sign
|
2
5
|
include Comparable
|
3
6
|
|
4
|
-
attr_reader :identifier, :
|
7
|
+
attr_reader :identifier, :ascii, :unicode, :html_entity, :cents
|
5
8
|
|
6
9
|
def self.all
|
7
10
|
@all ||= [
|
8
|
-
new(identifier: :sharp,
|
9
|
-
new(identifier: :flat,
|
10
|
-
new(identifier: :natural,
|
11
|
-
new(identifier: :double_sharp,
|
12
|
-
new(identifier: :double_flat,
|
11
|
+
new(identifier: :sharp, ascii: '#', unicode: '♯', html_entity: '♯', cents: 100),
|
12
|
+
new(identifier: :flat, ascii: 'b', unicode: '♭', html_entity: '♭', cents: -100),
|
13
|
+
new(identifier: :natural, ascii: '', unicode: '♮', html_entity: '♮', cents: 0),
|
14
|
+
new(identifier: :double_sharp, ascii: '##', unicode: '𝄪', html_entity: '𝄪', cents: 200),
|
15
|
+
new(identifier: :double_flat, ascii: 'bb', unicode: '𝄫', html_entity: '𝄫', cents: -200),
|
13
16
|
]
|
14
17
|
end
|
15
18
|
|
@@ -22,7 +25,7 @@ class HeadMusic::Sign
|
|
22
25
|
end
|
23
26
|
|
24
27
|
def self.symbol?(candidate)
|
25
|
-
/^(#{matcher})
|
28
|
+
candidate =~ /^(#{matcher})$/
|
26
29
|
end
|
27
30
|
|
28
31
|
def self.get(identifier)
|
@@ -34,14 +37,17 @@ class HeadMusic::Sign
|
|
34
37
|
|
35
38
|
def self.by(key, value)
|
36
39
|
all.detect do |sign|
|
37
|
-
if %i[cents semitones].include?(key.to_sym)
|
38
|
-
sign.send(key) == value
|
39
|
-
end
|
40
|
+
sign.send(key) == value if %i[cents semitones].include?(key.to_sym)
|
40
41
|
end
|
41
42
|
end
|
42
43
|
|
44
|
+
def name
|
45
|
+
identifier.to_s.tr('_', ' ')
|
46
|
+
end
|
47
|
+
|
43
48
|
def representions
|
44
|
-
[identifier, identifier.to_s, name, ascii, unicode, html_entity].
|
49
|
+
[identifier, identifier.to_s, name, ascii, unicode, html_entity].
|
50
|
+
reject { |representation| representation.to_s.strip == '' }
|
45
51
|
end
|
46
52
|
|
47
53
|
def semitones
|
@@ -54,14 +60,13 @@ class HeadMusic::Sign
|
|
54
60
|
|
55
61
|
def <=>(other)
|
56
62
|
other = HeadMusic::Sign.get(other)
|
57
|
-
|
63
|
+
cents <=> other.cents
|
58
64
|
end
|
59
65
|
|
60
66
|
private
|
61
67
|
|
62
68
|
def initialize(attributes)
|
63
69
|
@identifier = attributes[:identifier]
|
64
|
-
@name = attributes[:name]
|
65
70
|
@ascii = attributes[:ascii]
|
66
71
|
@unicode = attributes[:unicode]
|
67
72
|
@html_entity = attributes[:html_entity]
|
data/lib/head_music/spelling.rb
CHANGED
@@ -1,3 +1,8 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Represents the spelling of a pitch, such as C# or Db.
|
4
|
+
# Composite of a LetterName and an optional Sign.
|
5
|
+
# Does not include the octave. See Pitch for that.
|
1
6
|
class HeadMusic::Spelling
|
2
7
|
MATCHER = /^\s*([A-G])(#{HeadMusic::Sign.matcher}?)(\-?\d+)?\s*$/i
|
3
8
|
|
@@ -15,18 +20,17 @@ class HeadMusic::Spelling
|
|
15
20
|
from_name(identifier) || from_number(identifier)
|
16
21
|
end
|
17
22
|
|
18
|
-
def self.
|
23
|
+
def self.matching_string(string)
|
19
24
|
string.to_s.match(MATCHER)
|
20
25
|
end
|
21
26
|
|
22
27
|
def self.from_name(name)
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
end
|
28
|
+
return nil unless matching_string(name)
|
29
|
+
letter_name, sign_string, _octave = matching_string(name).captures
|
30
|
+
letter_name = HeadMusic::LetterName.get(letter_name)
|
31
|
+
return nil unless letter_name
|
32
|
+
sign = HeadMusic::Sign.get(sign_string)
|
33
|
+
fetch_or_create(letter_name, sign)
|
30
34
|
end
|
31
35
|
|
32
36
|
def self.from_number(number)
|
@@ -73,8 +77,8 @@ class HeadMusic::Spelling
|
|
73
77
|
sign && sign == 'b'
|
74
78
|
end
|
75
79
|
|
76
|
-
def ==(
|
77
|
-
other = HeadMusic::Spelling.get(
|
80
|
+
def ==(other)
|
81
|
+
other = HeadMusic::Spelling.get(other)
|
78
82
|
to_s == other.to_s
|
79
83
|
end
|
80
84
|
|
data/lib/head_music/staff.rb
CHANGED
@@ -1,8 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# A staff is a set of lines and spaces that provides context for a pitch
|
1
4
|
class HeadMusic::Staff
|
2
5
|
DEFAULT_LINE_COUNT = 5
|
3
6
|
|
4
7
|
attr_reader :default_clef, :line_count, :instrument
|
5
|
-
|
8
|
+
alias clef default_clef
|
6
9
|
|
7
10
|
def initialize(default_clef, instrument: nil, line_count: nil)
|
8
11
|
@default_clef = HeadMusic::Clef.get(default_clef)
|