head_music 12.4.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 (57) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +110 -0
  3. data/CLAUDE.md +27 -11
  4. data/Gemfile.lock +2 -1
  5. data/MUSIC_THEORY.md +97 -34
  6. data/Rakefile +2 -2
  7. data/lib/head_music/style/annotation.rb +31 -1
  8. data/lib/head_music/style/guidelines/allowed_rhythmic_values_for_fifth_species.rb +99 -4
  9. data/lib/head_music/style/guidelines/at_least_eight_notes.rb +2 -24
  10. data/lib/head_music/style/guidelines/consonant_downbeats.rb +16 -3
  11. data/lib/head_music/style/guidelines/direction_changes.rb +10 -2
  12. data/lib/head_music/style/guidelines/dissonance_figure_detection.rb +71 -0
  13. data/lib/head_music/style/guidelines/florid_dissonance_treatment.rb +11 -8
  14. data/lib/head_music/style/guidelines/four_per_bar.rb +2 -12
  15. data/lib/head_music/style/guidelines/frequent_direction_changes.rb +1 -4
  16. data/lib/head_music/style/guidelines/limit_octave_leaps.rb +11 -2
  17. data/lib/head_music/style/guidelines/maximum_notes.rb +24 -0
  18. data/lib/head_music/style/guidelines/minimum_notes.rb +34 -0
  19. data/lib/head_music/style/guidelines/mixed_rhythmic_values.rb +4 -4
  20. data/lib/head_music/style/guidelines/moderate_direction_changes.rb +1 -4
  21. data/lib/head_music/style/guidelines/mostly_conjunct.rb +7 -2
  22. data/lib/head_music/style/guidelines/note_count_per_bar.rb +26 -1
  23. data/lib/head_music/style/guidelines/one_per_bar.rb +2 -12
  24. data/lib/head_music/style/guidelines/second_species_break.rb +5 -1
  25. data/lib/head_music/style/guidelines/singable_range.rb +18 -2
  26. data/lib/head_music/style/guidelines/suspension_treatment.rb +3 -3
  27. data/lib/head_music/style/guidelines/third_species_dissonance_treatment.rb +2 -65
  28. data/lib/head_music/style/guidelines/three_per_bar.rb +2 -12
  29. data/lib/head_music/style/guidelines/two_per_bar.rb +2 -12
  30. data/lib/head_music/style/guidelines/up_to_fourteen_notes.rb +2 -14
  31. data/lib/head_music/style/guides/base.rb +10 -0
  32. data/lib/head_music/style/guides/combined_first_second_third_species_harmony.rb +1 -7
  33. data/lib/head_music/style/guides/combined_first_second_third_species_melody.rb +4 -9
  34. data/lib/head_music/style/guides/diatonic_melody.rb +11 -0
  35. data/lib/head_music/style/guides/fifth_species_harmony.rb +4 -8
  36. data/lib/head_music/style/guides/fifth_species_melody.rb +7 -10
  37. data/lib/head_music/style/guides/first_species_harmony.rb +2 -8
  38. data/lib/head_music/style/guides/first_species_melody.rb +2 -7
  39. data/lib/head_music/style/guides/fourth_species_harmony.rb +1 -7
  40. data/lib/head_music/style/guides/fourth_species_melody.rb +4 -9
  41. data/lib/head_music/style/guides/fux_cantus_firmus.rb +1 -6
  42. data/lib/head_music/style/guides/modern_cantus_firmus.rb +1 -6
  43. data/lib/head_music/style/guides/second_species_harmony.rb +1 -7
  44. data/lib/head_music/style/guides/second_species_melody.rb +4 -9
  45. data/lib/head_music/style/guides/species_harmony.rb +14 -5
  46. data/lib/head_music/style/guides/species_melody.rb +13 -5
  47. data/lib/head_music/style/guides/third_species_harmony.rb +1 -7
  48. data/lib/head_music/style/guides/third_species_melody.rb +4 -9
  49. data/lib/head_music/style/guides/third_species_triple_meter_harmony.rb +1 -7
  50. data/lib/head_music/style/guides/third_species_triple_meter_melody.rb +4 -9
  51. data/lib/head_music/version.rb +1 -1
  52. data/lib/head_music.rb +6 -3
  53. data/references/fifth-species-counterpoint.md +578 -0
  54. metadata +9 -6
  55. data/lib/head_music/style/guidelines/allowed_rhythmic_values_for_combined234.rb +0 -23
  56. data/lib/head_music/style/guides/combined_second_third_fourth_species_harmony.rb +0 -18
  57. data/lib/head_music/style/guides/combined_second_third_fourth_species_melody.rb +0 -22
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fc52f4684dac9eebefdc696f1d7f74094c98b26994905f7436cd757800880764
4
- data.tar.gz: 3087c3c68254f16768f875538d54028db26498b8f7dfb8e543e7d402582a4581
3
+ metadata.gz: db91f6ff65b081bb369f3d2a85e2bc6e6cd092bb0018c2c416bf36bcfc4d4e9d
4
+ data.tar.gz: 266914dfbc382dc27591ea37302b57571b8693f2e0e4bd5189d2fffdff2a7c00
5
5
  SHA512:
6
- metadata.gz: 6344e623f21eaad3c5c1dab093980062701d33ebca7f73050e211a7665afc2f81e49ef0de69e99f6a5c14d0d7797608cf35a88ca2c54c94590bbf404e575db9f
7
- data.tar.gz: d28d37afdaae4bb89cbe3de6e1b9957fc5d19d3e54a03f1d9145092e7e7727545751365bad050b96a2f26a5b8f56972ebcbd99f9ad985c11a27e6e95636c174b
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
@@ -93,17 +93,33 @@ The codebase follows a domain-driven design with clear module boundaries:
93
93
  4. **HeadMusic::Content** - Musical composition representation
94
94
  - Compositions, voices, bars, positions
95
95
  - Notes in context (pitch + duration + placement)
96
- - Temporal organization
97
-
98
- 5. **HeadMusic::Analysis** - Musical analysis tools
99
- - Intervals, chords, motion analysis
100
- - Harmonic and melodic analysis
96
+ - Uses HeadMusic::Time for temporal positioning
97
+
98
+ 5. **HeadMusic::Time** - Temporal infrastructure
99
+ - Clock positions (elapsed nanoseconds as source of truth)
100
+ - Musical positions (bars:beats:ticks:subticks notation)
101
+ - SMPTE timecode (hours:minutes:seconds:frames for video/audio sync)
102
+ - Conductor: converts between clock time and musical position
103
+ - Tempo and meter maps for tracking changes over time
104
+
105
+ 6. **HeadMusic::Analysis** - Musical analysis tools
106
+ - Diatonic intervals (quality + size)
107
+ - Harmonic and melodic interval analysis
108
+ - Consonance classification
109
+ - Motion analysis (parallel, similar, contrary, oblique)
101
110
  - Pitch class sets and collections
111
+ - Sonority and dyad analysis
112
+ - Circle (of fifths) and interval cycles
102
113
 
103
- 6. **HeadMusic::Style** - Composition rules and guidelines
104
- - Counterpoint rules
114
+ 7. **HeadMusic::Style** - Composition rules and guidelines
115
+ - Counterpoint rules (species counterpoint guides)
105
116
  - Voice leading guidelines
106
- - Style analysis
117
+ - Style analysis and annotations
118
+ - Historical traditions (medieval, renaissance, modern)
119
+
120
+ 8. **HeadMusic::Utilities** - Shared helpers
121
+ - Case conversion
122
+ - Hash key utilities
107
123
 
108
124
  ### Key Design Patterns
109
125
 
@@ -171,7 +187,7 @@ This project deliberately deprioritizes formal documentation in favor of clear,
171
187
  Domain reference materials live in `references/`.
172
188
  Consult these when implementing or modifying style guidelines:
173
189
 
174
- - `references/second-species-counterpoint.md` Pedagogical survey of second-species counterpoint rules from Fux through contemporary sources, with cross-source comparison and mapping to head_music's style guideline architecture.
190
+ Consult the files in `references/` when implementing or modifying style guidelines. These contain pedagogical surveys with cross-source comparisons mapped to head_music's style guideline architecture.
175
191
 
176
192
  ## Music theory and concepts
177
193
 
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- head_music (12.4.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/MUSIC_THEORY.md CHANGED
@@ -8,7 +8,7 @@ The rudiments of music theory are built up piece-by-piece.
8
8
  letter name
9
9
  - C, D, E, F, G, A, B, …
10
10
  - equivalent to:
11
- - do, re, mi, fa, sol, la, si/ti, …
11
+ - do, re, mi, fa, sol, la, si/ti, …
12
12
 
13
13
  alteration
14
14
  - sharp, flat, double sharp, double flat
@@ -44,7 +44,7 @@ rhythmic value (duration)
44
44
  - augmentation dots extend the duration of the rhythmic unit
45
45
  - one dot extends the rhythmic value 50%
46
46
  - two dots extend the rhythmic value 75%
47
- - two dots extend the rhythmic value 87.5%
47
+ - three dots extend the rhythmic value 87.5%
48
48
 
49
49
  rest
50
50
  - a rhythmic value that passes in silence
@@ -64,9 +64,9 @@ tied duration
64
64
  articulation
65
65
  - a category of expressions that modify how one or more notes are performed.
66
66
  - level of connection
67
- - staccatissimo - the most staccato, even shorted and more clipped
68
- - staccato – detatched (shortened to lease space between adjacent notes)
69
- - tenuto - held of the full value
67
+ - staccatissimo - the most staccato, even shorter and more clipped
68
+ - staccato – detached (shortened to leave space between adjacent notes)
69
+ - tenuto - held for the full value
70
70
  - legato - smoothly connected
71
71
  - emphasis
72
72
  - accent - play with emphasis or a stronger attack
@@ -76,45 +76,108 @@ articulation
76
76
  - breath mark
77
77
  - roll mark
78
78
 
79
- ## Instrument Families
79
+ ## Intervals
80
+
81
+ interval
82
+ - the distance between two pitches
83
+ - described by quality + size
84
+ - size: unison, second, third, fourth, fifth, sixth, seventh, octave, etc.
85
+ - quality: perfect, major, minor, augmented, diminished
86
+ - perfect intervals: unison, fourth, fifth, octave
87
+ - major/minor intervals: second, third, sixth, seventh
88
+ - a chromatic interval measures distance in half steps (semitones)
89
+ - a diatonic interval considers the letter names and key context
90
+
91
+ harmonic interval
92
+ - two pitches sounding simultaneously
93
+
94
+ melodic interval
95
+ - two pitches sounding in succession
96
+ - ascending or descending
97
+
98
+ ## Consonance and Dissonance
99
+
100
+ consonance
101
+ - intervals that sound stable and resolved
102
+ - perfect consonances: unison, perfect fifth, octave
103
+ - imperfect consonances: major/minor third, major/minor sixth
104
+
105
+ dissonance
106
+ - intervals that sound tense and seek resolution
107
+ - seconds, sevenths, tritone (augmented fourth / diminished fifth)
108
+ - perfect fourth considered "dissonance" requiring resolution in traditional counterpoint
80
109
 
81
- There are several ways that people talk about families and other categorizations of instruments. For example, the "string family" or "woodwind family", but also more specifically, the word family is applied more specifically (e.g. the "oboe family").
110
+ ## Scales and Modes
82
111
 
83
- ### Text Conversation with Brian Head (May 7, 2023)
112
+ scale
113
+ - an ordered collection of pitches spanning an octave
114
+ - defined by a pattern of whole steps and half steps
115
+ - named by its tonic (starting pitch) and type
84
116
 
85
- Brian Head, brother and faculty at USC Thornton School of Music
86
- https://music.usc.edu/brian-head/
117
+ major scale
118
+ - pattern: W-W-H-W-W-W-H (W = whole step, H = half step)
87
119
 
88
- Robert Head:
89
- Hey, Brian. You have a sec for a music question? I’m trying to come up with different terms for the way “family” is used. “Woodwind family” vs. “Oboe family”.
120
+ natural minor scale
121
+ - pattern: W-H-W-W-H-W-W
90
122
 
91
- Brian Head:
92
- Those are good, I’d say. In what context?
123
+ modes
124
+ - scales derived by starting on different degrees of a parent scale
125
+ - Ionian (= major), Dorian, Phrygian, Lydian, Mixolydian, Aeolian (= natural minor), Locrian
93
126
 
94
- Robert Head:
95
- In my software project, I’m trying to define those relationships. One is a section of the orchestra and the other is species of instrument. But usually people, in my experience, just say “family”.
96
- Is there some more precise terminology or adjective that can disambiguate those two terms?
127
+ ## Keys and Key Signatures
97
128
 
98
- Brian Head:
99
- Hmmm. Already “family” is informal. Blatter distinguishes between “choir” and “family” as in the trumpet family” within the “brass choir”.
129
+ key
130
+ - a tonal center (tonic) combined with a scale type (major or minor)
131
+ - example: "E-flat major", "A minor"
100
132
 
101
- Robert Head:
102
- Ah, yes! I do like that.
103
- But does it apply to percussion?
104
- “Section” is another candidate, but that gets used for string parts as well.
133
+ key signature
134
+ - the set of sharps or flats applied to a key
135
+ - written at the beginning of each staff line
136
+ - determines the default alteration for each letter name
105
137
 
106
- Brian Head:
107
- Strings and percussion probably don’t think of themselves as a choir, but if you’re mainly looking for a taxonomically consistent word, that’s the feat I can think of at the moment.
108
- Section is a good word, too, which easily flows between smaller and larger meanings.
138
+ ## Chords
109
139
 
110
- Robert Head:
111
- Obviously, the four “families” is a garbage way to classify all instruments and it really is more applicable to the orchestral context, so maybe “orchestra family” or “orchestra section”.
140
+ chord
141
+ - three or more pitches sounding together
142
+ - built by stacking intervals (typically thirds)
112
143
 
113
- Brian Head:
114
- I’d say that “family” is best used at the instrument level, as in “saxophone family”. Choir or section are better for larger collections. Still, both of those words connote membership in an orchestra. Or large ensemble. “Woodwinds” or “percussion” describe the class of instruments themselves.
144
+ triad
145
+ - a three-note chord: root, third, fifth
146
+ - types: major, minor, diminished, augmented
115
147
 
116
- Robert Head:
117
- Cool
148
+ seventh chord
149
+ - a four-note chord: root, third, fifth, seventh
150
+ - types: major seventh, dominant seventh, minor seventh, half-diminished, diminished
151
+
152
+ inversion
153
+ - a reordering of chord tones so that a note other than the root is the lowest
154
+ - root position, first inversion, second inversion (third inversion for seventh chords)
155
+
156
+ ## Voice Leading and Counterpoint
157
+
158
+ voice leading
159
+ - the way individual melodic lines (voices) move from one chord to the next
160
+ - smooth voice leading minimizes the distance each voice moves
161
+
162
+ types of motion between two voices
163
+ - parallel: both voices move in the same direction by the same interval
164
+ - similar: both voices move in the same direction by different intervals
165
+ - contrary: voices move in opposite directions
166
+ - oblique: one voice stays on the same pitch while the other moves
167
+
168
+ counterpoint
169
+ - the art of combining independent melodic lines
170
+ - species counterpoint is a pedagogical method with five species of increasing complexity
171
+ - first species: note against note
172
+ - second species: two notes against one
173
+ - third species: four notes against one
174
+ - fourth species: suspensions — notes tied across the barline create dissonances that resolve by step
175
+ - fifth species: florid counterpoint, combining all species
176
+
177
+ ## Instrument Families
118
178
 
119
- Action Item: Call them orchestra_section
120
- DONE
179
+ instrument family
180
+ - instruments grouped by shared characteristics
181
+ - broad families: strings, woodwinds, brass, percussion, keyboards, voices
182
+ - specific families: e.g. the oboe family (oboe, English horn, oboe d'amore)
183
+ - instruments within a family share similar tone production and playing technique
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,12 +1,107 @@
1
1
  # Module for style guidelines.
2
2
  module HeadMusic::Style::Guidelines; end
3
3
 
4
- # All rhythmic values are allowed in fifth species counterpoint.
5
- # This guideline is always adherent.
4
+ # Validates that counterpoint notes use only permitted rhythmic values for fifth species.
5
+ # Whole notes (final bar only), half notes, quarter notes, and paired stepwise eighth notes
6
+ # on weak beats are allowed. No dotted rhythms.
6
7
  class HeadMusic::Style::Guidelines::AllowedRhythmicValuesForFifthSpecies < HeadMusic::Style::Annotation
7
- MESSAGE = "All rhythmic values are allowed in fifth species."
8
+ MESSAGE = "Use only permitted rhythmic values: whole (final bar only), half, quarter, " \
9
+ "or paired stepwise eighth notes on weak beats."
10
+
11
+ PERMITTED_UNIT_NAMES = %w[whole half quarter eighth].freeze
8
12
 
9
13
  def marks
10
- []
14
+ [
15
+ *disallowed_unit_marks,
16
+ *dotted_rhythm_marks,
17
+ *whole_note_not_in_final_bar_marks,
18
+ *unpaired_eighth_note_marks,
19
+ *non_stepwise_eighth_note_marks,
20
+ *excess_eighth_note_pair_marks
21
+ ]
22
+ end
23
+
24
+ private
25
+
26
+ def disallowed_unit_marks
27
+ mark_each(notes.reject { |note| PERMITTED_UNIT_NAMES.include?(note.rhythmic_value.unit_name) })
28
+ end
29
+
30
+ def dotted_rhythm_marks
31
+ mark_each(notes.select { |note| note.rhythmic_value.dots > 0 })
32
+ end
33
+
34
+ def whole_note_not_in_final_bar_marks
35
+ mark_each(
36
+ notes
37
+ .select { |note| note.rhythmic_value.unit_name == "whole" }
38
+ .reject { |note| note.position.bar_number == final_bar_number }
39
+ )
40
+ end
41
+
42
+ def unpaired_eighth_note_marks
43
+ mark_each(eighth_notes.reject { |note| paired_eighth?(note) })
44
+ end
45
+
46
+ def non_stepwise_eighth_note_marks
47
+ mark_each(
48
+ eighth_notes
49
+ .select { |note| paired_eighth?(note) }
50
+ .reject { |note| stepwise_eighth?(note) }
51
+ )
52
+ end
53
+
54
+ def excess_eighth_note_pair_marks
55
+ bars_with_excess_eighth_pairs.flat_map do |bar_number|
56
+ mark_each(eighth_notes_in_bar(bar_number)[2..])
57
+ end
58
+ end
59
+
60
+ def mark_each(violating_notes)
61
+ violating_notes.map { |note| HeadMusic::Style::Mark.for(note) }
62
+ end
63
+
64
+ def eighth_notes
65
+ @eighth_notes ||= notes.select { |note| note.rhythmic_value.unit_name == "eighth" }
66
+ end
67
+
68
+ def paired_eighth?(note)
69
+ prev = preceding_note(note)
70
+ foll = following_note(note)
71
+ (prev && prev.rhythmic_value.unit_name == "eighth") ||
72
+ (foll && foll.rhythmic_value.unit_name == "eighth")
73
+ end
74
+
75
+ def stepwise_eighth?(note)
76
+ prev = preceding_note(note)
77
+ foll = following_note(note)
78
+ stepwise_from_prev = prev && HeadMusic::Analysis::MelodicInterval.new(prev, note).step?
79
+ stepwise_to_foll = foll && HeadMusic::Analysis::MelodicInterval.new(note, foll).step?
80
+ stepwise_from_prev || stepwise_to_foll
81
+ end
82
+
83
+ def bars_with_excess_eighth_pairs
84
+ eighth_notes
85
+ .group_by { |note| note.position.bar_number }
86
+ .select { |_bar, bar_eighths| bar_eighths.length > 2 }
87
+ .keys
88
+ end
89
+
90
+ def eighth_notes_in_bar(bar_number)
91
+ eighth_notes.select { |note| note.position.bar_number == bar_number }
92
+ end
93
+
94
+ def final_bar_number
95
+ last_note&.position&.bar_number
96
+ end
97
+
98
+ def preceding_note(note)
99
+ index = notes.index(note)
100
+ notes[index - 1] if index && index > 0
101
+ end
102
+
103
+ def following_note(note)
104
+ index = notes.index(note)
105
+ notes[index + 1] if index && index < notes.length - 1
11
106
  end
12
107
  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,9 +1,11 @@
1
1
  # Module for style guidelines.
2
2
  module HeadMusic::Style::Guidelines; end
3
3
 
4
- # A counterpoint guideline
4
+ # A counterpoint guideline.
5
+ # Dissonant downbeats are permitted only when the counterpoint note is a
6
+ # properly tied-over suspension (i.e., it began before the current CF note).
5
7
  class HeadMusic::Style::Guidelines::ConsonantDownbeats < HeadMusic::Style::Annotation
6
- MESSAGE = "Use consonant harmonic intervals on every downbeat."
8
+ MESSAGE = "Use consonant harmonic intervals on every downbeat (unless a tied suspension)."
7
9
 
8
10
  def marks
9
11
  dissonant_pairs.map do |dissonant_pair|
@@ -14,10 +16,21 @@ class HeadMusic::Style::Guidelines::ConsonantDownbeats < HeadMusic::Style::Annot
14
16
  private
15
17
 
16
18
  def dissonant_pairs
17
- dissonant_intervals.map(&:notes).compact
19
+ non_suspension_dissonant_intervals.map(&:notes).compact
20
+ end
21
+
22
+ def non_suspension_dissonant_intervals
23
+ dissonant_intervals.reject { |interval| tied_suspension?(interval) }
18
24
  end
19
25
 
20
26
  def dissonant_intervals
21
27
  downbeat_harmonic_intervals.select { |interval| interval.dissonance?(:two_part_harmony) }
22
28
  end
29
+
30
+ def tied_suspension?(interval)
31
+ cp_note = voice.note_at(interval.position)
32
+ return false unless cp_note
33
+
34
+ cp_note.position < interval.position
35
+ end
23
36
  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