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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +110 -0
- data/CLAUDE.md +27 -11
- data/Gemfile.lock +2 -1
- data/MUSIC_THEORY.md +97 -34
- data/Rakefile +2 -2
- data/lib/head_music/style/annotation.rb +31 -1
- data/lib/head_music/style/guidelines/allowed_rhythmic_values_for_fifth_species.rb +99 -4
- data/lib/head_music/style/guidelines/at_least_eight_notes.rb +2 -24
- data/lib/head_music/style/guidelines/consonant_downbeats.rb +16 -3
- data/lib/head_music/style/guidelines/direction_changes.rb +10 -2
- data/lib/head_music/style/guidelines/dissonance_figure_detection.rb +71 -0
- data/lib/head_music/style/guidelines/florid_dissonance_treatment.rb +11 -8
- data/lib/head_music/style/guidelines/four_per_bar.rb +2 -12
- data/lib/head_music/style/guidelines/frequent_direction_changes.rb +1 -4
- data/lib/head_music/style/guidelines/limit_octave_leaps.rb +11 -2
- data/lib/head_music/style/guidelines/maximum_notes.rb +24 -0
- data/lib/head_music/style/guidelines/minimum_notes.rb +34 -0
- data/lib/head_music/style/guidelines/mixed_rhythmic_values.rb +4 -4
- data/lib/head_music/style/guidelines/moderate_direction_changes.rb +1 -4
- data/lib/head_music/style/guidelines/mostly_conjunct.rb +7 -2
- data/lib/head_music/style/guidelines/note_count_per_bar.rb +26 -1
- data/lib/head_music/style/guidelines/one_per_bar.rb +2 -12
- data/lib/head_music/style/guidelines/second_species_break.rb +5 -1
- data/lib/head_music/style/guidelines/singable_range.rb +18 -2
- data/lib/head_music/style/guidelines/suspension_treatment.rb +3 -3
- data/lib/head_music/style/guidelines/third_species_dissonance_treatment.rb +2 -65
- data/lib/head_music/style/guidelines/three_per_bar.rb +2 -12
- data/lib/head_music/style/guidelines/two_per_bar.rb +2 -12
- data/lib/head_music/style/guidelines/up_to_fourteen_notes.rb +2 -14
- data/lib/head_music/style/guides/base.rb +10 -0
- data/lib/head_music/style/guides/combined_first_second_third_species_harmony.rb +1 -7
- data/lib/head_music/style/guides/combined_first_second_third_species_melody.rb +4 -9
- data/lib/head_music/style/guides/diatonic_melody.rb +11 -0
- data/lib/head_music/style/guides/fifth_species_harmony.rb +4 -8
- data/lib/head_music/style/guides/fifth_species_melody.rb +7 -10
- data/lib/head_music/style/guides/first_species_harmony.rb +2 -8
- data/lib/head_music/style/guides/first_species_melody.rb +2 -7
- data/lib/head_music/style/guides/fourth_species_harmony.rb +1 -7
- data/lib/head_music/style/guides/fourth_species_melody.rb +4 -9
- data/lib/head_music/style/guides/fux_cantus_firmus.rb +1 -6
- data/lib/head_music/style/guides/modern_cantus_firmus.rb +1 -6
- data/lib/head_music/style/guides/second_species_harmony.rb +1 -7
- data/lib/head_music/style/guides/second_species_melody.rb +4 -9
- data/lib/head_music/style/guides/species_harmony.rb +14 -5
- data/lib/head_music/style/guides/species_melody.rb +13 -5
- data/lib/head_music/style/guides/third_species_harmony.rb +1 -7
- data/lib/head_music/style/guides/third_species_melody.rb +4 -9
- data/lib/head_music/style/guides/third_species_triple_meter_harmony.rb +1 -7
- data/lib/head_music/style/guides/third_species_triple_meter_melody.rb +4 -9
- data/lib/head_music/version.rb +1 -1
- data/lib/head_music.rb +6 -3
- data/references/fifth-species-counterpoint.md +578 -0
- metadata +9 -6
- data/lib/head_music/style/guidelines/allowed_rhythmic_values_for_combined234.rb +0 -23
- data/lib/head_music/style/guides/combined_second_third_fourth_species_harmony.rb +0 -18
- 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:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: db91f6ff65b081bb369f3d2a85e2bc6e6cd092bb0018c2c416bf36bcfc4d4e9d
|
|
4
|
+
data.tar.gz: 266914dfbc382dc27591ea37302b57571b8693f2e0e4bd5189d2fffdff2a7c00
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
30
|
-
bundle exec rake
|
|
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
|
-
-
|
|
97
|
-
|
|
98
|
-
5. **HeadMusic::
|
|
99
|
-
-
|
|
100
|
-
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
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
|
-
|
|
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
|
-
-
|
|
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
|
|
68
|
-
- staccato –
|
|
69
|
-
- tenuto - held
|
|
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
|
-
##
|
|
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
|
-
|
|
110
|
+
## Scales and Modes
|
|
82
111
|
|
|
83
|
-
|
|
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
|
-
|
|
86
|
-
|
|
117
|
+
major scale
|
|
118
|
+
- pattern: W-W-H-W-W-W-H (W = whole step, H = half step)
|
|
87
119
|
|
|
88
|
-
|
|
89
|
-
|
|
120
|
+
natural minor scale
|
|
121
|
+
- pattern: W-H-W-W-H-W-W
|
|
90
122
|
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
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
|
-
|
|
99
|
-
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
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
|
-
|
|
111
|
-
|
|
140
|
+
chord
|
|
141
|
+
- three or more pitches sounding together
|
|
142
|
+
- built by stacking intervals (typically thirds)
|
|
112
143
|
|
|
113
|
-
|
|
114
|
-
|
|
144
|
+
triad
|
|
145
|
+
- a three-note chord: root, third, fifth
|
|
146
|
+
- types: major, minor, diminished, augmented
|
|
115
147
|
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
|
|
120
|
-
|
|
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
|
|
37
|
-
task
|
|
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
|
-
#
|
|
5
|
-
#
|
|
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 = "
|
|
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::
|
|
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
|
-
|
|
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
|
|
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 -
|
|
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
|