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.
Files changed (86) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +12 -1145
  3. data/Gemfile +8 -2
  4. data/Rakefile +7 -5
  5. data/bin/console +4 -3
  6. data/circle.yml +1 -1
  7. data/head_music.gemspec +19 -17
  8. data/lib/head_music/bar.rb +2 -0
  9. data/lib/head_music/chord.rb +24 -6
  10. data/lib/head_music/circle.rb +5 -0
  11. data/lib/head_music/clef.rb +5 -2
  12. data/lib/head_music/composition.rb +3 -0
  13. data/lib/head_music/consonance.rb +5 -2
  14. data/lib/head_music/functional_interval.rb +84 -47
  15. data/lib/head_music/grand_staff.rb +11 -11
  16. data/lib/head_music/harmonic_interval.rb +13 -7
  17. data/lib/head_music/instrument.rb +9 -6
  18. data/lib/head_music/interval.rb +13 -7
  19. data/lib/head_music/key_signature.rb +8 -5
  20. data/lib/head_music/language.rb +21 -14
  21. data/lib/head_music/letter_name.rb +13 -10
  22. data/lib/head_music/melodic_interval.rb +10 -4
  23. data/lib/head_music/meter.rb +12 -12
  24. data/lib/head_music/motion.rb +12 -9
  25. data/lib/head_music/named_rudiment.rb +22 -20
  26. data/lib/head_music/note.rb +7 -1
  27. data/lib/head_music/octave.rb +9 -7
  28. data/lib/head_music/pitch.rb +54 -27
  29. data/lib/head_music/pitch_class.rb +17 -12
  30. data/lib/head_music/placement.rb +22 -9
  31. data/lib/head_music/position.rb +8 -9
  32. data/lib/head_music/quality.rb +9 -6
  33. data/lib/head_music/rhythm.rb +2 -0
  34. data/lib/head_music/rhythmic_unit.rb +29 -19
  35. data/lib/head_music/rhythmic_value.rb +5 -2
  36. data/lib/head_music/scale.rb +65 -45
  37. data/lib/head_music/scale_degree.rb +9 -6
  38. data/lib/head_music/scale_type.rb +70 -30
  39. data/lib/head_music/sign.rb +18 -13
  40. data/lib/head_music/spelling.rb +14 -10
  41. data/lib/head_music/staff.rb +4 -1
  42. data/lib/head_music/style/analysis.rb +36 -34
  43. data/lib/head_music/style/annotation.rb +14 -13
  44. data/lib/head_music/style/annotations/always_move.rb +7 -6
  45. data/lib/head_music/style/annotations/approach_perfection_contrarily.rb +5 -2
  46. data/lib/head_music/style/annotations/at_least_eight_notes.rb +10 -8
  47. data/lib/head_music/style/annotations/avoid_crossing_voices.rb +11 -8
  48. data/lib/head_music/style/annotations/avoid_overlapping_voices.rb +17 -10
  49. data/lib/head_music/style/annotations/consonant_climax.rb +18 -15
  50. data/lib/head_music/style/annotations/consonant_downbeats.rb +6 -3
  51. data/lib/head_music/style/annotations/diatonic.rb +8 -5
  52. data/lib/head_music/style/annotations/direction_changes.rb +8 -6
  53. data/lib/head_music/style/annotations/end_on_perfect_consonance.rb +5 -5
  54. data/lib/head_music/style/annotations/end_on_tonic.rb +7 -6
  55. data/lib/head_music/style/annotations/frequent_direction_changes.rb +6 -3
  56. data/lib/head_music/style/annotations/limit_octave_leaps.rb +9 -7
  57. data/lib/head_music/style/annotations/moderate_direction_changes.rb +6 -3
  58. data/lib/head_music/style/annotations/mostly_conjunct.rb +8 -5
  59. data/lib/head_music/style/annotations/no_rests.rb +6 -3
  60. data/lib/head_music/style/annotations/no_unisons_in_middle.rb +6 -3
  61. data/lib/head_music/style/annotations/notes_same_length.rb +9 -6
  62. data/lib/head_music/style/annotations/one_to_one.rb +10 -7
  63. data/lib/head_music/style/annotations/prefer_contrary_motion.rb +6 -3
  64. data/lib/head_music/style/annotations/prefer_imperfect.rb +7 -6
  65. data/lib/head_music/style/annotations/prepare_octave_leaps.rb +21 -12
  66. data/lib/head_music/style/annotations/recover_large_leaps.rb +17 -12
  67. data/lib/head_music/style/annotations/singable_intervals.rb +8 -5
  68. data/lib/head_music/style/annotations/singable_range.rb +7 -6
  69. data/lib/head_music/style/annotations/single_large_leaps.rb +6 -3
  70. data/lib/head_music/style/annotations/start_on_perfect_consonance.rb +6 -5
  71. data/lib/head_music/style/annotations/start_on_tonic.rb +6 -5
  72. data/lib/head_music/style/annotations/step_down_to_final_note.rb +12 -12
  73. data/lib/head_music/style/annotations/step_out_of_unison.rb +10 -7
  74. data/lib/head_music/style/annotations/step_to_final_note.rb +6 -3
  75. data/lib/head_music/style/annotations/step_up_to_final_note.rb +12 -12
  76. data/lib/head_music/style/annotations/up_to_fourteen_notes.rb +6 -5
  77. data/lib/head_music/style/mark.rb +10 -4
  78. data/lib/head_music/style/rulesets/first_species_harmony.rb +6 -3
  79. data/lib/head_music/style/rulesets/first_species_melody.rb +6 -3
  80. data/lib/head_music/style/rulesets/fux_cantus_firmus.rb +6 -3
  81. data/lib/head_music/style/rulesets/modern_cantus_firmus.rb +6 -3
  82. data/lib/head_music/utilities/hash_key.rb +9 -7
  83. data/lib/head_music/version.rb +3 -1
  84. data/lib/head_music/voice.rb +7 -3
  85. data/lib/head_music.rb +2 -0
  86. metadata +4 -4
@@ -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
- self.to_s == other.to_s
54
+ to_s == other.to_s
52
55
  end
53
56
 
54
57
  def shorthand
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module HeadMusic::Rhythm
2
4
  PPQN = PULSES_PER_QUARTER_NOTE = 960
3
5
  end
@@ -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 = ['whole', 'half', 'quarter', 'eighth', 'sixteenth', 'thirty-second', 'sixty-fourth', 'hundred twenty-eighth', 'two hundred fifty-sixth']
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[semibreve minim crotchet quaver semiquaver demisemiquaver hemidemisemiquaver semihemidemisemiquaver demisemihemidemisemiquaver]
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
- case relative_value
36
- when 8
37
- :maxima
38
- when 4
39
- :longa
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 has_stem?
55
+ def stemmed?
54
56
  relative_value < 1
55
57
  end
56
58
 
57
59
  def british_name
58
- if MULTIPLES.include?(name)
60
+ if multiple?
59
61
  BRITISH_MULTIPLE_NAMES[MULTIPLES.index(name)]
60
- elsif FRACTIONS.include?(name)
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 =~ /dotted/
29
- modifier, _ = identifier.split(/_*dotted_*/)
31
+ return 0 unless identifier.match?(/dotted/)
32
+ modifier, = identifier.split(/_*dotted_*/)
30
33
  case modifier
31
34
  when /tripl\w/
32
35
  3
@@ -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 { |parent_scale_pitch|
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 && spelling.sign.semitones || 0
21
- usual_sign_semitones = scale_degree_usual_spelling.sign && scale_degree_usual_spelling.sign.semitones || 0
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
- if scale_type.diatonic?
40
- NAME_FOR_DIATONIC_DEGREE[degree] ||
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, WH, 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: [:ionian, :major],
22
+ i: %i[ionian major],
21
23
  ii: [:dorian],
22
24
  iii: [:phrygian],
23
25
  iv: [:lydian],
24
26
  v: [:mixolydian],
25
- vi: [:aeolian, :minor, :natural_minor],
27
+ vi: %i[aeolian minor natural_minor],
26
28
  vii: [:locrian],
27
- }
28
- SCALE_TYPES = {}
29
- MODE_NAMES.each do |roman_numeral, aliases|
30
- intervals = { ascending: const_get(roman_numeral.upcase) }
31
- SCALE_TYPES[roman_numeral] = intervals
32
- aliases.each do |name|
33
- SCALE_TYPES[name.to_sym] = intervals
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
- CHROMATIC = [H, H, H, H, H, H, H, H, H, H, H, H]
40
- SCALE_TYPES[:chromatic] = { ascending: CHROMATIC }
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
- 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 }
52
+ def self._chromatic_scales
53
+ { chromatic: { ascending: CHROMATIC } }
54
+ end
48
55
 
49
- # exotic scales
50
- SCALE_TYPES[:octatonic] = { ascending: [W, H, W, H, W, H, W, H] }
51
- SCALE_TYPES[:whole_tone] = { ascending: [W, W, W, W, W, W] }
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.keys.each do |name|
82
+ SCALE_TYPES.each_key do |name|
55
83
  define_method(name) do
56
- self.get(name)
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
- alias_method :intervals, :ascending_intervals
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
- self.state == other.state
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
@@ -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, :name, :ascii, :unicode, :html_entity, :cents
7
+ attr_reader :identifier, :ascii, :unicode, :html_entity, :cents
5
8
 
6
9
  def self.all
7
10
  @all ||= [
8
- new(identifier: :sharp, name: 'sharp', ascii: '#', unicode: '♯', html_entity: '&#9839;', cents: 100),
9
- new(identifier: :flat, name: 'flat', ascii: 'b', unicode: "", html_entity: '&#9837;', cents: -100),
10
- new(identifier: :natural, name: 'natural', ascii: '', unicode: '♮', html_entity: '&#9838;', cents: 0),
11
- new(identifier: :double_sharp, name: 'double sharp', ascii: '##', unicode: '𝄪', html_entity: '&#119082;', cents: 200),
12
- new(identifier: :double_flat, name: 'double flat', ascii: 'bb', unicode: '𝄫', html_entity: '&#119083;', cents: -200),
11
+ new(identifier: :sharp, ascii: '#', unicode: '♯', html_entity: '&#9839;', cents: 100),
12
+ new(identifier: :flat, ascii: 'b', unicode: '', html_entity: '&#9837;', cents: -100),
13
+ new(identifier: :natural, ascii: '', unicode: '♮', html_entity: '&#9838;', cents: 0),
14
+ new(identifier: :double_sharp, ascii: '##', unicode: '𝄪', html_entity: '&#119082;', cents: 200),
15
+ new(identifier: :double_flat, ascii: 'bb', unicode: '𝄫', html_entity: '&#119083;', 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})$/.match?(candidate)
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].reject { |representation| representation.to_s.strip == '' }
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
- self.cents <=> other.cents
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]
@@ -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.match(string)
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
- if match(name)
24
- letter_name, sign_string, _octave = match(name).captures
25
- letter_name = HeadMusic::LetterName.get(letter_name)
26
- return nil unless letter_name
27
- sign = HeadMusic::Sign.get(sign_string)
28
- fetch_or_create(letter_name, sign)
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 ==(value)
77
- other = HeadMusic::Spelling.get(value)
80
+ def ==(other)
81
+ other = HeadMusic::Spelling.get(other)
78
82
  to_s == other.to_s
79
83
  end
80
84
 
@@ -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
- alias_method :clef, :default_clef
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)