head_music 12.5.0 → 12.6.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.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +110 -0
  3. data/CLAUDE.md +2 -2
  4. data/Gemfile.lock +2 -1
  5. data/Rakefile +2 -2
  6. data/lib/head_music/style/annotation.rb +31 -1
  7. data/lib/head_music/style/guidelines/at_least_eight_notes.rb +2 -24
  8. data/lib/head_music/style/guidelines/direction_changes.rb +10 -2
  9. data/lib/head_music/style/guidelines/four_per_bar.rb +2 -12
  10. data/lib/head_music/style/guidelines/frequent_direction_changes.rb +1 -4
  11. data/lib/head_music/style/guidelines/limit_octave_leaps.rb +11 -2
  12. data/lib/head_music/style/guidelines/maximum_notes.rb +24 -0
  13. data/lib/head_music/style/guidelines/minimum_notes.rb +34 -0
  14. data/lib/head_music/style/guidelines/moderate_direction_changes.rb +1 -4
  15. data/lib/head_music/style/guidelines/mostly_conjunct.rb +7 -2
  16. data/lib/head_music/style/guidelines/note_count_per_bar.rb +26 -1
  17. data/lib/head_music/style/guidelines/one_per_bar.rb +2 -12
  18. data/lib/head_music/style/guidelines/second_species_break.rb +5 -1
  19. data/lib/head_music/style/guidelines/singable_range.rb +18 -2
  20. data/lib/head_music/style/guidelines/three_per_bar.rb +2 -12
  21. data/lib/head_music/style/guidelines/two_per_bar.rb +2 -12
  22. data/lib/head_music/style/guidelines/up_to_fourteen_notes.rb +2 -14
  23. data/lib/head_music/style/guides/base.rb +10 -0
  24. data/lib/head_music/style/guides/combined_first_second_third_species_harmony.rb +1 -7
  25. data/lib/head_music/style/guides/combined_first_second_third_species_melody.rb +4 -9
  26. data/lib/head_music/style/guides/diatonic_melody.rb +11 -0
  27. data/lib/head_music/style/guides/fifth_species_harmony.rb +2 -8
  28. data/lib/head_music/style/guides/fifth_species_melody.rb +5 -10
  29. data/lib/head_music/style/guides/first_species_harmony.rb +2 -8
  30. data/lib/head_music/style/guides/first_species_melody.rb +2 -7
  31. data/lib/head_music/style/guides/fourth_species_harmony.rb +1 -7
  32. data/lib/head_music/style/guides/fourth_species_melody.rb +4 -9
  33. data/lib/head_music/style/guides/fux_cantus_firmus.rb +1 -6
  34. data/lib/head_music/style/guides/modern_cantus_firmus.rb +1 -6
  35. data/lib/head_music/style/guides/second_species_harmony.rb +1 -7
  36. data/lib/head_music/style/guides/second_species_melody.rb +4 -9
  37. data/lib/head_music/style/guides/species_harmony.rb +14 -5
  38. data/lib/head_music/style/guides/species_melody.rb +13 -5
  39. data/lib/head_music/style/guides/third_species_harmony.rb +1 -7
  40. data/lib/head_music/style/guides/third_species_melody.rb +4 -9
  41. data/lib/head_music/style/guides/third_species_triple_meter_harmony.rb +1 -7
  42. data/lib/head_music/style/guides/third_species_triple_meter_melody.rb +4 -9
  43. data/lib/head_music/version.rb +1 -1
  44. data/lib/head_music.rb +5 -0
  45. metadata +7 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4a02a5ef281eecc76bcf953aa51c04078858b4588f2b85c0b1a4d1f4d88464d6
4
- data.tar.gz: 5a31a94133ffbb97944524d75aedcbbfcbf7ce769fe6049559818a1940438442
3
+ metadata.gz: db91f6ff65b081bb369f3d2a85e2bc6e6cd092bb0018c2c416bf36bcfc4d4e9d
4
+ data.tar.gz: 266914dfbc382dc27591ea37302b57571b8693f2e0e4bd5189d2fffdff2a7c00
5
5
  SHA512:
6
- metadata.gz: 369c959ae6f69cd609a65e4dee6b4aace0283d107132fec61f3b35afadfeb27d5386c60e969d88cfe626bdc0873e3c5dbed736513d93523e68c5dfca4127b6f1
7
- data.tar.gz: 0f04cf766ce824188270ad00fe29866f652befcb0ff0d5c3eeaadd21e286b088bfb8883fe8687f94c0eaf2e216c5a2ee0377afc91756df18e9e587b5fef0eb41
6
+ metadata.gz: d53ea28ee01b301ae1070a94fe8929d00b649c7d581976183d20e0bbfd080ccf4c08003719f2280001fb7d6ed00cfe1a5a634bd69b046d5859db56dfab6c6bb5
7
+ data.tar.gz: 9cd2929bddff0033d3caa84832e97a77a920cfc61e4e2d55d4d0be338f1ef7904beaad73748f6d6cc4ef93aea5a3b06292e16b285d0b61d5ad78b4ba2818932b
data/CHANGELOG.md CHANGED
@@ -7,6 +7,116 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [12.6.0] - 2026-07-03
11
+
12
+ ### Added
13
+
14
+ - `DiatonicMelody` guide: a free diatonic melody not bound to cantus firmus start/end constraints (note-count range configurable, defaulting to 5–24)
15
+ - Configurable guidelines — a guideline can now carry configuration into a `RULESET` via `Annotation.with(...)` (wrapped in `Annotation::Configured`):
16
+ - `MinimumNotes` / `MaximumNotes` — configurable note-count floor and ceiling (`AtLeastEightNotes` / `UpToFourteenNotes` retained as named defaults)
17
+ - `NoteCountPerBar` — configurable `count` and `rhythmic_value` (unifies `OnePerBar`, `TwoPerBar`, `ThreePerBar`, `FourPerBar`)
18
+ - `DirectionChanges` — configurable `maximum_notes_per_direction` (unifies `ModerateDirectionChanges` and `FrequentDirectionChanges`)
19
+ - Configurable thresholds on `SingableRange`, `MostlyConjunct`, `LimitOctaveLeaps`, and `SecondSpeciesBreak`
20
+
21
+ ### Changed
22
+
23
+ - Extracted `HeadMusic::Style::Guides::Base` for shared guide analysis behavior; `SpeciesMelody` and `SpeciesHarmony` now inherit from it
24
+ - Hoisted the guidelines common to every guide into `MELODIC_CORE` / `HARMONIC_CORE` constants on the species base classes
25
+ - `SingableRange`'s message now reflects the configured range
26
+ - Renamed the `quality` rake task to `validate`
27
+
28
+ ## [12.5.0] - 2026-04-08
29
+
30
+ ### Changed
31
+
32
+ - Improved fifth-species counterpoint guidelines
33
+ - Code quality improvements
34
+
35
+ ### Removed
36
+
37
+ - Combined 2+3+4 species guides and their guidelines
38
+
39
+ ## [12.4.0] - 2026-04-06
40
+
41
+ ### Added
42
+
43
+ - Fifth-species (florid) counterpoint guides
44
+ - Standard and alternate instrument tunings
45
+
46
+ ### Fixed
47
+
48
+ - Ukulele family stringings, tunings, and range data
49
+
50
+ ## [12.3.0] - 2026-02-25
51
+
52
+ ### Added
53
+
54
+ - Fourth-species counterpoint guides
55
+
56
+ ### Changed
57
+
58
+ - Code quality pass
59
+
60
+ ## [12.2.0] - 2026-02-24
61
+
62
+ ### Changed
63
+
64
+ - Improved guidelines for first-bar entry
65
+
66
+ ## [12.1.0] - 2026-02-24
67
+
68
+ ### Changed
69
+
70
+ - Refactored species guidelines into separate first-bar, middle-bar, and final-bar rules
71
+ - Unified `FinalBarWholeNote` and `FinalBarDottedHalfNote` into `NoteFillsFinalBar`
72
+
73
+ ## [12.0.1] - 2026-02-23
74
+
75
+ ### Changed
76
+
77
+ - Renamed triple-meter guides
78
+
79
+ ## [12.0.0] - 2026-02-21
80
+
81
+ ### Added
82
+
83
+ - Third-species 3:1 guidelines
84
+ - Allow a descending minor sixth as a singable interval
85
+
86
+ ### Changed
87
+
88
+ - Extracted shared base classes for guides and step-to-final-note guidelines
89
+ - Refactored third-species dissonance handling and other files to reduce code smells
90
+
91
+ ## [11.8.0] - 2026-02-16
92
+
93
+ ### Added
94
+
95
+ - `ThirdSpeciesMelody` and `ThirdSpeciesHarmony` guides
96
+ - `ThirdSpeciesDissonanceTreatment` guideline
97
+ - `FourToOne` guideline
98
+ - Third-species counterpoint reference document
99
+
100
+ ## [11.7.0] - 2026-02-13
101
+
102
+ ### Added
103
+
104
+ - Parallel-perfect check for first species
105
+
106
+ ### Changed
107
+
108
+ - `Analysis#fitness` now uses the geometric mean
109
+
110
+ ## [11.6.1] - 2026-02-12
111
+
112
+ ### Added
113
+
114
+ - Additional test coverage for the two-to-one guideline
115
+
116
+ ### Fixed
117
+
118
+ - Accept an implied rest in the first bar of second-species counterpoint
119
+
10
120
  ## [11.6.0] - 2026-02-10
11
121
 
12
122
  ### Added
data/CLAUDE.md CHANGED
@@ -26,8 +26,8 @@ bundle exec rspec spec/head_music/rudiments/pitch_spec.rb
26
26
  # Run linting
27
27
  bundle exec rubocop
28
28
 
29
- # Run all quality checks (tests, linting, security)
30
- bundle exec rake quality
29
+ # Run all validation checks (tests, linting, security)
30
+ bundle exec rake validate
31
31
 
32
32
  # Open interactive console with gem loaded
33
33
  bin/console
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- head_music (12.5.0)
4
+ head_music (12.6.0)
5
5
  activesupport (>= 7.0, < 10)
6
6
  humanize (~> 2.0)
7
7
  i18n (~> 1.8)
@@ -211,6 +211,7 @@ GEM
211
211
  PLATFORMS
212
212
  arm64-darwin-22
213
213
  arm64-darwin-23
214
+ arm64-darwin-25
214
215
  x86_64-linux
215
216
 
216
217
  DEPENDENCIES
data/Rakefile CHANGED
@@ -33,8 +33,8 @@ end
33
33
 
34
34
  task default: :spec
35
35
 
36
- desc "Run all quality checks (tests, linting, security audit, code quality)"
37
- task quality: [:spec, :standard, "bundle:audit:check", :rubycritic]
36
+ desc "Run all validation checks (tests, linting, security audit, code quality)"
37
+ task validate: [:spec, :standard, "bundle:audit:check", :rubycritic]
38
38
 
39
39
  desc "Open an irb session preloaded with this library"
40
40
  task :console do
@@ -23,8 +23,36 @@ class HeadMusic::Style::Annotation
23
23
  delegate :key_signature, to: :composition
24
24
  delegate :tonic_spelling, to: :key_signature
25
25
 
26
- def initialize(voice)
26
+ def initialize(voice, **options)
27
27
  @voice = voice
28
+ @options = options
29
+ end
30
+
31
+ # Wraps a guideline class with preset options so it can live in a RULESET
32
+ # and still be instantiated with just a voice, e.g. MinimumNotes.with(minimum: 5).
33
+ def self.with(**options)
34
+ Configured.new(self, options)
35
+ end
36
+
37
+ # A RULESET entry pairing a guideline class with configuration. Quacks like a
38
+ # class to the analyze loop by responding to #new(voice).
39
+ class Configured
40
+ attr_reader :guideline_class, :options
41
+
42
+ def initialize(guideline_class, options)
43
+ @guideline_class = guideline_class
44
+ @options = options
45
+ end
46
+
47
+ def new(voice)
48
+ guideline_class.new(voice, **options)
49
+ end
50
+
51
+ def name
52
+ guideline_class.name
53
+ end
54
+ alias_method :to_s, :name
55
+ alias_method :inspect, :name
28
56
  end
29
57
 
30
58
  def fitness
@@ -61,6 +89,8 @@ class HeadMusic::Style::Annotation
61
89
 
62
90
  protected
63
91
 
92
+ attr_reader :options
93
+
64
94
  def voices
65
95
  @voices ||= voice.composition.voices
66
96
  end
@@ -1,29 +1,7 @@
1
1
  # Module for style guidelines.
2
2
  module HeadMusic::Style::Guidelines; end
3
3
 
4
- # A counterpoint guideline
5
- class HeadMusic::Style::Guidelines::AtLeastEightNotes < HeadMusic::Style::Annotation
4
+ # A counterpoint guideline: the cantus firmus should have at least eight notes.
5
+ class HeadMusic::Style::Guidelines::AtLeastEightNotes < HeadMusic::Style::Guidelines::MinimumNotes
6
6
  MINIMUM_NOTES = 8
7
-
8
- MESSAGE = "Write at least eight notes."
9
-
10
- def marks
11
- placements.empty? ? no_placements_mark : deficiency_mark
12
- end
13
-
14
- private
15
-
16
- def no_placements_mark
17
- HeadMusic::Style::Mark.new(
18
- HeadMusic::Content::Position.new(composition, "1:1"),
19
- HeadMusic::Content::Position.new(composition, "2:1"),
20
- fitness: 0
21
- )
22
- end
23
-
24
- def deficiency_mark
25
- return unless notes.length < MINIMUM_NOTES
26
-
27
- HeadMusic::Style::Mark.for_all(placements, fitness: notes.length.to_f / MINIMUM_NOTES)
28
- end
29
7
  end
@@ -1,8 +1,12 @@
1
1
  # Module for style guidelines.
2
2
  module HeadMusic::Style::Guidelines; end
3
3
 
4
- # A melodic line should have direction changes.
4
+ # A melodic line should change direction often enough. Configurable via the
5
+ # `maximum_notes_per_direction:` option; subclasses may set a
6
+ # MAXIMUM_NOTES_PER_DIRECTION default and their own MESSAGE.
5
7
  class HeadMusic::Style::Guidelines::DirectionChanges < HeadMusic::Style::Annotation
8
+ MESSAGE = "Change melodic direction regularly."
9
+
6
10
  def marks
7
11
  return unless overage.positive?
8
12
 
@@ -15,7 +19,11 @@ class HeadMusic::Style::Guidelines::DirectionChanges < HeadMusic::Style::Annotat
15
19
  def overage
16
20
  return 0 if notes.length < 2
17
21
 
18
- [notes_per_direction - self.class.maximum_notes_per_direction, 0].max
22
+ [notes_per_direction - maximum_notes_per_direction, 0].max
23
+ end
24
+
25
+ def maximum_notes_per_direction
26
+ options.fetch(:maximum_notes_per_direction) { self.class::MAXIMUM_NOTES_PER_DIRECTION }
19
27
  end
20
28
 
21
29
  def notes_per_direction
@@ -3,16 +3,6 @@ module HeadMusic::Style::Guidelines; end
3
3
 
4
4
  # Checks that each middle bar contains exactly four quarter notes.
5
5
  class HeadMusic::Style::Guidelines::FourPerBar < HeadMusic::Style::Guidelines::NoteCountPerBar
6
- MESSAGE = "Use four quarter notes in each middle bar."
7
-
8
- QUARTER = HeadMusic::Rudiment::RhythmicValue.get(:quarter)
9
-
10
- private
11
-
12
- def check_middle_bar(bar_number)
13
- bar_notes = notes_in_bar(bar_number)
14
- return if bar_notes.length == 4 && bar_notes.all? { |note| note.rhythmic_value == QUARTER }
15
-
16
- mark_bar(bar_number)
17
- end
6
+ COUNT = 4
7
+ RHYTHMIC_VALUE = :quarter
18
8
  end
@@ -4,8 +4,5 @@ module HeadMusic::Style::Guidelines; end
4
4
  # A counterpoint guideline
5
5
  class HeadMusic::Style::Guidelines::FrequentDirectionChanges < HeadMusic::Style::Guidelines::DirectionChanges
6
6
  MESSAGE = "Change melodic direction frequently."
7
-
8
- def self.maximum_notes_per_direction
9
- 3
10
- end
7
+ MAXIMUM_NOTES_PER_DIRECTION = 3
11
8
  end
@@ -2,19 +2,28 @@
2
2
  module HeadMusic::Style::Guidelines; end
3
3
 
4
4
  # A counterpoint guideline: Use a maximum of one octave leap.
5
+ # Configurable via the `maximum_leaps:` option.
5
6
  class HeadMusic::Style::Guidelines::LimitOctaveLeaps < HeadMusic::Style::Annotation
6
- MESSAGE = "Use a maximum of one octave leap."
7
+ MAXIMUM_LEAPS = 1
7
8
 
8
9
  def marks
9
- return if octave_leaps.length <= 1
10
+ return if octave_leaps.length <= maximum_leaps
10
11
 
11
12
  octave_leaps.map do |leap|
12
13
  HeadMusic::Style::Mark.for_all(leap.notes)
13
14
  end
14
15
  end
15
16
 
17
+ def message
18
+ "Use a maximum of #{maximum_leaps.humanize} octave #{(maximum_leaps == 1) ? "leap" : "leaps"}."
19
+ end
20
+
16
21
  private
17
22
 
23
+ def maximum_leaps
24
+ options.fetch(:maximum_leaps) { self.class::MAXIMUM_LEAPS }
25
+ end
26
+
18
27
  def octave_leaps
19
28
  melodic_note_pairs.select(&:octave?)
20
29
  end
@@ -0,0 +1,24 @@
1
+ # Module for style guidelines.
2
+ module HeadMusic::Style::Guidelines; end
3
+
4
+ # Flags a melody with more than the allowed number of notes.
5
+ # Configurable via the `maximum:` option; subclasses may set a MAXIMUM_NOTES default.
6
+ class HeadMusic::Style::Guidelines::MaximumNotes < HeadMusic::Style::Annotation
7
+ def marks
8
+ HeadMusic::Style::Mark.for_each(notes[maximum..]) if overage.positive?
9
+ end
10
+
11
+ def message
12
+ "Write up to #{maximum.humanize} notes."
13
+ end
14
+
15
+ private
16
+
17
+ def maximum
18
+ options.fetch(:maximum) { self.class::MAXIMUM_NOTES }
19
+ end
20
+
21
+ def overage
22
+ [notes.length - maximum, 0].max
23
+ end
24
+ end
@@ -0,0 +1,34 @@
1
+ # Module for style guidelines.
2
+ module HeadMusic::Style::Guidelines; end
3
+
4
+ # Flags a melody with fewer than the required number of notes.
5
+ # Configurable via the `minimum:` option; subclasses may set a MINIMUM_NOTES default.
6
+ class HeadMusic::Style::Guidelines::MinimumNotes < HeadMusic::Style::Annotation
7
+ def marks
8
+ placements.empty? ? no_placements_mark : deficiency_mark
9
+ end
10
+
11
+ def message
12
+ "Write at least #{minimum.humanize} notes."
13
+ end
14
+
15
+ private
16
+
17
+ def minimum
18
+ options.fetch(:minimum) { self.class::MINIMUM_NOTES }
19
+ end
20
+
21
+ def no_placements_mark
22
+ HeadMusic::Style::Mark.new(
23
+ HeadMusic::Content::Position.new(composition, "1:1"),
24
+ HeadMusic::Content::Position.new(composition, "2:1"),
25
+ fitness: 0
26
+ )
27
+ end
28
+
29
+ def deficiency_mark
30
+ return unless notes.length < minimum
31
+
32
+ HeadMusic::Style::Mark.for_all(placements, fitness: notes.length.to_f / minimum)
33
+ end
34
+ end
@@ -4,8 +4,5 @@ module HeadMusic::Style::Guidelines; end
4
4
  # A counterpoint guideline
5
5
  class HeadMusic::Style::Guidelines::ModerateDirectionChanges < HeadMusic::Style::Guidelines::DirectionChanges
6
6
  MESSAGE = "Change melodic direction occasionally."
7
-
8
- def self.maximum_notes_per_direction
9
- 5
10
- end
7
+ MAXIMUM_NOTES_PER_DIRECTION = 5
11
8
  end
@@ -1,7 +1,8 @@
1
1
  # Module for style guidelines.
2
2
  module HeadMusic::Style::Guidelines; end
3
3
 
4
- # A counterpoint guideline
4
+ # A counterpoint guideline. Configurable via the `minimum_conjunct_portion:`
5
+ # option (the minimum fraction of melodic motion that must be stepwise).
5
6
  class HeadMusic::Style::Guidelines::MostlyConjunct < HeadMusic::Style::Annotation
6
7
  MESSAGE = "Use mostly conjunct motion."
7
8
 
@@ -10,11 +11,15 @@ class HeadMusic::Style::Guidelines::MostlyConjunct < HeadMusic::Style::Annotatio
10
11
  # Fux is 5/13 for lydian cantus firmus
11
12
 
12
13
  def marks
13
- marks_for_skips_and_leaps if conjunct_ratio < MINIMUM_CONJUNCT_PORTION
14
+ marks_for_skips_and_leaps if conjunct_ratio < minimum_conjunct_portion
14
15
  end
15
16
 
16
17
  private
17
18
 
19
+ def minimum_conjunct_portion
20
+ options.fetch(:minimum_conjunct_portion) { self.class::MINIMUM_CONJUNCT_PORTION }
21
+ end
22
+
18
23
  def marks_for_skips_and_leaps
19
24
  melodic_note_pairs
20
25
  .reject(&:step?)
@@ -1,7 +1,9 @@
1
1
  # Module for style guidelines.
2
2
  module HeadMusic::Style::Guidelines; end
3
3
 
4
- # Base class for guidelines that check note counts in middle bars (not first or last).
4
+ # Checks that each middle bar (not first or last) contains an exact number of
5
+ # notes of a given rhythmic value. Configurable via the `count:` and
6
+ # `rhythmic_value:` options; subclasses may set COUNT and RHYTHMIC_VALUE defaults.
5
7
  class HeadMusic::Style::Guidelines::NoteCountPerBar < HeadMusic::Style::Annotation
6
8
  def marks
7
9
  return [] unless cantus_firmus&.notes&.any?
@@ -9,8 +11,31 @@ class HeadMusic::Style::Guidelines::NoteCountPerBar < HeadMusic::Style::Annotati
9
11
  middle_bars.filter_map { |bar_number| check_middle_bar(bar_number) }
10
12
  end
11
13
 
14
+ def message
15
+ "Use #{count.humanize} #{rhythmic_unit} #{(count == 1) ? "note" : "notes"} in each middle bar."
16
+ end
17
+
12
18
  private
13
19
 
20
+ def count
21
+ options.fetch(:count) { self.class::COUNT }
22
+ end
23
+
24
+ def rhythmic_unit
25
+ options.fetch(:rhythmic_value) { self.class::RHYTHMIC_VALUE }
26
+ end
27
+
28
+ def rhythmic_value
29
+ @rhythmic_value ||= HeadMusic::Rudiment::RhythmicValue.get(rhythmic_unit)
30
+ end
31
+
32
+ def check_middle_bar(bar_number)
33
+ bar_notes = notes_in_bar(bar_number)
34
+ return if bar_notes.length == count && bar_notes.all? { |note| note.rhythmic_value == rhythmic_value }
35
+
36
+ mark_bar(bar_number)
37
+ end
38
+
14
39
  def middle_bars
15
40
  cf_notes = cantus_firmus.notes
16
41
  return [] if cf_notes.length <= 2
@@ -3,16 +3,6 @@ module HeadMusic::Style::Guidelines; end
3
3
 
4
4
  # Checks that each middle bar contains exactly one whole note.
5
5
  class HeadMusic::Style::Guidelines::OnePerBar < HeadMusic::Style::Guidelines::NoteCountPerBar
6
- MESSAGE = "Use one whole note in each middle bar."
7
-
8
- WHOLE = HeadMusic::Rudiment::RhythmicValue.get(:whole)
9
-
10
- private
11
-
12
- def check_middle_bar(bar_number)
13
- bar_notes = notes_in_bar(bar_number)
14
- return if bar_notes.length == 1 && bar_notes.first.rhythmic_value == WHOLE
15
-
16
- mark_bar(bar_number)
17
- end
6
+ COUNT = 1
7
+ RHYTHMIC_VALUE = :whole
18
8
  end
@@ -25,9 +25,13 @@ class HeadMusic::Style::Guidelines::SecondSpeciesBreak < HeadMusic::Style::Guide
25
25
  .map { |note| HeadMusic::Style::Mark.for(note) }
26
26
  end
27
27
 
28
+ def max_break_ratio
29
+ options.fetch(:max_break_ratio) { self.class::MAX_BREAK_RATIO }
30
+ end
31
+
28
32
  def frequency_marks
29
33
  return [] if total_bars <= 0
30
- return [] if break_bars.length <= total_bars * MAX_BREAK_RATIO
34
+ return [] if break_bars.length <= total_bars * max_break_ratio
31
35
 
32
36
  break_bar_notes = break_bars.flat_map { |bar| notes_in_bar(bar) }
33
37
  [HeadMusic::Style::Mark.for_all(break_bar_notes, fitness: HeadMusic::SMALL_PENALTY_FACTOR)]
@@ -2,19 +2,35 @@
2
2
  module HeadMusic::Style::Guidelines; end
3
3
 
4
4
  # A voice shouldn't expend the range of a 10th.
5
+ # Configurable via the `maximum_range:` option (a diatonic interval number).
5
6
  class HeadMusic::Style::Guidelines::SingableRange < HeadMusic::Style::Annotation
6
7
  MAXIMUM_RANGE = 10
7
8
 
8
- MESSAGE = "Limit melodic range to a 10th."
9
+ # Ordinals whose spoken form begins with a vowel sound take "an" instead of
10
+ # "a" (an eighth, an eleventh, an eighteenth). Others in the singable range
11
+ # take "a".
12
+ VOWEL_SOUND_ORDINALS = [8, 11, 18].freeze
9
13
 
10
14
  def marks
11
15
  HeadMusic::Style::Mark.for_each(extremes, fitness: HeadMusic::PENALTY_FACTOR**overage) if overage.positive?
12
16
  end
13
17
 
18
+ def message
19
+ "Limit melodic range to #{indefinite_article} #{maximum_range.ordinalize}."
20
+ end
21
+
14
22
  private
15
23
 
24
+ def maximum_range
25
+ options.fetch(:maximum_range) { self.class::MAXIMUM_RANGE }
26
+ end
27
+
28
+ def indefinite_article
29
+ VOWEL_SOUND_ORDINALS.include?(maximum_range) ? "an" : "a"
30
+ end
31
+
16
32
  def overage
17
- notes.any? ? [range.number - MAXIMUM_RANGE, 0].max : 0
33
+ notes.any? ? [range.number - maximum_range, 0].max : 0
18
34
  end
19
35
 
20
36
  def extremes
@@ -3,16 +3,6 @@ module HeadMusic::Style::Guidelines; end
3
3
 
4
4
  # Checks that each middle bar contains exactly three quarter notes.
5
5
  class HeadMusic::Style::Guidelines::ThreePerBar < HeadMusic::Style::Guidelines::NoteCountPerBar
6
- MESSAGE = "Use three quarter notes in each middle bar."
7
-
8
- QUARTER = HeadMusic::Rudiment::RhythmicValue.get(:quarter)
9
-
10
- private
11
-
12
- def check_middle_bar(bar_number)
13
- bar_notes = notes_in_bar(bar_number)
14
- return if bar_notes.length == 3 && bar_notes.all? { |note| note.rhythmic_value == QUARTER }
15
-
16
- mark_bar(bar_number)
17
- end
6
+ COUNT = 3
7
+ RHYTHMIC_VALUE = :quarter
18
8
  end
@@ -3,16 +3,6 @@ module HeadMusic::Style::Guidelines; end
3
3
 
4
4
  # Checks that each middle bar contains exactly two half notes.
5
5
  class HeadMusic::Style::Guidelines::TwoPerBar < HeadMusic::Style::Guidelines::NoteCountPerBar
6
- MESSAGE = "Use two half notes in each middle bar."
7
-
8
- HALF = HeadMusic::Rudiment::RhythmicValue.get(:half)
9
-
10
- private
11
-
12
- def check_middle_bar(bar_number)
13
- bar_notes = notes_in_bar(bar_number)
14
- return if bar_notes.length == 2 && bar_notes.all? { |note| note.rhythmic_value == HALF }
15
-
16
- mark_bar(bar_number)
17
- end
6
+ COUNT = 2
7
+ RHYTHMIC_VALUE = :half
18
8
  end
@@ -1,19 +1,7 @@
1
1
  # Module for style guidelines.
2
2
  module HeadMusic::Style::Guidelines; end
3
3
 
4
- # A counterpoint guideline
5
- class HeadMusic::Style::Guidelines::UpToFourteenNotes < HeadMusic::Style::Annotation
4
+ # A counterpoint guideline: the cantus firmus should have at most fourteen notes.
5
+ class HeadMusic::Style::Guidelines::UpToFourteenNotes < HeadMusic::Style::Guidelines::MaximumNotes
6
6
  MAXIMUM_NOTES = 14
7
-
8
- MESSAGE = "Write up to fourteen notes."
9
-
10
- def marks
11
- HeadMusic::Style::Mark.for_each(notes[MAXIMUM_NOTES..]) if overage.positive?
12
- end
13
-
14
- private
15
-
16
- def overage
17
- [notes.length - MAXIMUM_NOTES, 0].max
18
- end
19
7
  end
@@ -0,0 +1,10 @@
1
+ # Module for guides
2
+ module HeadMusic::Style::Guides; end
3
+
4
+ # Base class for style guides. A guide analyzes a voice against its RULESET,
5
+ # producing one annotation per rule.
6
+ class HeadMusic::Style::Guides::Base
7
+ def self.analyze(voice)
8
+ self::RULESET.map { |rule| rule.new(voice) }
9
+ end
10
+ end
@@ -4,13 +4,7 @@ module HeadMusic::Style::Guides; end
4
4
  # Rules for combined first, second, and third species harmony
5
5
  class HeadMusic::Style::Guides::CombinedFirstSecondThirdSpeciesHarmony < HeadMusic::Style::Guides::SpeciesHarmony
6
6
  RULESET = [
7
- HeadMusic::Style::Guidelines::ApproachPerfectionContrarily,
8
- HeadMusic::Style::Guidelines::AvoidCrossingVoices,
9
- HeadMusic::Style::Guidelines::AvoidOverlappingVoices,
10
- HeadMusic::Style::Guidelines::ConsonantDownbeats,
11
- HeadMusic::Style::Guidelines::NoParallelPerfectOnDownbeats,
12
- HeadMusic::Style::Guidelines::PreferContraryMotion,
13
- HeadMusic::Style::Guidelines::PreferImperfect,
7
+ *HARMONIC_CORE,
14
8
  HeadMusic::Style::Guidelines::FloridDissonanceTreatment
15
9
  ].freeze
16
10
  end
@@ -4,19 +4,14 @@ module HeadMusic::Style::Guides; end
4
4
  # Rules for combined first, second, and third species melodies
5
5
  class HeadMusic::Style::Guides::CombinedFirstSecondThirdSpeciesMelody < HeadMusic::Style::Guides::SpeciesMelody
6
6
  RULESET = [
7
+ *MELODIC_CORE,
8
+ HeadMusic::Style::Guidelines::AllowedRhythmicValuesForCombined123,
7
9
  HeadMusic::Style::Guidelines::AlwaysMove,
8
- HeadMusic::Style::Guidelines::ConsonantClimax,
9
- HeadMusic::Style::Guidelines::Diatonic,
10
10
  HeadMusic::Style::Guidelines::EndOnTonic,
11
11
  HeadMusic::Style::Guidelines::FrequentDirectionChanges,
12
- HeadMusic::Style::Guidelines::LimitOctaveLeaps,
13
- HeadMusic::Style::Guidelines::MostlyConjunct,
12
+ HeadMusic::Style::Guidelines::NoRestsAfterNote,
14
13
  HeadMusic::Style::Guidelines::PrepareOctaveLeaps,
15
- HeadMusic::Style::Guidelines::SingableIntervals,
16
- HeadMusic::Style::Guidelines::SingableRange,
17
14
  HeadMusic::Style::Guidelines::StartOnPerfectConsonance,
18
- HeadMusic::Style::Guidelines::StepUpToFinalNote,
19
- HeadMusic::Style::Guidelines::AllowedRhythmicValuesForCombined123,
20
- HeadMusic::Style::Guidelines::NoRestsAfterNote
15
+ HeadMusic::Style::Guidelines::StepUpToFinalNote
21
16
  ].freeze
22
17
  end
@@ -0,0 +1,11 @@
1
+ # Guidelines for a free diatonic melody (not bound to cantus firmus start/end constraints).
2
+ class HeadMusic::Style::Guides::DiatonicMelody < HeadMusic::Style::Guides::SpeciesMelody
3
+ RULESET = [
4
+ *MELODIC_CORE,
5
+ HeadMusic::Style::Guidelines::ModerateDirectionChanges,
6
+ HeadMusic::Style::Guidelines::PrepareOctaveLeaps,
7
+ HeadMusic::Style::Guidelines::SingleLargeLeaps,
8
+ HeadMusic::Style::Guidelines::MinimumNotes.with(minimum: 5),
9
+ HeadMusic::Style::Guidelines::MaximumNotes.with(maximum: 24)
10
+ ].freeze
11
+ end
@@ -4,17 +4,11 @@ module HeadMusic::Style::Guides; end
4
4
  # Rules for fifth species (florid) harmony
5
5
  class HeadMusic::Style::Guides::FifthSpeciesHarmony < HeadMusic::Style::Guides::SpeciesHarmony
6
6
  RULESET = [
7
- HeadMusic::Style::Guidelines::ApproachPerfectionContrarily,
8
- HeadMusic::Style::Guidelines::AvoidCrossingVoices,
9
- HeadMusic::Style::Guidelines::AvoidOverlappingVoices,
10
- HeadMusic::Style::Guidelines::ConsonantDownbeats,
7
+ *HARMONIC_CORE,
8
+ HeadMusic::Style::Guidelines::FloridDissonanceTreatment,
11
9
  HeadMusic::Style::Guidelines::NoParallelPerfectAcrossBarline,
12
- HeadMusic::Style::Guidelines::NoParallelPerfectOnDownbeats,
13
10
  HeadMusic::Style::Guidelines::NoParallelPerfectWithSyncopation,
14
11
  HeadMusic::Style::Guidelines::NoStrongBeatUnisons,
15
- HeadMusic::Style::Guidelines::PreferContraryMotion,
16
- HeadMusic::Style::Guidelines::PreferImperfect,
17
- HeadMusic::Style::Guidelines::FloridDissonanceTreatment,
18
12
  HeadMusic::Style::Guidelines::SuspensionTreatment
19
13
  ].freeze
20
14
  end
@@ -4,22 +4,17 @@ module HeadMusic::Style::Guides; end
4
4
  # Rules for fifth species (florid) melodies
5
5
  class HeadMusic::Style::Guides::FifthSpeciesMelody < HeadMusic::Style::Guides::SpeciesMelody
6
6
  RULESET = [
7
+ *MELODIC_CORE,
8
+ HeadMusic::Style::Guidelines::AllowedRhythmicValuesForFifthSpecies,
7
9
  HeadMusic::Style::Guidelines::AlwaysMove,
8
- HeadMusic::Style::Guidelines::ConsonantClimax,
9
- HeadMusic::Style::Guidelines::Diatonic,
10
10
  HeadMusic::Style::Guidelines::EndOnTonic,
11
11
  HeadMusic::Style::Guidelines::FrequentDirectionChanges,
12
- HeadMusic::Style::Guidelines::LimitOctaveLeaps,
13
- HeadMusic::Style::Guidelines::MostlyConjunct,
12
+ HeadMusic::Style::Guidelines::MixedRhythmicValues,
13
+ HeadMusic::Style::Guidelines::NoRestsAfterNote,
14
14
  HeadMusic::Style::Guidelines::NoteFillsFinalBar,
15
15
  HeadMusic::Style::Guidelines::PrepareOctaveLeaps,
16
- HeadMusic::Style::Guidelines::SingableIntervals,
17
- HeadMusic::Style::Guidelines::SingableRange,
18
16
  HeadMusic::Style::Guidelines::StartOnPerfectConsonance,
19
17
  HeadMusic::Style::Guidelines::StepOutOfUnison,
20
- HeadMusic::Style::Guidelines::StepUpToFinalNote,
21
- HeadMusic::Style::Guidelines::AllowedRhythmicValuesForFifthSpecies,
22
- HeadMusic::Style::Guidelines::MixedRhythmicValues,
23
- HeadMusic::Style::Guidelines::NoRestsAfterNote
18
+ HeadMusic::Style::Guidelines::StepUpToFinalNote
24
19
  ].freeze
25
20
  end
@@ -1,15 +1,9 @@
1
1
  # Rules for first species harmony
2
2
  class HeadMusic::Style::Guides::FirstSpeciesHarmony < HeadMusic::Style::Guides::SpeciesHarmony
3
3
  RULESET = [
4
- HeadMusic::Style::Guidelines::ApproachPerfectionContrarily,
5
- HeadMusic::Style::Guidelines::AvoidCrossingVoices,
6
- HeadMusic::Style::Guidelines::AvoidOverlappingVoices,
7
- HeadMusic::Style::Guidelines::ConsonantDownbeats,
8
- HeadMusic::Style::Guidelines::NoParallelPerfectOnDownbeats,
4
+ *HARMONIC_CORE,
9
5
  HeadMusic::Style::Guidelines::NoUnisonsInMiddle,
10
- HeadMusic::Style::Guidelines::OneToOne,
11
- HeadMusic::Style::Guidelines::PreferContraryMotion,
12
- HeadMusic::Style::Guidelines::PreferImperfect
6
+ HeadMusic::Style::Guidelines::OneToOne
13
7
  ].freeze
14
8
  end
15
9
 
@@ -4,18 +4,13 @@ module HeadMusic::Style::Guides; end
4
4
  # Rules for first species melodies
5
5
  class HeadMusic::Style::Guides::FirstSpeciesMelody < HeadMusic::Style::Guides::SpeciesMelody
6
6
  RULESET = [
7
- HeadMusic::Style::Guidelines::ConsonantClimax,
8
- HeadMusic::Style::Guidelines::Diatonic,
7
+ *MELODIC_CORE,
9
8
  HeadMusic::Style::Guidelines::EndOnTonic,
10
- HeadMusic::Style::Guidelines::NoteFillsFinalBar,
11
9
  HeadMusic::Style::Guidelines::FirstBarWholeNote,
12
10
  HeadMusic::Style::Guidelines::FrequentDirectionChanges,
13
- HeadMusic::Style::Guidelines::LimitOctaveLeaps,
14
- HeadMusic::Style::Guidelines::MostlyConjunct,
11
+ HeadMusic::Style::Guidelines::NoteFillsFinalBar,
15
12
  HeadMusic::Style::Guidelines::OnePerBar,
16
13
  HeadMusic::Style::Guidelines::PrepareOctaveLeaps,
17
- HeadMusic::Style::Guidelines::SingableIntervals,
18
- HeadMusic::Style::Guidelines::SingableRange,
19
14
  HeadMusic::Style::Guidelines::StartOnPerfectConsonance,
20
15
  HeadMusic::Style::Guidelines::StepOutOfUnison,
21
16
  HeadMusic::Style::Guidelines::StepUpToFinalNote
@@ -1,15 +1,9 @@
1
1
  # Rules for fourth species harmony
2
2
  class HeadMusic::Style::Guides::FourthSpeciesHarmony < HeadMusic::Style::Guides::SpeciesHarmony
3
3
  RULESET = [
4
- HeadMusic::Style::Guidelines::ApproachPerfectionContrarily,
5
- HeadMusic::Style::Guidelines::AvoidCrossingVoices,
6
- HeadMusic::Style::Guidelines::AvoidOverlappingVoices,
7
- HeadMusic::Style::Guidelines::ConsonantDownbeats,
8
- HeadMusic::Style::Guidelines::NoParallelPerfectOnDownbeats,
4
+ *HARMONIC_CORE,
9
5
  HeadMusic::Style::Guidelines::NoParallelPerfectWithSyncopation,
10
6
  HeadMusic::Style::Guidelines::NoStrongBeatUnisons,
11
- HeadMusic::Style::Guidelines::PreferContraryMotion,
12
- HeadMusic::Style::Guidelines::PreferImperfect,
13
7
  HeadMusic::Style::Guidelines::SecondSpeciesBreak,
14
8
  HeadMusic::Style::Guidelines::SuspensionTreatment
15
9
  ].freeze
@@ -4,21 +4,16 @@ module HeadMusic::Style::Guides; end
4
4
  # Rules for fourth species melodies
5
5
  class HeadMusic::Style::Guides::FourthSpeciesMelody < HeadMusic::Style::Guides::SpeciesMelody
6
6
  RULESET = [
7
+ *MELODIC_CORE,
7
8
  HeadMusic::Style::Guidelines::AlwaysMove,
8
- HeadMusic::Style::Guidelines::ConsonantClimax,
9
- HeadMusic::Style::Guidelines::Diatonic,
10
9
  HeadMusic::Style::Guidelines::EndOnTonic,
11
- HeadMusic::Style::Guidelines::NoteFillsFinalBar,
12
10
  HeadMusic::Style::Guidelines::FrequentDirectionChanges,
13
- HeadMusic::Style::Guidelines::LimitOctaveLeaps,
14
- HeadMusic::Style::Guidelines::MostlyConjunct,
11
+ HeadMusic::Style::Guidelines::NoRestsAfterNote,
12
+ HeadMusic::Style::Guidelines::NoteFillsFinalBar,
15
13
  HeadMusic::Style::Guidelines::OneToOneWithTies,
16
14
  HeadMusic::Style::Guidelines::PrepareOctaveLeaps,
17
- HeadMusic::Style::Guidelines::SingableIntervals,
18
- HeadMusic::Style::Guidelines::SingableRange,
19
15
  HeadMusic::Style::Guidelines::StartOnPerfectConsonance,
20
16
  HeadMusic::Style::Guidelines::StepOutOfUnison,
21
- HeadMusic::Style::Guidelines::StepUpToFinalNote,
22
- HeadMusic::Style::Guidelines::NoRestsAfterNote
17
+ HeadMusic::Style::Guidelines::StepUpToFinalNote
23
18
  ].freeze
24
19
  end
@@ -4,19 +4,14 @@ module HeadMusic::Style::Guides; end
4
4
  # Rules for the cantus firmus according to Fux.
5
5
  class HeadMusic::Style::Guides::FuxCantusFirmus < HeadMusic::Style::Guides::SpeciesMelody
6
6
  RULESET = [
7
+ *MELODIC_CORE,
7
8
  HeadMusic::Style::Guidelines::AlwaysMove,
8
9
  HeadMusic::Style::Guidelines::AtLeastEightNotes,
9
- HeadMusic::Style::Guidelines::ConsonantClimax,
10
- HeadMusic::Style::Guidelines::Diatonic,
11
10
  HeadMusic::Style::Guidelines::EndOnTonic,
12
11
  HeadMusic::Style::Guidelines::FrequentDirectionChanges,
13
- HeadMusic::Style::Guidelines::LimitOctaveLeaps,
14
- HeadMusic::Style::Guidelines::MostlyConjunct,
15
12
  HeadMusic::Style::Guidelines::NoRests,
16
13
  HeadMusic::Style::Guidelines::NotesSameLength,
17
14
  HeadMusic::Style::Guidelines::RecoverLargeLeaps,
18
- HeadMusic::Style::Guidelines::SingableIntervals,
19
- HeadMusic::Style::Guidelines::SingableRange,
20
15
  HeadMusic::Style::Guidelines::StartOnTonic,
21
16
  HeadMusic::Style::Guidelines::StepDownToFinalNote,
22
17
  HeadMusic::Style::Guidelines::UpToFourteenNotes
@@ -4,19 +4,14 @@ module HeadMusic::Style::Guides; end
4
4
  # Modern rules for the cantus firmus
5
5
  class HeadMusic::Style::Guides::ModernCantusFirmus < HeadMusic::Style::Guides::SpeciesMelody
6
6
  RULESET = [
7
+ *MELODIC_CORE,
7
8
  HeadMusic::Style::Guidelines::AlwaysMove,
8
9
  HeadMusic::Style::Guidelines::AtLeastEightNotes,
9
- HeadMusic::Style::Guidelines::ConsonantClimax,
10
- HeadMusic::Style::Guidelines::Diatonic,
11
10
  HeadMusic::Style::Guidelines::EndOnTonic,
12
- HeadMusic::Style::Guidelines::LimitOctaveLeaps,
13
11
  HeadMusic::Style::Guidelines::ModerateDirectionChanges,
14
- HeadMusic::Style::Guidelines::MostlyConjunct,
15
12
  HeadMusic::Style::Guidelines::NoRests,
16
13
  HeadMusic::Style::Guidelines::NotesSameLength,
17
14
  HeadMusic::Style::Guidelines::PrepareOctaveLeaps,
18
- HeadMusic::Style::Guidelines::SingableIntervals,
19
- HeadMusic::Style::Guidelines::SingableRange,
20
15
  HeadMusic::Style::Guidelines::SingleLargeLeaps,
21
16
  HeadMusic::Style::Guidelines::StartOnTonic,
22
17
  HeadMusic::Style::Guidelines::StepToFinalNote,
@@ -1,15 +1,9 @@
1
1
  # Rules for second species harmony
2
2
  class HeadMusic::Style::Guides::SecondSpeciesHarmony < HeadMusic::Style::Guides::SpeciesHarmony
3
3
  RULESET = [
4
- HeadMusic::Style::Guidelines::ApproachPerfectionContrarily,
5
- HeadMusic::Style::Guidelines::AvoidCrossingVoices,
6
- HeadMusic::Style::Guidelines::AvoidOverlappingVoices,
7
- HeadMusic::Style::Guidelines::ConsonantDownbeats,
4
+ *HARMONIC_CORE,
8
5
  HeadMusic::Style::Guidelines::NoParallelPerfectAcrossBarline,
9
- HeadMusic::Style::Guidelines::NoParallelPerfectOnDownbeats,
10
6
  HeadMusic::Style::Guidelines::NoStrongBeatUnisons,
11
- HeadMusic::Style::Guidelines::PreferContraryMotion,
12
- HeadMusic::Style::Guidelines::PreferImperfect,
13
7
  HeadMusic::Style::Guidelines::WeakBeatDissonanceTreatment
14
8
  ].freeze
15
9
  end
@@ -4,22 +4,17 @@ module HeadMusic::Style::Guides; end
4
4
  # Rules for second species melodies
5
5
  class HeadMusic::Style::Guides::SecondSpeciesMelody < HeadMusic::Style::Guides::SpeciesMelody
6
6
  RULESET = [
7
+ *MELODIC_CORE,
7
8
  HeadMusic::Style::Guidelines::AlwaysMove,
8
- HeadMusic::Style::Guidelines::ConsonantClimax,
9
- HeadMusic::Style::Guidelines::Diatonic,
10
9
  HeadMusic::Style::Guidelines::EndOnTonic,
11
- HeadMusic::Style::Guidelines::NoteFillsFinalBar,
12
10
  HeadMusic::Style::Guidelines::FirstBarHalfNotes,
13
11
  HeadMusic::Style::Guidelines::FrequentDirectionChanges,
14
- HeadMusic::Style::Guidelines::LimitOctaveLeaps,
15
- HeadMusic::Style::Guidelines::MostlyConjunct,
12
+ HeadMusic::Style::Guidelines::NoRestsAfterNote,
13
+ HeadMusic::Style::Guidelines::NoteFillsFinalBar,
16
14
  HeadMusic::Style::Guidelines::PrepareOctaveLeaps,
17
- HeadMusic::Style::Guidelines::SingableIntervals,
18
- HeadMusic::Style::Guidelines::SingableRange,
19
15
  HeadMusic::Style::Guidelines::StartOnPerfectConsonance,
20
16
  HeadMusic::Style::Guidelines::StepOutOfUnison,
21
17
  HeadMusic::Style::Guidelines::StepUpToFinalNote,
22
- HeadMusic::Style::Guidelines::TwoPerBar,
23
- HeadMusic::Style::Guidelines::NoRestsAfterNote
18
+ HeadMusic::Style::Guidelines::TwoPerBar
24
19
  ].freeze
25
20
  end
@@ -1,9 +1,18 @@
1
1
  # Module for guides
2
2
  module HeadMusic::Style::Guides; end
3
3
 
4
- # Base class for species harmony guides providing shared analysis behavior
5
- class HeadMusic::Style::Guides::SpeciesHarmony
6
- def self.analyze(voice)
7
- self::RULESET.map { |rule| rule.new(voice) }
8
- end
4
+ # Base class for species harmony guides. Inherits analysis behavior from Base;
5
+ # exists as a semantic marker distinguishing harmony guides from melody guides.
6
+ class HeadMusic::Style::Guides::SpeciesHarmony < HeadMusic::Style::Guides::Base
7
+ # Guidelines shared by every harmonic guide. Subclasses splat this into their
8
+ # RULESET: RULESET = [*HARMONIC_CORE, ...species-specific rules].
9
+ HARMONIC_CORE = [
10
+ HeadMusic::Style::Guidelines::ApproachPerfectionContrarily,
11
+ HeadMusic::Style::Guidelines::AvoidCrossingVoices,
12
+ HeadMusic::Style::Guidelines::AvoidOverlappingVoices,
13
+ HeadMusic::Style::Guidelines::ConsonantDownbeats,
14
+ HeadMusic::Style::Guidelines::NoParallelPerfectOnDownbeats,
15
+ HeadMusic::Style::Guidelines::PreferContraryMotion,
16
+ HeadMusic::Style::Guidelines::PreferImperfect
17
+ ].freeze
9
18
  end
@@ -1,9 +1,17 @@
1
1
  # Module for guides
2
2
  module HeadMusic::Style::Guides; end
3
3
 
4
- # Base class for species melody guides providing shared analysis behavior
5
- class HeadMusic::Style::Guides::SpeciesMelody
6
- def self.analyze(voice)
7
- self::RULESET.map { |rule| rule.new(voice) }
8
- end
4
+ # Base class for species melody guides. Inherits analysis behavior from Base;
5
+ # exists as a semantic marker distinguishing melody guides from harmony guides.
6
+ class HeadMusic::Style::Guides::SpeciesMelody < HeadMusic::Style::Guides::Base
7
+ # Guidelines shared by every melodic guide. Subclasses splat this into their
8
+ # RULESET: RULESET = [*MELODIC_CORE, ...species-specific rules].
9
+ MELODIC_CORE = [
10
+ HeadMusic::Style::Guidelines::ConsonantClimax,
11
+ HeadMusic::Style::Guidelines::Diatonic,
12
+ HeadMusic::Style::Guidelines::LimitOctaveLeaps,
13
+ HeadMusic::Style::Guidelines::MostlyConjunct,
14
+ HeadMusic::Style::Guidelines::SingableIntervals,
15
+ HeadMusic::Style::Guidelines::SingableRange
16
+ ].freeze
9
17
  end
@@ -1,15 +1,9 @@
1
1
  # Rules for third species harmony
2
2
  class HeadMusic::Style::Guides::ThirdSpeciesHarmony < HeadMusic::Style::Guides::SpeciesHarmony
3
3
  RULESET = [
4
- HeadMusic::Style::Guidelines::ApproachPerfectionContrarily,
5
- HeadMusic::Style::Guidelines::AvoidCrossingVoices,
6
- HeadMusic::Style::Guidelines::AvoidOverlappingVoices,
7
- HeadMusic::Style::Guidelines::ConsonantDownbeats,
4
+ *HARMONIC_CORE,
8
5
  HeadMusic::Style::Guidelines::NoParallelPerfectAcrossBarline,
9
- HeadMusic::Style::Guidelines::NoParallelPerfectOnDownbeats,
10
6
  HeadMusic::Style::Guidelines::NoStrongBeatUnisons,
11
- HeadMusic::Style::Guidelines::PreferContraryMotion,
12
- HeadMusic::Style::Guidelines::PreferImperfect,
13
7
  HeadMusic::Style::Guidelines::ThirdSpeciesDissonanceTreatment
14
8
  ].freeze
15
9
  end
@@ -4,22 +4,17 @@ module HeadMusic::Style::Guides; end
4
4
  # Rules for third species melodies
5
5
  class HeadMusic::Style::Guides::ThirdSpeciesMelody < HeadMusic::Style::Guides::SpeciesMelody
6
6
  RULESET = [
7
+ *MELODIC_CORE,
7
8
  HeadMusic::Style::Guidelines::AlwaysMove,
8
- HeadMusic::Style::Guidelines::ConsonantClimax,
9
- HeadMusic::Style::Guidelines::Diatonic,
10
9
  HeadMusic::Style::Guidelines::EndOnTonic,
11
- HeadMusic::Style::Guidelines::NoteFillsFinalBar,
12
10
  HeadMusic::Style::Guidelines::FirstBarQuarterNotes,
13
11
  HeadMusic::Style::Guidelines::FourPerBar,
14
12
  HeadMusic::Style::Guidelines::FrequentDirectionChanges,
15
- HeadMusic::Style::Guidelines::LimitOctaveLeaps,
16
- HeadMusic::Style::Guidelines::MostlyConjunct,
13
+ HeadMusic::Style::Guidelines::NoRestsAfterNote,
14
+ HeadMusic::Style::Guidelines::NoteFillsFinalBar,
17
15
  HeadMusic::Style::Guidelines::PrepareOctaveLeaps,
18
- HeadMusic::Style::Guidelines::SingableIntervals,
19
- HeadMusic::Style::Guidelines::SingableRange,
20
16
  HeadMusic::Style::Guidelines::StartOnPerfectConsonance,
21
17
  HeadMusic::Style::Guidelines::StepOutOfUnison,
22
- HeadMusic::Style::Guidelines::StepUpToFinalNote,
23
- HeadMusic::Style::Guidelines::NoRestsAfterNote
18
+ HeadMusic::Style::Guidelines::StepUpToFinalNote
24
19
  ].freeze
25
20
  end
@@ -1,15 +1,9 @@
1
1
  # Rules for triple meter harmony
2
2
  class HeadMusic::Style::Guides::ThirdSpeciesTripleMeterHarmony < HeadMusic::Style::Guides::SpeciesHarmony
3
3
  RULESET = [
4
- HeadMusic::Style::Guidelines::ApproachPerfectionContrarily,
5
- HeadMusic::Style::Guidelines::AvoidCrossingVoices,
6
- HeadMusic::Style::Guidelines::AvoidOverlappingVoices,
7
- HeadMusic::Style::Guidelines::ConsonantDownbeats,
4
+ *HARMONIC_CORE,
8
5
  HeadMusic::Style::Guidelines::NoParallelPerfectAcrossBarline,
9
- HeadMusic::Style::Guidelines::NoParallelPerfectOnDownbeats,
10
6
  HeadMusic::Style::Guidelines::NoStrongBeatUnisons,
11
- HeadMusic::Style::Guidelines::PreferContraryMotion,
12
- HeadMusic::Style::Guidelines::PreferImperfect,
13
7
  HeadMusic::Style::Guidelines::TripleMeterDissonanceTreatment
14
8
  ].freeze
15
9
  end
@@ -4,22 +4,17 @@ module HeadMusic::Style::Guides; end
4
4
  # Rules for triple meter melodies
5
5
  class HeadMusic::Style::Guides::ThirdSpeciesTripleMeterMelody < HeadMusic::Style::Guides::SpeciesMelody
6
6
  RULESET = [
7
+ *MELODIC_CORE,
7
8
  HeadMusic::Style::Guidelines::AlwaysMove,
8
- HeadMusic::Style::Guidelines::ConsonantClimax,
9
- HeadMusic::Style::Guidelines::Diatonic,
10
9
  HeadMusic::Style::Guidelines::EndOnTonic,
11
- HeadMusic::Style::Guidelines::NoteFillsFinalBar,
12
10
  HeadMusic::Style::Guidelines::FirstBarQuarterNotes,
13
11
  HeadMusic::Style::Guidelines::FrequentDirectionChanges,
14
- HeadMusic::Style::Guidelines::LimitOctaveLeaps,
15
- HeadMusic::Style::Guidelines::MostlyConjunct,
12
+ HeadMusic::Style::Guidelines::NoRestsAfterNote,
13
+ HeadMusic::Style::Guidelines::NoteFillsFinalBar,
16
14
  HeadMusic::Style::Guidelines::PrepareOctaveLeaps,
17
- HeadMusic::Style::Guidelines::SingableIntervals,
18
- HeadMusic::Style::Guidelines::SingableRange,
19
15
  HeadMusic::Style::Guidelines::StartOnPerfectConsonance,
20
16
  HeadMusic::Style::Guidelines::StepOutOfUnison,
21
17
  HeadMusic::Style::Guidelines::StepUpToFinalNote,
22
- HeadMusic::Style::Guidelines::ThreePerBar,
23
- HeadMusic::Style::Guidelines::NoRestsAfterNote
18
+ HeadMusic::Style::Guidelines::ThreePerBar
24
19
  ].freeze
25
20
  end
@@ -1,3 +1,3 @@
1
1
  module HeadMusic
2
- VERSION = "12.5.0"
2
+ VERSION = "12.6.0"
3
3
  end
data/lib/head_music.rb CHANGED
@@ -9,6 +9,7 @@ require "head_music/version"
9
9
 
10
10
  require "active_support/core_ext/module/delegation"
11
11
  require "active_support/core_ext/string/access"
12
+ require "active_support/core_ext/integer/inflections"
12
13
  require "humanize"
13
14
  require "i18n"
14
15
  require "i18n/backend/fallbacks"
@@ -152,6 +153,7 @@ require "head_music/style/mark"
152
153
  # style guidelines
153
154
  require "head_music/style/guidelines/always_move"
154
155
  require "head_music/style/guidelines/approach_perfection_contrarily"
156
+ require "head_music/style/guidelines/minimum_notes"
155
157
  require "head_music/style/guidelines/at_least_eight_notes"
156
158
  require "head_music/style/guidelines/avoid_crossing_voices"
157
159
  require "head_music/style/guidelines/avoid_overlapping_voices"
@@ -200,6 +202,7 @@ require "head_music/style/guidelines/step_up_to_final_note"
200
202
  require "head_music/style/guidelines/suspension_treatment"
201
203
  require "head_music/style/guidelines/three_per_bar"
202
204
  require "head_music/style/guidelines/two_per_bar"
205
+ require "head_music/style/guidelines/maximum_notes"
203
206
  require "head_music/style/guidelines/up_to_fourteen_notes"
204
207
  require "head_music/style/guidelines/weak_beat_dissonance_treatment"
205
208
  require "head_music/style/guidelines/dissonance_figure_detection"
@@ -212,10 +215,12 @@ require "head_music/style/guidelines/florid_dissonance_treatment"
212
215
  require "head_music/style/guidelines/mixed_rhythmic_values"
213
216
 
214
217
  # style guides
218
+ require "head_music/style/guides/base"
215
219
  require "head_music/style/guides/species_melody"
216
220
  require "head_music/style/guides/fux_cantus_firmus"
217
221
  require "head_music/style/guides/modern_cantus_firmus"
218
222
  require "head_music/style/guides/first_species_melody"
223
+ require "head_music/style/guides/diatonic_melody"
219
224
  require "head_music/style/guides/species_harmony"
220
225
  require "head_music/style/guides/first_species_harmony"
221
226
  require "head_music/style/guides/second_species_melody"
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: 12.5.0
4
+ version: 12.6.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: 2026-04-08 00:00:00.000000000 Z
11
+ date: 2026-07-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -291,6 +291,8 @@ files:
291
291
  - lib/head_music/style/guidelines/four_per_bar.rb
292
292
  - lib/head_music/style/guidelines/frequent_direction_changes.rb
293
293
  - lib/head_music/style/guidelines/limit_octave_leaps.rb
294
+ - lib/head_music/style/guidelines/maximum_notes.rb
295
+ - lib/head_music/style/guidelines/minimum_notes.rb
294
296
  - lib/head_music/style/guidelines/mixed_rhythmic_values.rb
295
297
  - lib/head_music/style/guidelines/moderate_direction_changes.rb
296
298
  - lib/head_music/style/guidelines/mostly_conjunct.rb
@@ -328,8 +330,10 @@ files:
328
330
  - lib/head_music/style/guidelines/two_per_bar.rb
329
331
  - lib/head_music/style/guidelines/up_to_fourteen_notes.rb
330
332
  - lib/head_music/style/guidelines/weak_beat_dissonance_treatment.rb
333
+ - lib/head_music/style/guides/base.rb
331
334
  - lib/head_music/style/guides/combined_first_second_third_species_harmony.rb
332
335
  - lib/head_music/style/guides/combined_first_second_third_species_melody.rb
336
+ - lib/head_music/style/guides/diatonic_melody.rb
333
337
  - lib/head_music/style/guides/fifth_species_harmony.rb
334
338
  - lib/head_music/style/guides/fifth_species_melody.rb
335
339
  - lib/head_music/style/guides/first_species_harmony.rb
@@ -417,7 +421,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
417
421
  - !ruby/object:Gem::Version
418
422
  version: '0'
419
423
  requirements: []
420
- rubygems_version: 3.5.6
424
+ rubygems_version: 3.5.3
421
425
  signing_key:
422
426
  specification_version: 4
423
427
  summary: The rudiments of western music theory and analysis.