head_music 13.0.0 → 15.0.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: ef07dfecedad63290f5f352704f96967dff09ba5bc305adf04b25c033bf580fb
4
- data.tar.gz: fd20210a40bd733fada3307f6b9b772bd317168132443d164aa417a0d2c490bd
3
+ metadata.gz: 7c29fa7f976d16091512b8bdc9eb8befcb4766c4bb70d387a67991e9b9897a5c
4
+ data.tar.gz: b43c0fed22c3ed7c79a7a0195847740ba7b3bea608a24921893a329f0c66ba7f
5
5
  SHA512:
6
- metadata.gz: cee6694acb9303262ac645153f47462d5e6870e62853bc800d00e731dc0fc5321c00d2e75109c6805a1c2ba0d1b62ddbfdc8196bd445d52062097fd05c24d9eb
7
- data.tar.gz: 8ee30bdeaf48faf0f43411352763c6d67ec3f5919c707054bcdee4ab07514936d09c335a03fa031e7df59479c597aeb4c165765b8c6de5210d9d45edd5639e3a
6
+ metadata.gz: 47d395cc1709167182e0810c7383120395e927629a2e5e5076e197bdacfd377445546a842a032fecabd9865f62e878f969fec301535758072b45db229c80e8e4
7
+ data.tar.gz: '0950f0d5a6a2b830a1733d8d0a7d19eb9fcb87815e25d3e562dc6aa94bc95c16a2d7710ca31890e23d1dcc07fc64247030565741d2edf38da6cb177f7e091fb9'
data/CHANGELOG.md CHANGED
@@ -7,6 +7,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [15.0.0] - 2026-07-06
11
+
12
+ ### Added
13
+
14
+ - `HeadMusic::Style::Guidelines::MinimumMelodicIntervals` — a sufficiency gate on the number of moving melodic intervals, so a line that never (or barely) moves reads as a non-attempt rather than a flawed melody (`MinimumMelodicIntervals.with(2)`). The contour guides use it; `StaticContourMelody` omits it so a repeated single pitch remains a legitimate static contour.
15
+ - `weight:` and `gate:` options on `Annotation.with` — any ruleset entry can now carry a rubric weight or be marked as a gate, and `Configured#with` layers options so presets compose (e.g. `MinimumNotes.with(5).with(gate: true)`)
16
+
17
+ ### Changed
18
+
19
+ - **Breaking:** `Analysis#fitness` is now a gated weighted rubric instead of an unweighted geometric mean: the product of the gate fitnesses multiplies a weighted arithmetic mean of the remaining (rubric) rules. Every fitness value shifts numerically; downstream consumers that compare grades against stored thresholds must recalibrate.
20
+ - Non-attempts now grade zero: sufficiency guidelines (`MinimumNotes`, `MinimumMelodicIntervals`) act as graded gate multipliers, so an empty or insufficient line scales the whole grade down to 0 instead of averaging against the other rules.
21
+ - Contour guides weight `Contoured` at the inverse golden ratio (φ⁻¹ ≈ 0.618) with their ten rubric peers sharing φ⁻² evenly, so a wrong-contour but otherwise perfect line grades exactly ~0.618 (`HeadMusic::GOLDEN_RATIO_INVERSE`)
22
+ - `Diatonic` and `MaximumNotes` are rate-normalized (fitness raised to 1/note-count), so grades are length-invariant: the same violation rate scores the same in an eight-note line as in a sixteen-note line
23
+ - Broken-but-real work now lands on a deliberate soft floor (roughly 0.3–0.55): rate-normalized rules bottom out near φ⁻¹ and the arithmetic mean averages them, so a gate-passing melody that breaks most of the rubric grades substantially below perfect without collapsing toward the gated zero of a non-attempt
24
+
25
+ ## [14.0.0] - 2026-07-05
26
+
27
+ ### Added
28
+
29
+ - `HeadMusic::Style::Guidelines::Contoured` — configurable guideline judging a melody against a chosen contour (`Contoured.with(:arch)` and five other keys: `ascending`, `descending`, `valley`, `wave`, `static`). Predicates are trend-based rather than strictly monotonic; a wrong contour receives a single mark spanning the melody. Unknown contour keys raise `ArgumentError` at guide-definition time.
30
+ - Six contour guides subclassing `Guides::DiatonicMelody`, each appending the configured `Contoured` guideline to the inherited ruleset: `ArchContourMelody`, `AscendingContourMelody`, `DescendingContourMelody`, `StaticContourMelody`, `ValleyContourMelody`, `WaveContourMelody`
31
+ - Contour judgments deliberately complement `ConsonantClimax`: an arch requires only an interior climax pitch level, leaving climax uniqueness and consonance to the existing guideline
32
+
10
33
  ## [13.0.0] - 2026-07-05
11
34
 
12
35
  ### Added
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- head_music (13.0.0)
4
+ head_music (15.0.0)
5
5
  activesupport (>= 7.0, < 10)
6
6
  humanize (~> 2.0)
7
7
  i18n (~> 1.8)
@@ -19,19 +19,34 @@ class HeadMusic::Style::Analysis
19
19
  @annotations ||= @guide.analyze(voice)
20
20
  end
21
21
 
22
+ # The grade: sufficiency gates multiply against a weighted average of the
23
+ # rubric rules, so an insufficient exercise scales the whole grade down
24
+ # while ordinary rules trade off against each other by weight.
22
25
  def fitness
23
26
  return 1.0 if annotations.empty?
24
27
 
25
- @fitness ||= fitness_scores.reduce(:*)**(1.0 / fitness_scores.length)
28
+ @fitness ||= gate_factor * rubric_fitness
26
29
  end
27
30
 
28
31
  def adherent?
29
- fitness == 1
32
+ annotations.all?(&:adherent?)
30
33
  end
31
34
 
32
35
  private
33
36
 
34
- def fitness_scores
35
- @fitness_scores ||= annotations.map(&:fitness)
37
+ def gate_factor
38
+ gates.map(&:fitness).reduce(1, :*)
39
+ end
40
+
41
+ def rubric_fitness
42
+ rubric = annotations.reject(&:gate?)
43
+ total_weight = rubric.sum(&:weight)
44
+ return 1.0 if rubric.empty? || total_weight.zero?
45
+
46
+ rubric.sum { |annotation| annotation.weight * annotation.fitness } / total_weight
47
+ end
48
+
49
+ def gates
50
+ annotations.select(&:gate?)
36
51
  end
37
52
  end
@@ -2,6 +2,8 @@
2
2
  class HeadMusic::Style::Annotation
3
3
  MESSAGE = "Write music."
4
4
 
5
+ DEFAULT_WEIGHT = 1.0
6
+
5
7
  attr_reader :voice
6
8
 
7
9
  delegate(
@@ -34,6 +36,14 @@ class HeadMusic::Style::Annotation
34
36
  Configured.new(self, options)
35
37
  end
36
38
 
39
+ def self.default_weight
40
+ DEFAULT_WEIGHT
41
+ end
42
+
43
+ def self.default_gate?
44
+ false
45
+ end
46
+
37
47
  # A RULESET entry pairing a guideline class with configuration. Quacks like a
38
48
  # class to the analyze loop by responding to #new(voice).
39
49
  class Configured
@@ -48,6 +58,19 @@ class HeadMusic::Style::Annotation
48
58
  guideline_class.new(voice, **options)
49
59
  end
50
60
 
61
+ # Layers additional options onto an already-configured entry, e.g.
62
+ # MinimumNotes.with(5).with(gate: true), without dropping prior options.
63
+ def with(**more)
64
+ Configured.new(guideline_class, options.merge(more))
65
+ end
66
+
67
+ # Mirrors the class-level predicate so build-time RULESET filters can
68
+ # classify any entry (bare class or configured) uniformly. A per-entry
69
+ # gate: option takes precedence over the guideline class's default.
70
+ def default_gate?
71
+ options.fetch(:gate, guideline_class.default_gate?)
72
+ end
73
+
51
74
  def name
52
75
  guideline_class.name
53
76
  end
@@ -56,13 +79,24 @@ class HeadMusic::Style::Annotation
56
79
  end
57
80
 
58
81
  def fitness
59
- [marks].flatten.compact.map(&:fitness).reduce(1, :*)
82
+ mark_fitnesses = [marks].flatten.compact.map(&:fitness)
83
+ return 1.0 if mark_fitnesses.empty?
84
+
85
+ mark_fitnesses.reduce(1, :*)**(1.0 / [fitness_denominator, 1].max)
60
86
  end
61
87
 
62
88
  def adherent?
63
89
  fitness == 1
64
90
  end
65
91
 
92
+ def weight
93
+ options.fetch(:weight, self.class.default_weight)
94
+ end
95
+
96
+ def gate?
97
+ options.fetch(:gate, self.class.default_gate?)
98
+ end
99
+
66
100
  def has_notes?
67
101
  !!first_note
68
102
  end
@@ -91,6 +125,13 @@ class HeadMusic::Style::Annotation
91
125
 
92
126
  attr_reader :options
93
127
 
128
+ # Normalization rate for the product of mark fitnesses. Subclasses override
129
+ # (e.g. with an opportunity count) to score by violation rate rather than
130
+ # raw violation count. The default of 1 preserves the raw product.
131
+ def fitness_denominator
132
+ 1
133
+ end
134
+
94
135
  def voices
95
136
  @voices ||= voice.composition.voices
96
137
  end
@@ -0,0 +1,140 @@
1
+ # Module for style guidelines.
2
+ module HeadMusic::Style::Guidelines; end
3
+
4
+ # Flags a melody without the configured contour
5
+ # Configure the contour with the factory, e.g. Contoured.with(:arch).
6
+ class HeadMusic::Style::Guidelines::Contoured < HeadMusic::Style::Annotation
7
+ CONTOURS = %i[ascending descending arch valley wave static].freeze
8
+
9
+ TREND_REVERSAL_SEMITONES = 3 # a trend reversal must exceed a whole step
10
+
11
+ DEFAULT_WEIGHT = HeadMusic::GOLDEN_RATIO_INVERSE
12
+
13
+ def self.with(contour_key, **options)
14
+ contour = contour_key.to_s.downcase.to_sym
15
+ unless CONTOURS.include?(contour)
16
+ raise ArgumentError, "Contour must be one of: #{CONTOURS.join(", ")} (got #{contour_key.inspect})"
17
+ end
18
+
19
+ super(contour: contour, **options)
20
+ end
21
+
22
+ def self.default_weight
23
+ DEFAULT_WEIGHT
24
+ end
25
+
26
+ def marks
27
+ return if notes.empty? || matches_contour?
28
+
29
+ HeadMusic::Style::Mark.for_all(notes, fitness: HeadMusic::GOLDEN_RATIO_INVERSE**2)
30
+ end
31
+
32
+ def message
33
+ "Write a melody with the #{contour} contour."
34
+ end
35
+
36
+ private
37
+
38
+ # Validated again here because Contoured.new(voice, contour: :bogus) bypasses .with.
39
+ def contour
40
+ @contour ||= begin
41
+ key = options.fetch(:contour)
42
+ contour = key.to_s.downcase.to_sym
43
+ unless CONTOURS.include?(contour)
44
+ raise ArgumentError, "Contour must be one of: #{CONTOURS.join(", ")} (got #{key.inspect})"
45
+ end
46
+
47
+ contour
48
+ end
49
+ end
50
+
51
+ def matches_contour?
52
+ send("#{contour}?")
53
+ end
54
+
55
+ def ascending?
56
+ first_note.pitch == lowest_pitch && last_note.pitch == highest_pitch
57
+ end
58
+
59
+ def descending?
60
+ first_note.pitch == highest_pitch && last_note.pitch == lowest_pitch
61
+ end
62
+
63
+ # The climax is by definition the maximum, so "net rise before, net fall after"
64
+ # is equivalent to both endpoints sitting below the climax pitch.
65
+ # Climax uniqueness and consonance remain ConsonantClimax's job.
66
+ def arch?
67
+ notes.length >= 3 && first_note.pitch < highest_pitch && last_note.pitch < highest_pitch
68
+ end
69
+
70
+ def valley?
71
+ notes.length >= 3 && first_note.pitch > lowest_pitch && last_note.pitch > lowest_pitch
72
+ end
73
+
74
+ def wave?
75
+ trend_directions.length >= 3
76
+ end
77
+
78
+ def static?
79
+ range <= HeadMusic::Analysis::DiatonicInterval.get(:major_third) && !directional_endpoints?
80
+ end
81
+
82
+ # The highest_pitch > lowest_pitch guard is load-bearing: without it, an
83
+ # all-same-pitch melody (first == lowest and last == highest simultaneously)
84
+ # would absurdly fail static.
85
+ def directional_endpoints?
86
+ highest_pitch > lowest_pitch &&
87
+ ((first_note.pitch == lowest_pitch && last_note.pitch == highest_pitch) ||
88
+ (first_note.pitch == highest_pitch && last_note.pitch == lowest_pitch))
89
+ end
90
+
91
+ def pitch_numbers
92
+ @pitch_numbers ||= notes.map { |note| note.pitch.midi_note_number }
93
+ end
94
+
95
+ # Zigzag walk: a trend reversal is confirmed only when the melody retraces at
96
+ # least TREND_REVERSAL_SEMITONES from the running extreme of the current trend,
97
+ # so stepwise neighbor-note undulation never registers as a trend change.
98
+ def trend_directions
99
+ @trend_directions ||= begin
100
+ directions = []
101
+ direction = nil
102
+ high = low = pitch_numbers.first
103
+ extreme = nil
104
+ pitch_numbers.drop(1).each do |number|
105
+ case direction
106
+ when nil # no trend confirmed yet
107
+ if number - low >= TREND_REVERSAL_SEMITONES
108
+ direction = :ascending
109
+ extreme = number
110
+ directions << direction
111
+ elsif high - number >= TREND_REVERSAL_SEMITONES
112
+ direction = :descending
113
+ extreme = number
114
+ directions << direction
115
+ else
116
+ high = [high, number].max
117
+ low = [low, number].min
118
+ end
119
+ when :ascending
120
+ if number > extreme
121
+ extreme = number
122
+ elsif extreme - number >= TREND_REVERSAL_SEMITONES
123
+ direction = :descending
124
+ extreme = number
125
+ directions << direction
126
+ end
127
+ when :descending
128
+ if number < extreme
129
+ extreme = number
130
+ elsif number - extreme >= TREND_REVERSAL_SEMITONES
131
+ direction = :ascending
132
+ extreme = number
133
+ directions << direction
134
+ end
135
+ end
136
+ end
137
+ directions
138
+ end
139
+ end
140
+ end
@@ -9,6 +9,14 @@ class HeadMusic::Style::Guidelines::Diatonic < HeadMusic::Style::Annotation
9
9
  HeadMusic::Style::Mark.for_each(notes_not_in_key_excluding_penultimate_leading_tone)
10
10
  end
11
11
 
12
+ protected
13
+
14
+ # Score by the rate of out-of-key notes rather than the raw count,
15
+ # so fitness is invariant to melody length.
16
+ def fitness_denominator
17
+ notes.length
18
+ end
19
+
12
20
  private
13
21
 
14
22
  def notes_not_in_key_excluding_penultimate_leading_tone
@@ -4,8 +4,8 @@ module HeadMusic::Style::Guidelines; end
4
4
  # Flags a melody with more than the allowed number of notes.
5
5
  # Configure the threshold with the factory, e.g. MaximumNotes.with(14).
6
6
  class HeadMusic::Style::Guidelines::MaximumNotes < HeadMusic::Style::Annotation
7
- def self.with(maximum)
8
- super(maximum: maximum)
7
+ def self.with(maximum, **options)
8
+ super(maximum: maximum, **options)
9
9
  end
10
10
 
11
11
  def marks
@@ -16,6 +16,14 @@ class HeadMusic::Style::Guidelines::MaximumNotes < HeadMusic::Style::Annotation
16
16
  "Write up to #{maximum.humanize} notes."
17
17
  end
18
18
 
19
+ protected
20
+
21
+ # Score by the rate of overage notes rather than the raw count,
22
+ # so fitness is invariant to melody length.
23
+ def fitness_denominator
24
+ notes.length
25
+ end
26
+
19
27
  private
20
28
 
21
29
  def maximum
@@ -0,0 +1,55 @@
1
+ # Module for style guidelines.
2
+ module HeadMusic::Style::Guidelines; end
3
+
4
+ # Flags a melody with fewer than the required number of moving melodic intervals.
5
+ # Repeated-note pairs don't count as motion, so an all-repeated-note line gates to 0.
6
+ # Configure the threshold with the factory, e.g. MinimumMelodicIntervals.with(2).
7
+ class HeadMusic::Style::Guidelines::MinimumMelodicIntervals < HeadMusic::Style::Annotation
8
+ def self.with(minimum, **options)
9
+ super(minimum: minimum, **options)
10
+ end
11
+
12
+ def self.default_gate?
13
+ true
14
+ end
15
+
16
+ def marks
17
+ return no_motion_mark if moving_intervals.empty?
18
+
19
+ deficiency_mark
20
+ end
21
+
22
+ def message
23
+ "Write at least #{minimum.humanize} melodic intervals."
24
+ end
25
+
26
+ private
27
+
28
+ def minimum
29
+ options.fetch(:minimum)
30
+ end
31
+
32
+ def moving_intervals
33
+ melodic_intervals.select(&:moving?)
34
+ end
35
+
36
+ def no_motion_mark
37
+ return no_placements_mark if placements.empty?
38
+
39
+ HeadMusic::Style::Mark.for_all(placements, fitness: 0)
40
+ end
41
+
42
+ def no_placements_mark
43
+ HeadMusic::Style::Mark.new(
44
+ HeadMusic::Content::Position.new(composition, "1:1"),
45
+ HeadMusic::Content::Position.new(composition, "2:1"),
46
+ fitness: 0
47
+ )
48
+ end
49
+
50
+ def deficiency_mark
51
+ return unless moving_intervals.length < minimum
52
+
53
+ HeadMusic::Style::Mark.for_all(placements, fitness: moving_intervals.length.to_f / minimum)
54
+ end
55
+ end
@@ -4,8 +4,12 @@ module HeadMusic::Style::Guidelines; end
4
4
  # Flags a melody with fewer than the required number of notes.
5
5
  # Configure the threshold with the factory, e.g. MinimumNotes.with(8).
6
6
  class HeadMusic::Style::Guidelines::MinimumNotes < HeadMusic::Style::Annotation
7
- def self.with(minimum)
8
- super(minimum: minimum)
7
+ def self.with(minimum, **options)
8
+ super(minimum: minimum, **options)
9
+ end
10
+
11
+ def self.default_gate?
12
+ true
9
13
  end
10
14
 
11
15
  def marks
@@ -0,0 +1,4 @@
1
+ # A free diatonic melody with an arch contour (interior climax).
2
+ class HeadMusic::Style::Guides::ArchContourMelody < HeadMusic::Style::Guides::DiatonicMelody
3
+ RULESET = contour_ruleset(:arch, minimum_melodic_intervals: 2)
4
+ end
@@ -0,0 +1,4 @@
1
+ # A free diatonic melody with an ascending contour (departs its floor, arrives at its ceiling).
2
+ class HeadMusic::Style::Guides::AscendingContourMelody < HeadMusic::Style::Guides::DiatonicMelody
3
+ RULESET = contour_ruleset(:ascending, minimum_melodic_intervals: 1)
4
+ end
@@ -0,0 +1,4 @@
1
+ # A free diatonic melody with a descending contour (departs its ceiling, arrives at its floor).
2
+ class HeadMusic::Style::Guides::DescendingContourMelody < HeadMusic::Style::Guides::DiatonicMelody
3
+ RULESET = contour_ruleset(:descending, minimum_melodic_intervals: 1)
4
+ end
@@ -18,4 +18,28 @@ class HeadMusic::Style::Guides::DiatonicMelody < HeadMusic::Style::Guides::Speci
18
18
  HeadMusic::Style::Guidelines::MinimumNotes.with(5),
19
19
  HeadMusic::Style::Guidelines::MaximumNotes.with(32)
20
20
  ].freeze
21
+
22
+ # The non-gate peers of a contour guide share phi^-2 of rubric weight, so
23
+ # that with Contoured at its default weight of phi^-1 (and phi^-1 + phi^-2
24
+ # = 1), a wrong contour on an otherwise perfect line grades exactly phi^-1.
25
+ CONTOUR_PEER_WEIGHT_BUDGET = HeadMusic::GOLDEN_RATIO_INVERSE**2
26
+
27
+ # Builds a contour guide's ruleset from this guide's rules: gate entries
28
+ # pass through unchanged, non-gate peers split the peer weight budget
29
+ # evenly, and the contour guideline is appended at its own default weight.
30
+ # An optional motion gate excludes non-attempts (nil omits it, so a static
31
+ # contour can legitimately repeat a single pitch).
32
+ def self.contour_ruleset(contour_key, minimum_melodic_intervals: nil)
33
+ gates, peers = RULESET.partition(&:default_gate?)
34
+ peer_weight = CONTOUR_PEER_WEIGHT_BUDGET / peers.length
35
+ motion_gate =
36
+ minimum_melodic_intervals &&
37
+ HeadMusic::Style::Guidelines::MinimumMelodicIntervals.with(minimum_melodic_intervals)
38
+ [
39
+ *gates,
40
+ motion_gate,
41
+ *peers.map { |rule| rule.with(weight: peer_weight) },
42
+ HeadMusic::Style::Guidelines::Contoured.with(contour_key)
43
+ ].compact.freeze
44
+ end
21
45
  end
@@ -0,0 +1,5 @@
1
+ # A free diatonic melody with a static contour (narrow range, non-directional endpoints).
2
+ # No motion gate: an all-repeated-note line is a legitimate static contour.
3
+ class HeadMusic::Style::Guides::StaticContourMelody < HeadMusic::Style::Guides::DiatonicMelody
4
+ RULESET = contour_ruleset(:static)
5
+ end
@@ -0,0 +1,4 @@
1
+ # A free diatonic melody with a valley contour (interior nadir).
2
+ class HeadMusic::Style::Guides::ValleyContourMelody < HeadMusic::Style::Guides::DiatonicMelody
3
+ RULESET = contour_ruleset(:valley, minimum_melodic_intervals: 2)
4
+ end
@@ -0,0 +1,4 @@
1
+ # A free diatonic melody with a wave contour (repeated undulation at the trend level).
2
+ class HeadMusic::Style::Guides::WaveContourMelody < HeadMusic::Style::Guides::DiatonicMelody
3
+ RULESET = contour_ruleset(:wave, minimum_melodic_intervals: 2)
4
+ end
@@ -1,3 +1,3 @@
1
1
  module HeadMusic
2
- VERSION = "13.0.0"
2
+ VERSION = "15.0.0"
3
3
  end
data/lib/head_music.rb CHANGED
@@ -154,11 +154,13 @@ require "head_music/style/mark"
154
154
  # style guidelines
155
155
  require "head_music/style/guidelines/always_move"
156
156
  require "head_music/style/guidelines/approach_perfection_contrarily"
157
+ require "head_music/style/guidelines/minimum_melodic_intervals"
157
158
  require "head_music/style/guidelines/minimum_notes"
158
159
  require "head_music/style/guidelines/avoid_crossing_voices"
159
160
  require "head_music/style/guidelines/avoid_overlapping_voices"
160
161
  require "head_music/style/guidelines/consonant_climax"
161
162
  require "head_music/style/guidelines/consonant_downbeats"
163
+ require "head_music/style/guidelines/contoured"
162
164
  require "head_music/style/guidelines/diatonic"
163
165
  require "head_music/style/guidelines/direction_changes"
164
166
  require "head_music/style/guidelines/end_on_perfect_consonance"
@@ -219,6 +221,12 @@ require "head_music/style/guides/fux_cantus_firmus"
219
221
  require "head_music/style/guides/salzer_schachter_cantus_firmus"
220
222
  require "head_music/style/guides/first_species_melody"
221
223
  require "head_music/style/guides/diatonic_melody"
224
+ require "head_music/style/guides/arch_contour_melody"
225
+ require "head_music/style/guides/ascending_contour_melody"
226
+ require "head_music/style/guides/descending_contour_melody"
227
+ require "head_music/style/guides/static_contour_melody"
228
+ require "head_music/style/guides/valley_contour_melody"
229
+ require "head_music/style/guides/wave_contour_melody"
222
230
  require "head_music/style/guides/species_harmony"
223
231
  require "head_music/style/guides/first_species_harmony"
224
232
  require "head_music/style/guides/second_species_melody"