head_music 8.0.2 → 8.1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 59fd761fa9c441ada6674ebabb1e83c81991f21b20d2b29812160cf7d1bfd1f6
4
- data.tar.gz: 6d7edeb8c3c8fbcd2abf3069860c7a31bc2c40cc95e38ef718d94ce2821a655d
3
+ metadata.gz: bf272cf25921ce289e80bbbfbb8566178b541ce8cdc4711feb269cb7863f0dd7
4
+ data.tar.gz: 35d4beef32b86a25376136dbaae5f2749ca13b22e2d359a6e83297fadfb6fca0
5
5
  SHA512:
6
- metadata.gz: 605e82fb95ba7e5c91ca2f861bafbf9dd54e94ab02404382a2ae3293a943a85455dd26c40a46d6725fd26c0caeb89c898220d9b572c5a0b22c0bac0a172e31ce
7
- data.tar.gz: 9a920c87fad466335f8b02cff76cd1a202c3a20d9d6e0bc53e91a93648406587e652f0dc47296063d40a26c2fa385a7250f3b6e955c9e99820c513ce97788540
6
+ metadata.gz: 169bdae2dc9223bee17d1dc5cb67baa2b3ea9c80a6a83df055e0ed1707b57ab14834b6193c8c2a03cb317532f243ea2f805a1be981987c5885ca88e08c725c73
7
+ data.tar.gz: 5bea0f2ff5883d4fd8b6d37bdcf1ea49d19469eb23b5da3f733e57524dbbb6299027f9a287b5e2d291f7813429088ba409d0fd045c80da66f482ab49d94ef472
@@ -66,10 +66,14 @@ class HeadMusic::Analysis::DiatonicInterval
66
66
  new(HeadMusic::Rudiment::Pitch.middle_c, higher_pitch)
67
67
  end
68
68
 
69
- def initialize(pitch1, pitch2)
70
- pitch1 = HeadMusic::Rudiment::Pitch.get(pitch1)
71
- pitch2 = HeadMusic::Rudiment::Pitch.get(pitch2)
72
- @lower_pitch, @higher_pitch = [pitch1, pitch2].sort
69
+ def initialize(first_pitch, second_pitch)
70
+ first_pitch = HeadMusic::Rudiment::Pitch.get(first_pitch)
71
+ second_pitch = HeadMusic::Rudiment::Pitch.get(second_pitch)
72
+ @lower_pitch, @higher_pitch = [first_pitch, second_pitch].sort
73
+ end
74
+
75
+ def spans?(pitch)
76
+ pitch >= lower_pitch && pitch <= higher_pitch
73
77
  end
74
78
 
75
79
  def quality
@@ -1,43 +1,22 @@
1
1
  # A module for musical analysis
2
2
  module HeadMusic::Analysis; end
3
3
 
4
- # A melodic interval is the distance between one note and the next.
4
+ # A melodic interval is the distance between two sequential pitches.
5
5
  class HeadMusic::Analysis::MelodicInterval
6
- attr_reader :first_note, :second_note
6
+ attr_reader :first_pitch, :second_pitch
7
7
 
8
- def initialize(note1, note2)
9
- @first_note = note1
10
- @second_note = note2
8
+ def initialize(first, second)
9
+ @first_pitch, @second_pitch = extract_pitches(first, second)
11
10
  end
12
11
 
13
12
  def diatonic_interval
14
13
  @diatonic_interval ||= HeadMusic::Analysis::DiatonicInterval.new(first_pitch, second_pitch)
15
14
  end
16
15
 
17
- def position_start
18
- first_note.position
19
- end
20
-
21
- def position_end
22
- second_note.next_position
23
- end
24
-
25
- def notes
26
- [first_note, second_note]
27
- end
28
-
29
16
  def pitches
30
17
  [first_pitch, second_pitch]
31
18
  end
32
19
 
33
- def first_pitch
34
- @first_pitch ||= first_note.pitch
35
- end
36
-
37
- def second_pitch
38
- @second_pitch ||= second_note.pitch
39
- end
40
-
41
20
  def to_s
42
21
  [direction, diatonic_interval].join(" ")
43
22
  end
@@ -58,10 +37,6 @@ class HeadMusic::Analysis::MelodicInterval
58
37
  !moving?
59
38
  end
60
39
 
61
- def spans?(pitch)
62
- pitch.between?(low_pitch, high_pitch)
63
- end
64
-
65
40
  def high_pitch
66
41
  pitches.max
67
42
  end
@@ -91,10 +66,18 @@ class HeadMusic::Analysis::MelodicInterval
91
66
  end
92
67
 
93
68
  def method_missing(method_name, *args, &block)
94
- respond_to_missing?(method_name) ? diatonic_interval.send(method_name, *args, &block) : super
69
+ diatonic_interval.respond_to?(method_name) ? diatonic_interval.send(method_name, *args, &block) : super
95
70
  end
96
71
 
97
- def respond_to_missing?(method_name, *_args)
98
- diatonic_interval.respond_to?(method_name)
72
+ def respond_to_missing?(method_name, include_private = false)
73
+ diatonic_interval.respond_to?(method_name, include_private) || super
74
+ end
75
+
76
+ private
77
+
78
+ def extract_pitches(first, second)
79
+ first_pitch = first.respond_to?(:pitch) ? first.pitch : first
80
+ second_pitch = second.respond_to?(:pitch) ? second.pitch : second
81
+ [first_pitch, second_pitch]
99
82
  end
100
83
  end
@@ -23,7 +23,7 @@ class HeadMusic::Content::Voice
23
23
  end
24
24
 
25
25
  def notes
26
- @placements.select(&:note?)
26
+ @placements.select(&:note?).sort_by(&:position)
27
27
  end
28
28
 
29
29
  def notes_not_in_key
@@ -59,17 +59,23 @@ class HeadMusic::Content::Voice
59
59
  HeadMusic::Analysis::DiatonicInterval.new(lowest_pitch, highest_pitch)
60
60
  end
61
61
 
62
+ def melodic_note_pairs
63
+ @melodic_note_pairs ||= notes.each_cons(2).map do |note1, note2|
64
+ HeadMusic::Content::Voice::MelodicNotePair.new(note1, note2)
65
+ end
66
+ end
67
+
62
68
  def melodic_intervals
63
69
  @melodic_intervals ||=
64
- notes.each_cons(2).map { |note_pair| HeadMusic::Analysis::MelodicInterval.new(*note_pair) }
70
+ melodic_note_pairs.map { |note_pair| HeadMusic::Analysis::MelodicInterval.new(*note_pair.notes) }
65
71
  end
66
72
 
67
73
  def leaps
68
- melodic_intervals.select(&:leap?)
74
+ melodic_note_pairs.select(&:leap?)
69
75
  end
70
76
 
71
77
  def large_leaps
72
- melodic_intervals.select(&:large_leap?)
78
+ melodic_note_pairs.select(&:large_leap?)
73
79
  end
74
80
 
75
81
  def cantus_firmus?
@@ -120,4 +126,43 @@ class HeadMusic::Content::Voice
120
126
  def pitches_string
121
127
  pitches.first(10).map(&:to_s).join(" ")
122
128
  end
129
+
130
+ class MelodicNotePair
131
+ attr_reader :first_note, :second_note
132
+
133
+ delegate(
134
+ :octave?, :unison?,
135
+ :perfect?,
136
+ :step?, :leap?, :large_leap?,
137
+ :ascending?, :descending?, :repetition?,
138
+ :spans?,
139
+ to: :melodic_interval
140
+ )
141
+
142
+ def initialize(first_note, second_note)
143
+ @first_note = first_note
144
+ @second_note = second_note
145
+ end
146
+
147
+ def notes
148
+ @notes ||= [first_note, second_note]
149
+ end
150
+
151
+ def pitches
152
+ @pitches ||= notes.map(&:pitch)
153
+ end
154
+
155
+ def melodic_interval
156
+ @melodic_interval ||= HeadMusic::Analysis::MelodicInterval.new(*notes)
157
+ end
158
+
159
+ def spells_consonant_triad_with?(other_note_pair)
160
+ return false if step? || other_note_pair.step?
161
+
162
+ combined_pitches = (pitches + other_note_pair.pitches).uniq
163
+ return false if combined_pitches.length < 3
164
+
165
+ HeadMusic::Analysis::PitchSet.new(combined_pitches).consonant_triad?
166
+ end
167
+ end
123
168
  end
@@ -10,6 +10,7 @@ class HeadMusic::Style::Annotation
10
10
  :lowest_pitch,
11
11
  :highest_notes,
12
12
  :lowest_notes,
13
+ :melodic_note_pairs,
13
14
  :melodic_intervals,
14
15
  :notes,
15
16
  :notes_not_in_key,
@@ -6,8 +6,8 @@ class HeadMusic::Style::Guidelines::AlwaysMove < HeadMusic::Style::Annotation
6
6
  MESSAGE = "Always move to a different note."
7
7
 
8
8
  def marks
9
- melodic_intervals
10
- .select { |interval| interval.perfect? && interval.unison? }
11
- .map { |interval| HeadMusic::Style::Mark.for_all(interval.notes) }
9
+ melodic_note_pairs
10
+ .select { |pair| pair.perfect? && pair.unison? }
11
+ .map { |pair| HeadMusic::Style::Mark.for_all(pair.notes) }
12
12
  end
13
13
  end
@@ -6,7 +6,7 @@ class HeadMusic::Style::Guidelines::EndOnTonic < HeadMusic::Style::Annotation
6
6
  MESSAGE = "End on the first scale degree."
7
7
 
8
8
  def marks
9
- HeadMusic::Style::Mark.for(notes.last) if notes.any? && !ends_on_tonic?
9
+ HeadMusic::Style::Mark.for(last_note) if notes.any? && !ends_on_tonic?
10
10
  end
11
11
 
12
12
  private
@@ -16,6 +16,6 @@ class HeadMusic::Style::Guidelines::LimitOctaveLeaps < HeadMusic::Style::Annotat
16
16
  private
17
17
 
18
18
  def octave_leaps
19
- melodic_intervals.select(&:octave?)
19
+ melodic_note_pairs.select(&:octave?)
20
20
  end
21
21
  end
@@ -16,14 +16,14 @@ class HeadMusic::Style::Guidelines::MostlyConjunct < HeadMusic::Style::Annotatio
16
16
  private
17
17
 
18
18
  def marks_for_skips_and_leaps
19
- melodic_intervals
19
+ melodic_note_pairs
20
20
  .reject(&:step?)
21
- .map { |interval| HeadMusic::Style::Mark.for_all(interval.notes, fitness: HeadMusic::SMALL_PENALTY_FACTOR) }
21
+ .map { |note_pair| HeadMusic::Style::Mark.for_all(note_pair.notes, fitness: HeadMusic::SMALL_PENALTY_FACTOR) }
22
22
  end
23
23
 
24
24
  def conjunct_ratio
25
- return 1 if melodic_intervals.empty?
25
+ return 1 if melodic_note_pairs.empty?
26
26
 
27
- melodic_intervals.count(&:step?).to_f / melodic_intervals.length
27
+ melodic_note_pairs.count(&:step?).to_f / melodic_note_pairs.length
28
28
  end
29
29
  end
@@ -6,32 +6,48 @@ class HeadMusic::Style::Guidelines::PrepareOctaveLeaps < HeadMusic::Style::Annot
6
6
  MESSAGE = "Enter and exit an octave leap from within."
7
7
 
8
8
  def marks
9
- (external_entries + external_exits + octave_ending).map do |trouble_spot|
9
+ external_entries_marks + external_exits_marks + octave_ending_marks
10
+ end
11
+
12
+ private
13
+
14
+ def external_entries_marks
15
+ external_entries.map do |trouble_spot|
10
16
  HeadMusic::Style::Mark.for_all(trouble_spot)
11
17
  end
12
18
  end
13
19
 
14
- private
20
+ def external_exits_marks
21
+ external_exits.map do |trouble_spot|
22
+ HeadMusic::Style::Mark.for_all(trouble_spot)
23
+ end
24
+ end
25
+
26
+ def octave_ending_marks
27
+ return [] unless octave_ending?
28
+
29
+ [HeadMusic::Style::Mark.for_all(octave_ending)]
30
+ end
15
31
 
16
32
  def external_entries
17
- melodic_intervals.each_cons(2).map do |pair|
33
+ melodic_note_pairs.each_cons(2).map do |pair|
18
34
  first, second = *pair
19
- pair.map(&:notes).uniq if second.octave? && !second.spans?(first.first_note.pitch)
35
+ pair.map(&:notes).flatten.uniq if second.octave? && !second.spans?(first.pitches.first)
20
36
  end.compact
21
37
  end
22
38
 
23
39
  def external_exits
24
- melodic_intervals.each_cons(2).map do |pair|
40
+ melodic_note_pairs.each_cons(2).map do |pair|
25
41
  first, second = *pair
26
- pair.map(&:notes).uniq if first.octave? && !first.spans?(second.second_note.pitch)
42
+ pair.map(&:notes).flatten.uniq if first.octave? && !first.spans?(second.pitches.last)
27
43
  end.compact
28
44
  end
29
45
 
30
46
  def octave_ending
31
- octave_ending? ? [melodic_intervals.last.notes] : []
47
+ octave_ending? ? melodic_note_pairs.last.notes : []
32
48
  end
33
49
 
34
50
  def octave_ending?
35
- melodic_intervals.last&.octave?
51
+ melodic_note_pairs.last&.octave?
36
52
  end
37
53
  end
@@ -9,35 +9,35 @@ class HeadMusic::Style::Guidelines::RecoverLargeLeaps < HeadMusic::Style::Annota
9
9
  MESSAGE = "Recover large leaps by step in the opposite direction."
10
10
 
11
11
  def marks
12
- melodic_intervals.each_cons(3).map do |intervals|
13
- if unrecovered_leap?(intervals[0], intervals[1], intervals[2])
14
- HeadMusic::Style::Mark.for_all(notes_in_intervals(intervals))
12
+ melodic_note_pairs.each_cons(3).map do |note_pairs|
13
+ if unrecovered_leap?(note_pairs[0], note_pairs[1], note_pairs[2])
14
+ HeadMusic::Style::Mark.for_all(notes_in_note_pairs(note_pairs))
15
15
  end
16
16
  end.compact
17
17
  end
18
18
 
19
19
  private
20
20
 
21
- def notes_in_intervals(intervals)
22
- (intervals[0].notes + intervals[1].notes).uniq
21
+ def notes_in_note_pairs(note_pairs)
22
+ (note_pairs[0].notes + note_pairs[1].notes).uniq
23
23
  end
24
24
 
25
- def unrecovered_leap?(first_interval, second_interval, third_interval)
26
- first_interval.large_leap? &&
27
- !spelling_consonant_triad?(first_interval, second_interval, third_interval) &&
25
+ def unrecovered_leap?(first_note_pair, second_note_pair, third_note_pair)
26
+ first_note_pair.large_leap? &&
27
+ !spelling_consonant_triad?(first_note_pair, second_note_pair, third_note_pair) &&
28
28
  (
29
- !direction_changed?(first_interval, second_interval) ||
30
- !second_interval.step?
29
+ !direction_changed?(first_note_pair, second_note_pair) ||
30
+ !second_note_pair.step?
31
31
  )
32
32
  end
33
33
 
34
- def spelling_consonant_triad?(first_interval, second_interval, third_interval)
35
- first_interval.spells_consonant_triad_with?(second_interval) ||
36
- second_interval.spells_consonant_triad_with?(third_interval)
34
+ def spelling_consonant_triad?(first_note_pair, second_note_pair, third_note_pair)
35
+ first_note_pair.spells_consonant_triad_with?(second_note_pair) ||
36
+ second_note_pair.spells_consonant_triad_with?(third_note_pair)
37
37
  end
38
38
 
39
- def direction_changed?(first_interval, second_interval)
40
- first_interval.ascending? && second_interval.descending? ||
41
- first_interval.descending? && second_interval.ascending?
39
+ def direction_changed?(first_note_pair, second_note_pair)
40
+ first_note_pair.ascending? && second_note_pair.descending? ||
41
+ first_note_pair.descending? && second_note_pair.ascending?
42
42
  end
43
43
  end
@@ -9,14 +9,15 @@ class HeadMusic::Style::Guidelines::SingableIntervals < HeadMusic::Style::Annota
9
9
  MESSAGE = "Use only PU, m2, M2, m3, M3, P4, P5, m6 (ascending), P8 in the melodic line."
10
10
 
11
11
  def marks
12
- melodic_intervals.reject { |interval| permitted?(interval) }.map do |unpermitted_interval|
13
- HeadMusic::Style::Mark.for_all([unpermitted_interval.first_note, unpermitted_interval.second_note])
12
+ melodic_note_pairs.reject { |note_pair| permitted?(note_pair) }.map do |pair_with_unpermitted_interval|
13
+ HeadMusic::Style::Mark.for_all(pair_with_unpermitted_interval.notes)
14
14
  end
15
15
  end
16
16
 
17
17
  private
18
18
 
19
- def permitted?(melodic_interval)
19
+ def permitted?(note_pair)
20
+ melodic_interval = note_pair.melodic_interval
20
21
  whitelist_for_interval(melodic_interval).include?(melodic_interval.shorthand)
21
22
  end
22
23
 
@@ -7,12 +7,12 @@ class HeadMusic::Style::Guidelines::SingleLargeLeaps < HeadMusic::Style::Guideli
7
7
 
8
8
  private
9
9
 
10
- def unrecovered_leap?(first_interval, second_interval, third_interval)
11
- return false unless first_interval.large_leap?
12
- return false if spelling_consonant_triad?(first_interval, second_interval, third_interval)
13
- return false if second_interval.step?
14
- return false if second_interval.repetition?
10
+ def unrecovered_leap?(first_note_pair, second_note_pair, third_note_pair)
11
+ return false unless first_note_pair.large_leap?
12
+ return false if spelling_consonant_triad?(first_note_pair, second_note_pair, third_note_pair)
13
+ return false if second_note_pair.step?
14
+ return false if second_note_pair.repetition?
15
15
 
16
- !direction_changed?(first_interval, second_interval) && second_interval.leap?
16
+ !direction_changed?(first_note_pair, second_note_pair) && second_note_pair.leap?
17
17
  end
18
18
  end
@@ -6,23 +6,23 @@ class HeadMusic::Style::Guidelines::StepOutOfUnison < HeadMusic::Style::Annotati
6
6
  MESSAGE = "Exit a unison by step."
7
7
 
8
8
  def marks
9
- leaps_following_unisons.map do |skip|
10
- HeadMusic::Style::Mark.for_all(skip.notes)
9
+ leaps_following_unisons.map do |note_pair|
10
+ HeadMusic::Style::Mark.for_all(note_pair.notes)
11
11
  end.flatten
12
12
  end
13
13
 
14
14
  private
15
15
 
16
16
  def leaps_following_unisons
17
- melodic_intervals_following_unisons.select(&:leap?)
17
+ melodic_note_pairs_following_unisons.select(&:leap?)
18
18
  end
19
19
 
20
- def melodic_intervals_following_unisons
21
- @melodic_intervals_following_unisons ||=
20
+ def melodic_note_pairs_following_unisons
21
+ @melodic_note_pairs_following_unisons ||=
22
22
  perfect_unisons.map do |unison|
23
23
  note1 = voice.note_at(unison.position)
24
24
  note2 = voice.note_following(unison.position)
25
- HeadMusic::Analysis::MelodicInterval.new(note1, note2) if note1 && note2
25
+ HeadMusic::Content::Voice::MelodicNotePair.new(note1, note2) if note1 && note2
26
26
  end.compact
27
27
  end
28
28
 
@@ -1,3 +1,3 @@
1
1
  module HeadMusic
2
- VERSION = "8.0.2"
2
+ VERSION = "8.1.0"
3
3
  end
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: 8.0.2
4
+ version: 8.1.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: 2025-05-04 00:00:00.000000000 Z
11
+ date: 2025-05-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport