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 +4 -4
- data/lib/head_music/analysis/diatonic_interval.rb +8 -4
- data/lib/head_music/analysis/melodic_interval.rb +15 -32
- data/lib/head_music/content/voice.rb +49 -4
- data/lib/head_music/style/annotation.rb +1 -0
- data/lib/head_music/style/guidelines/always_move.rb +3 -3
- data/lib/head_music/style/guidelines/end_on_tonic.rb +1 -1
- data/lib/head_music/style/guidelines/limit_octave_leaps.rb +1 -1
- data/lib/head_music/style/guidelines/mostly_conjunct.rb +4 -4
- data/lib/head_music/style/guidelines/prepare_octave_leaps.rb +24 -8
- data/lib/head_music/style/guidelines/recover_large_leaps.rb +16 -16
- data/lib/head_music/style/guidelines/singable_intervals.rb +4 -3
- data/lib/head_music/style/guidelines/single_large_leaps.rb +6 -6
- data/lib/head_music/style/guidelines/step_out_of_unison.rb +6 -6
- data/lib/head_music/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: bf272cf25921ce289e80bbbfbb8566178b541ce8cdc4711feb269cb7863f0dd7
|
4
|
+
data.tar.gz: 35d4beef32b86a25376136dbaae5f2749ca13b22e2d359a6e83297fadfb6fca0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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(
|
70
|
-
|
71
|
-
|
72
|
-
@lower_pitch, @higher_pitch = [
|
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
|
4
|
+
# A melodic interval is the distance between two sequential pitches.
|
5
5
|
class HeadMusic::Analysis::MelodicInterval
|
6
|
-
attr_reader :
|
6
|
+
attr_reader :first_pitch, :second_pitch
|
7
7
|
|
8
|
-
def initialize(
|
9
|
-
@
|
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
|
-
|
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,
|
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
|
-
|
70
|
+
melodic_note_pairs.map { |note_pair| HeadMusic::Analysis::MelodicInterval.new(*note_pair.notes) }
|
65
71
|
end
|
66
72
|
|
67
73
|
def leaps
|
68
|
-
|
74
|
+
melodic_note_pairs.select(&:leap?)
|
69
75
|
end
|
70
76
|
|
71
77
|
def large_leaps
|
72
|
-
|
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
|
@@ -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
|
-
|
10
|
-
.select { |
|
11
|
-
.map { |
|
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(
|
9
|
+
HeadMusic::Style::Mark.for(last_note) if notes.any? && !ends_on_tonic?
|
10
10
|
end
|
11
11
|
|
12
12
|
private
|
@@ -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
|
-
|
19
|
+
melodic_note_pairs
|
20
20
|
.reject(&:step?)
|
21
|
-
.map { |
|
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
|
25
|
+
return 1 if melodic_note_pairs.empty?
|
26
26
|
|
27
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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.
|
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
|
-
|
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.
|
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? ?
|
47
|
+
octave_ending? ? melodic_note_pairs.last.notes : []
|
32
48
|
end
|
33
49
|
|
34
50
|
def octave_ending?
|
35
|
-
|
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
|
-
|
13
|
-
if unrecovered_leap?(
|
14
|
-
HeadMusic::Style::Mark.for_all(
|
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
|
22
|
-
(
|
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?(
|
26
|
-
|
27
|
-
!spelling_consonant_triad?(
|
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?(
|
30
|
-
!
|
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?(
|
35
|
-
|
36
|
-
|
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?(
|
40
|
-
|
41
|
-
|
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
|
-
|
13
|
-
HeadMusic::Style::Mark.for_all(
|
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?(
|
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?(
|
11
|
-
return false unless
|
12
|
-
return false if spelling_consonant_triad?(
|
13
|
-
return false if
|
14
|
-
return false if
|
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?(
|
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 |
|
10
|
-
HeadMusic::Style::Mark.for_all(
|
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
|
-
|
17
|
+
melodic_note_pairs_following_unisons.select(&:leap?)
|
18
18
|
end
|
19
19
|
|
20
|
-
def
|
21
|
-
@
|
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::
|
25
|
+
HeadMusic::Content::Voice::MelodicNotePair.new(note1, note2) if note1 && note2
|
26
26
|
end.compact
|
27
27
|
end
|
28
28
|
|
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: 8.0
|
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-
|
11
|
+
date: 2025-05-23 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|