head_music 12.2.0 → 12.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ccf523b5bb7b74f26a5d52696e46d227428c97279c9218642a0fda8277c845a0
4
- data.tar.gz: 9dacc76d972594748d14b5fad8e479b551c758c9ef7882f88354a28e62d316b3
3
+ metadata.gz: 20155b40b92a030588ea4b1c791efcc37c70aa40e5e2008b0d3a282cb72391a1
4
+ data.tar.gz: e717940c964c2859dcf7181a5ae7997f99cabaf741f6d5ab73dc374977ae2f3f
5
5
  SHA512:
6
- metadata.gz: be9bb15716c3f20d398bb1ba6f3d1faebe08233c271dd3e451da41f2654d7d3a9b3d6272a551c7f4214c28638962712bbb8b4eda412782c86204fd5a9c1eeb29
7
- data.tar.gz: 7285191b3b8be97a2d9cad0f2a650eb7f7e708723047c42d7ea5a80fdc77cc0f303e34727e6d827f40f598d3b735f67353e33b722f4e50877b41214b7d1a6b76
6
+ metadata.gz: 9b97712e65e7ecb1df4dc400854aedfb1ed9fbf0654fdda348dc72d552c6e3f061eaa1dec9b02034d867c7279fe1ad5a6305dd9724cf4c47f8389463486f1f4b
7
+ data.tar.gz: c3bd7b7c5bc5ea85790a7e15998eb25f9a32e6a249ffcbea26b4e420fdde5fad236e8e2ebf7a844405414e97a1104792f8ad0aae85862ed11712aa1103794490
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- head_music (12.2.0)
4
+ head_music (12.3.0)
5
5
  activesupport (>= 7.0, < 10)
6
6
  humanize (~> 2.0)
7
7
  i18n (~> 1.8)
@@ -0,0 +1,27 @@
1
+ # Module for style guidelines.
2
+ module HeadMusic::Style::Guidelines; end
3
+
4
+ # A counterpoint guideline
5
+ class HeadMusic::Style::Guidelines::NoParallelPerfectWithSyncopation < HeadMusic::Style::Annotation
6
+ MESSAGE = "Avoid parallel perfect consonances between syncopated notes."
7
+
8
+ def marks
9
+ parallel_perfect_syncopation_pairs.map do |pair|
10
+ HeadMusic::Style::Mark.for_all(pair.flat_map(&:notes))
11
+ end
12
+ end
13
+
14
+ private
15
+
16
+ def parallel_perfect_syncopation_pairs
17
+ harmonic_intervals.each_cons(2).select do |first, second|
18
+ first.perfect_consonance?(:two_part_harmony) &&
19
+ second.perfect_consonance?(:two_part_harmony) &&
20
+ same_simple_type?(first, second)
21
+ end
22
+ end
23
+
24
+ def same_simple_type?(first, second)
25
+ first.simple_number == second.simple_number
26
+ end
27
+ end
@@ -0,0 +1,27 @@
1
+ # Module for style guidelines.
2
+ module HeadMusic::Style::Guidelines; end
3
+
4
+ # A counterpoint guideline for fourth species counterpoint.
5
+ # For each cantus firmus note, verifies that one or two counterpoint notes
6
+ # are sounding at that position. A note may sustain across the barline
7
+ # (syncopation) rather than starting at the CF note position.
8
+ # Two notes sounding against one CF note is permitted as a "second species break."
9
+ class HeadMusic::Style::Guidelines::OneToOneWithTies < HeadMusic::Style::Annotation
10
+ MESSAGE = "Place one note per cantus firmus note. Notes may sustain across the barline."
11
+
12
+ def marks
13
+ return unless cantus_firmus&.notes
14
+ return if cantus_firmus.notes.empty?
15
+
16
+ HeadMusic::Style::Mark.for_each(uncovered_cantus_firmus_notes)
17
+ end
18
+
19
+ private
20
+
21
+ def uncovered_cantus_firmus_notes
22
+ cantus_firmus.notes.select do |cf_note|
23
+ notes_sounding = voice.notes_during(cf_note)
24
+ notes_sounding.empty? || notes_sounding.length > 2
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,64 @@
1
+ # Module for style guidelines.
2
+ module HeadMusic::Style::Guidelines; end
3
+
4
+ # A counterpoint guideline for fourth species counterpoint.
5
+ # The syncopated texture is occasionally broken: the counterpoint moves on the
6
+ # downbeat instead of sustaining. When this happens, a dissonant off-beat note
7
+ # is permitted only if it is a passing tone. Breaks should be infrequent.
8
+ class HeadMusic::Style::Guidelines::SecondSpeciesBreak < HeadMusic::Style::Guidelines::WeakBeatDissonanceTreatment
9
+ MESSAGE = "Use only passing tones when breaking the syncopated texture. Breaks should be infrequent."
10
+
11
+ MAX_BREAK_RATIO = 0.25
12
+
13
+ def marks
14
+ return [] unless cantus_firmus&.notes&.any?
15
+
16
+ dissonance_marks + frequency_marks
17
+ end
18
+
19
+ private
20
+
21
+ def dissonance_marks
22
+ break_bar_off_beat_notes
23
+ .select { |note| dissonant_with_cantus?(note) }
24
+ .reject { |note| passing_tone?(note) }
25
+ .map { |note| HeadMusic::Style::Mark.for(note) }
26
+ end
27
+
28
+ def frequency_marks
29
+ return [] if total_bars <= 0
30
+ return [] if break_bars.length <= total_bars * MAX_BREAK_RATIO
31
+
32
+ break_bar_notes = break_bars.flat_map { |bar| notes_in_bar(bar) }
33
+ [HeadMusic::Style::Mark.for_all(break_bar_notes, fitness: HeadMusic::SMALL_PENALTY_FACTOR)]
34
+ end
35
+
36
+ def break_bars
37
+ @break_bars ||= (first_bar..last_bar).select { |bar| break_bar?(bar) }
38
+ end
39
+
40
+ def break_bar?(bar)
41
+ downbeats, off_beats = notes_in_bar(bar).partition { |note| downbeat_position?(note.position) }
42
+ downbeats.any? && off_beats.any?
43
+ end
44
+
45
+ def break_bar_off_beat_notes
46
+ break_bars.flat_map { |bar| notes_in_bar(bar).reject { |note| downbeat_position?(note.position) } }
47
+ end
48
+
49
+ def notes_in_bar(bar)
50
+ notes.select { |note| note.position.bar_number == bar }
51
+ end
52
+
53
+ def total_bars
54
+ last_bar - first_bar + 1
55
+ end
56
+
57
+ def first_bar
58
+ cantus_firmus.notes.first.position.bar_number
59
+ end
60
+
61
+ def last_bar
62
+ cantus_firmus.notes.last.position.bar_number
63
+ end
64
+ end
@@ -0,0 +1,65 @@
1
+ # Module for style guidelines.
2
+ module HeadMusic::Style::Guidelines; end
3
+
4
+ # A counterpoint guideline for fourth species suspension treatment.
5
+ # A suspension has three parts:
6
+ # 1. Preparation: The note is consonant with the current cantus firmus note.
7
+ # 2. Suspension: The cantus firmus moves; the counterpoint sustains, becoming dissonant.
8
+ # 3. Resolution: The counterpoint resolves by step (usually down) to a consonance.
9
+ class HeadMusic::Style::Guidelines::SuspensionTreatment < HeadMusic::Style::Annotation
10
+ MESSAGE = "Treat suspensions with proper preparation and stepwise resolution."
11
+
12
+ def marks
13
+ return [] unless cantus_firmus&.notes&.any?
14
+
15
+ improperly_treated_suspensions.map do |note|
16
+ HeadMusic::Style::Mark.for(note)
17
+ end
18
+ end
19
+
20
+ private
21
+
22
+ def improperly_treated_suspensions
23
+ dissonant_suspensions.reject { |note, cf_note| properly_treated?(note, cf_note) }.map(&:first)
24
+ end
25
+
26
+ def dissonant_suspensions
27
+ cantus_firmus.notes[1..].filter_map do |cf_note|
28
+ cp_note = voice.note_at(cf_note.position)
29
+ next unless cp_note && cp_note.position < cf_note.position
30
+
31
+ interval = HeadMusic::Analysis::HarmonicInterval.new(cantus_firmus, voice, cf_note.position)
32
+ next unless interval.notes.length == 2 && interval.dissonance?(:two_part_harmony)
33
+
34
+ [cp_note, cf_note]
35
+ end
36
+ end
37
+
38
+ def properly_treated?(cp_note, cf_note)
39
+ prepared?(cp_note, cf_note) && resolved?(cp_note, cf_note)
40
+ end
41
+
42
+ def prepared?(cp_note, cf_note)
43
+ prev_cf = previous_cf_note(cf_note)
44
+ return false unless prev_cf
45
+
46
+ interval = HeadMusic::Analysis::HarmonicInterval.new(cantus_firmus, voice, cp_note.position)
47
+ interval.notes.length == 2 && interval.consonance?(:two_part_harmony)
48
+ end
49
+
50
+ def resolved?(cp_note, cf_note)
51
+ next_cp = voice.note_following(cp_note.position)
52
+ return false unless next_cp
53
+
54
+ melodic = HeadMusic::Analysis::MelodicInterval.new(cp_note, next_cp)
55
+ return false unless melodic.step?
56
+
57
+ resolution_interval = HeadMusic::Analysis::HarmonicInterval.new(cantus_firmus, voice, next_cp.position)
58
+ resolution_interval.notes.length == 2 && resolution_interval.consonance?(:two_part_harmony)
59
+ end
60
+
61
+ def previous_cf_note(cf_note)
62
+ index = cantus_firmus.notes.index(cf_note)
63
+ cantus_firmus.notes[index - 1] if index && index > 0
64
+ end
65
+ end
@@ -15,14 +15,7 @@ class HeadMusic::Style::Guidelines::ThirdSpeciesDissonanceTreatment < HeadMusic:
15
15
 
16
16
  # Neighbor tone: approached by step, left by step in the opposite direction.
17
17
  def neighbor_tone?(note)
18
- prev = preceding_note(note)
19
- foll = following_note(note)
20
- return false unless prev && foll
21
-
22
- approach = melodic_interval_between(prev, note)
23
- departure = melodic_interval_between(note, foll)
24
-
25
- approach.step? && departure.step? && approach.direction != departure.direction
18
+ stepwise_figure?(note, same_direction: false)
26
19
  end
27
20
 
28
21
  # Nota cambiata: a five-note figure where note 2 is dissonant,
@@ -32,7 +32,7 @@ class HeadMusic::Style::Guidelines::WeakBeatDissonanceTreatment < HeadMusic::Sty
32
32
  end
33
33
 
34
34
  def cantus_firmus_positions
35
- @cantus_firmus_positions ||= Set.new(cantus_firmus.notes.map { |n| n.position.to_s })
35
+ @cantus_firmus_positions ||= Set.new(cantus_firmus.notes.map { |note| note.position.to_s })
36
36
  end
37
37
 
38
38
  def dissonant_with_cantus?(note)
@@ -41,18 +41,26 @@ class HeadMusic::Style::Guidelines::WeakBeatDissonanceTreatment < HeadMusic::Sty
41
41
  end
42
42
 
43
43
  def passing_tone?(note)
44
- prev = preceding_note(note)
45
- foll = following_note(note)
46
- return false unless prev && foll
44
+ stepwise_figure?(note, same_direction: true)
45
+ end
47
46
 
48
- approach = melodic_interval_between(prev, note)
49
- departure = melodic_interval_between(note, foll)
47
+ def stepwise_figure?(note, same_direction:)
48
+ surrounding = surrounding_notes(note)
49
+ return false unless surrounding
50
50
 
51
- approach.step? && departure.step? && approach.direction == departure.direction
51
+ approach = melodic_interval_between(surrounding.first, note)
52
+ departure = melodic_interval_between(note, surrounding.last)
53
+ approach.step? && departure.step? && (approach.direction == departure.direction) == same_direction
54
+ end
55
+
56
+ def surrounding_notes(note)
57
+ prev = preceding_note(note)
58
+ foll = following_note(note)
59
+ [prev, foll] if prev && foll
52
60
  end
53
61
 
54
- def melodic_interval_between(note1, note2)
55
- HeadMusic::Analysis::MelodicInterval.new(note1, note2)
62
+ def melodic_interval_between(first_note, second_note)
63
+ HeadMusic::Analysis::MelodicInterval.new(first_note, second_note)
56
64
  end
57
65
 
58
66
  def preceding_note(note)
@@ -0,0 +1,16 @@
1
+ # Rules for fourth species harmony
2
+ class HeadMusic::Style::Guides::FourthSpeciesHarmony < HeadMusic::Style::Guides::SpeciesHarmony
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,
9
+ HeadMusic::Style::Guidelines::NoParallelPerfectWithSyncopation,
10
+ HeadMusic::Style::Guidelines::NoStrongBeatUnisons,
11
+ HeadMusic::Style::Guidelines::PreferContraryMotion,
12
+ HeadMusic::Style::Guidelines::PreferImperfect,
13
+ HeadMusic::Style::Guidelines::SecondSpeciesBreak,
14
+ HeadMusic::Style::Guidelines::SuspensionTreatment
15
+ ].freeze
16
+ end
@@ -0,0 +1,23 @@
1
+ # Module for guides
2
+ module HeadMusic::Style::Guides; end
3
+
4
+ # Rules for fourth species melodies
5
+ class HeadMusic::Style::Guides::FourthSpeciesMelody < HeadMusic::Style::Guides::SpeciesMelody
6
+ RULESET = [
7
+ HeadMusic::Style::Guidelines::AlwaysMove,
8
+ HeadMusic::Style::Guidelines::ConsonantClimax,
9
+ HeadMusic::Style::Guidelines::Diatonic,
10
+ HeadMusic::Style::Guidelines::EndOnTonic,
11
+ HeadMusic::Style::Guidelines::NoteFillsFinalBar,
12
+ HeadMusic::Style::Guidelines::FrequentDirectionChanges,
13
+ HeadMusic::Style::Guidelines::LimitOctaveLeaps,
14
+ HeadMusic::Style::Guidelines::MostlyConjunct,
15
+ HeadMusic::Style::Guidelines::OneToOneWithTies,
16
+ HeadMusic::Style::Guidelines::PrepareOctaveLeaps,
17
+ HeadMusic::Style::Guidelines::SingableIntervals,
18
+ HeadMusic::Style::Guidelines::SingableRange,
19
+ HeadMusic::Style::Guidelines::StartOnPerfectConsonance,
20
+ HeadMusic::Style::Guidelines::StepOutOfUnison,
21
+ HeadMusic::Style::Guidelines::StepUpToFinalNote
22
+ ].freeze
23
+ end
@@ -1,3 +1,3 @@
1
1
  module HeadMusic
2
- VERSION = "12.2.0"
2
+ VERSION = "12.3.0"
3
3
  end
data/lib/head_music.rb CHANGED
@@ -176,10 +176,12 @@ require "head_music/style/guidelines/mostly_conjunct"
176
176
  require "head_music/style/guidelines/notes_same_length"
177
177
  require "head_music/style/guidelines/no_parallel_perfect_across_barline"
178
178
  require "head_music/style/guidelines/no_parallel_perfect_on_downbeats"
179
+ require "head_music/style/guidelines/no_parallel_perfect_with_syncopation"
179
180
  require "head_music/style/guidelines/no_rests"
180
181
  require "head_music/style/guidelines/no_strong_beat_unisons"
181
182
  require "head_music/style/guidelines/no_unisons_in_middle"
182
183
  require "head_music/style/guidelines/one_to_one"
184
+ require "head_music/style/guidelines/one_to_one_with_ties"
183
185
  require "head_music/style/guidelines/prefer_contrary_motion"
184
186
  require "head_music/style/guidelines/prefer_imperfect"
185
187
  require "head_music/style/guidelines/prepare_octave_leaps"
@@ -194,12 +196,14 @@ require "head_music/style/guidelines/step_down_to_final_note"
194
196
  require "head_music/style/guidelines/step_out_of_unison"
195
197
  require "head_music/style/guidelines/step_to_final_note"
196
198
  require "head_music/style/guidelines/step_up_to_final_note"
199
+ require "head_music/style/guidelines/suspension_treatment"
197
200
  require "head_music/style/guidelines/three_per_bar"
198
201
  require "head_music/style/guidelines/two_per_bar"
199
202
  require "head_music/style/guidelines/up_to_fourteen_notes"
200
203
  require "head_music/style/guidelines/weak_beat_dissonance_treatment"
201
204
  require "head_music/style/guidelines/third_species_dissonance_treatment"
202
205
  require "head_music/style/guidelines/triple_meter_dissonance_treatment"
206
+ require "head_music/style/guidelines/second_species_break"
203
207
 
204
208
  # style guides
205
209
  require "head_music/style/guides/species_melody"
@@ -214,3 +218,5 @@ require "head_music/style/guides/third_species_triple_meter_melody"
214
218
  require "head_music/style/guides/third_species_triple_meter_harmony"
215
219
  require "head_music/style/guides/third_species_melody"
216
220
  require "head_music/style/guides/third_species_harmony"
221
+ require "head_music/style/guides/fourth_species_melody"
222
+ require "head_music/style/guides/fourth_species_harmony"
@@ -0,0 +1,251 @@
1
+ # Fourth-Species Counterpoint: Developer Reference
2
+
3
+ A concise reference for implementing fourth-species counterpoint guidelines in the `head_music` gem. Covers pedagogical foundations, music theory rules, and architectural mapping.
4
+
5
+ ---
6
+
7
+ ## 1. Historical Context
8
+
9
+ ### Fux (1725)
10
+
11
+ Johann Joseph Fux's *Gradus ad Parnassum* establishes the species framework that remains canonical. Fourth species introduces the **suspension**: the counterpoint voice ties across the barline, causing the previously consonant pitch to become dissonant against the cantus firmus on the downbeat, then resolving by step downward on the upbeat. Fux treats suspensions as the defining idiom of Renaissance vocal counterpoint.
12
+
13
+ ### Schenker (1910/1922)
14
+
15
+ Schenker reframes fourth species as a study in **voice-leading prolongation**. The suspension is not merely an ornament but a structural event: the tied note delays (prolongs) the resolution, creating tension that drives forward motion. He emphasizes the three-phase structure (preparation, suspension, resolution) as fundamental to understanding tonal prolongation at all structural levels.
16
+
17
+ ### Contemporary Pedagogy (Salzer & Schachter, Schubert, OMT)
18
+
19
+ Modern treatments preserve the Fuxian suspension taxonomy while clarifying edge cases: when ties break (the "second species break"), how to handle the final cadence, and the treatment of the 2-3 suspension in the bass. Open Music Theory and Schubert's *Modal Counterpoint* both codify the rule that when the syncopated texture is interrupted, the off-beat note reverts to second-species behavior and may be a dissonant passing tone.
20
+
21
+ ---
22
+
23
+ ## 2. The Syncopated Texture
24
+
25
+ Fourth species is built on **rhythmic syncopation**: notes begin on the weak beat of one bar and sustain through the downbeat of the next.
26
+
27
+ In 4/4 or cut time (the typical cantus firmus meter):
28
+
29
+ ```
30
+ Beat: 1 2 3 4
31
+ CF: o--------- (whole note)
32
+ CPT: o---------o---------
33
+ ^weak ^weak
34
+ preparation resolution
35
+ ^strong
36
+ suspension (may be dissonant)
37
+ ```
38
+
39
+ The **sounding duration** is what matters for analysis. A pitch that begins on beat 3 and sustains through beat 1 of the next bar is sounding on that downbeat regardless of how it is notated. See section 6 on notation.
40
+
41
+ ---
42
+
43
+ ## 3. Suspension Types
44
+
45
+ Suspensions are named by the interval sequence: **dissonant interval on the downbeat -- resolved interval on the upbeat**.
46
+
47
+ ### Upper Voice Suspensions (counterpoint above cantus firmus)
48
+
49
+ | Name | Suspension | Resolution | Notes |
50
+ |------|-----------|------------|-------|
51
+ | 7-6 | 7th (dissonant) | 6th | Most common; smooth resolution to imperfect consonance |
52
+ | 4-3 | 4th (dissonant) | 3rd | The 4th is dissonant in two-voice counterpoint |
53
+ | 9-8 | 9th (dissonant) | Octave | Resolves to perfect consonance; use with care |
54
+ | 2-1 | 2nd (dissonant) | Unison | Rare; treated as a form of 9-8 at the octave |
55
+
56
+ ### Lower Voice Suspensions (counterpoint below cantus firmus)
57
+
58
+ | Name | Suspension | Resolution | Notes |
59
+ |------|-----------|------------|-------|
60
+ | 2-3 | 2nd (dissonant) | 3rd | The "bass suspension"; resolution moves downward by step |
61
+ | 4-5 | 4th (dissonant) | 5th | Less common than 2-3 |
62
+ | 9-10 | 9th (dissonant) | 10th (3rd) | Compound form of 2-3 |
63
+
64
+ **Key constraint for all suspensions:** The resolution must move **by step downward** (in the resolving voice) from the suspended pitch. Upward resolution is not permitted in strict species counterpoint.
65
+
66
+ ---
67
+
68
+ ## 4. Suspension Structure: Preparation, Suspension, Resolution
69
+
70
+ Every suspension consists of three phases:
71
+
72
+ 1. **Preparation** -- The pitch to be suspended is heard on a **weak beat** as a **consonance** with the cantus firmus. This establishes the pitch as stable before it becomes dissonant.
73
+
74
+ 2. **Suspension** -- The same pitch is held (or re-articulated as tied) into the **strong beat** (downbeat) of the next bar, where it is now **dissonant** with the cantus firmus.
75
+
76
+ 3. **Resolution** -- On the following **weak beat**, the suspended pitch moves **by step downward** to a consonance.
77
+
78
+ ```
79
+ Phase: PREPARATION SUSPENSION RESOLUTION
80
+ Beat: weak strong weak
81
+ Harmony: consonant dissonant consonant
82
+ Motion: (arrival) (sustained) step down
83
+ ```
84
+
85
+ ### Preparation Requirements
86
+
87
+ - The preparation must be a consonance (not a dissonance, not a rest).
88
+ - The preparation pitch and the suspended pitch are the same pitch -- this is the defining feature of a suspension (vs. an appoggiatura, which arrives unprepared).
89
+ - In head_music terms: the `Placement` on the weak beat must produce a consonant `HarmonicInterval` with the cantus firmus at that position.
90
+
91
+ ### Resolution Requirements
92
+
93
+ - Resolution is **always** by **step downward** in the voice containing the suspension.
94
+ - Resolution arrives on a **consonance** with the cantus firmus.
95
+ - The cantus firmus does not move during the suspension (it is a whole note): the resolution step belongs to the counterpoint voice alone.
96
+
97
+ ---
98
+
99
+ ## 5. The "Second Species Break"
100
+
101
+ The syncopated texture is sometimes interrupted -- the tie does not occur and the voice articulates a new pitch on the weak beat without sustaining into the next downbeat. This moment is called a **second species break** (some sources call it a "cambiata" context or simply describe it as reverting to second-species behavior).
102
+
103
+ **Rule:** When the texture breaks and the off-beat note does not tie into the next downbeat, that off-beat note may be a **dissonant passing tone**, subject to the same conditions as second species:
104
+ - Approached by step.
105
+ - Left by step **in the same direction**.
106
+ - The dissonance falls on the weak beat only.
107
+
108
+ This is the only context in fourth species where a dissonance may appear without being a suspension. The break allows the voice to redirect melodically when a suspension would produce forbidden parallels or other violations.
109
+
110
+ **Implementation note:** Detecting a second species break requires knowing whether a `Placement` at a weak beat position ties into the next downbeat. If `placement.next_position` falls on a downbeat and `voice.note_at(next_downbeat)` returns a different pitch (or no pitch), the tie has broken.
111
+
112
+ ---
113
+
114
+ ## 6. Notation-Agnostic Analysis Principle
115
+
116
+ **Ties are a display concern only.** For analytical purposes, a pitch that sounds continuously from position A to position B is a single sounding event, regardless of how many notated note heads or tie symbols represent it.
117
+
118
+ These two notations are analytically identical in fourth species:
119
+
120
+ - A whole note C4 beginning at beat 3 of bar 1, sustained through beat 1 of bar 2.
121
+ - A half note C4 at beat 3 of bar 1, tied to a half note C4 at beat 1 of bar 2.
122
+
123
+ **Guidelines operate on sounding durations, not on notated note heads.**
124
+
125
+ In head_music, this principle is already encoded in the data model:
126
+
127
+ - A `Placement` represents a single sounding event with a `position` (when it starts) and a `rhythmic_value` (how long it lasts).
128
+ - `Placement#next_position` computes when the sound ends: `position + rhythmic_value`.
129
+ - The display layer (ties, beaming, notation symbols) is a separate concern handled by the `HeadMusic::Notation` module.
130
+
131
+ Analytical guidelines should never count note heads or detect ties in the notated score. Instead, they should query `voice.note_at(position)` for any given position to determine what is sounding.
132
+
133
+ ---
134
+
135
+ ## 7. Mapping to head_music Architecture
136
+
137
+ ### Voice and Placement Model
138
+
139
+ ```
140
+ Voice
141
+ #placements -> [Placement, ...] all sounding events (notes and rests)
142
+ #notes -> [Placement, ...] only pitched placements
143
+ #note_at(pos) -> Placement | nil what is sounding at a given Position
144
+ #note_preceding(pos) -> Placement | nil the note whose position is before pos
145
+ #note_following(pos) -> Placement | nil the note whose position is after pos
146
+
147
+ Placement
148
+ #position -> Position when the event begins
149
+ #rhythmic_value -> RhythmicValue how long it lasts
150
+ #next_position -> Position position + rhythmic_value (when it ends)
151
+ #pitch -> Pitch | nil nil for rests
152
+ #note? -> bool
153
+ #rest? -> bool
154
+ ```
155
+
156
+ ### Position
157
+
158
+ ```
159
+ Position
160
+ #within_placement?(placement) -> bool true if self >= placement.position
161
+ AND self < placement.next_position
162
+ #strong? -> bool downbeat (strength >= 80)
163
+ #weak? -> bool not strong
164
+ #bar_number -> Integer
165
+ #count -> Integer beat within bar
166
+ ```
167
+
168
+ `Position#within_placement?` is the key predicate for "is this pitch sounding at this moment?" It returns true for any position that falls during the placement's duration -- including the middle of a long note. This is the mechanism that makes sustained notes invisible to position-based queries.
169
+
170
+ ### HarmonicInterval
171
+
172
+ ```
173
+ HarmonicInterval.new(voice1, voice2, position)
174
+ ```
175
+
176
+ Internally calls `voice.note_at(position)` for each voice. Because `note_at` uses `position.within_placement?`, it correctly finds notes that **began earlier and are still sounding**, not only notes that **start at** the given position.
177
+
178
+ This means `HarmonicInterval` already handles sustained pitches correctly. A suspended note that began on beat 3 and is still sounding on beat 1 of the next bar will be found and evaluated as a harmonic interval at beat 1 without any special casing in the guideline code.
179
+
180
+ ### Identifying Suspension Phases in Guidelines
181
+
182
+ To analyze a suspension at a given downbeat position:
183
+
184
+ ```ruby
185
+ # The suspended note: sounding at the downbeat but started earlier
186
+ suspended_note = counterpoint_voice.note_at(downbeat_position)
187
+
188
+ # It is a suspension only if it started before the downbeat
189
+ is_suspension = suspended_note && suspended_note.position < downbeat_position
190
+
191
+ # Preparation: the same placement, evaluated at the previous weak beat
192
+ # (suspended_note itself IS the preparation placement)
193
+ preparation_harmonic_interval = HarmonicInterval.new(
194
+ cantus_firmus, counterpoint_voice, suspended_note.position
195
+ )
196
+
197
+ # Suspension: harmonic interval at the downbeat
198
+ suspension_harmonic_interval = HarmonicInterval.new(
199
+ cantus_firmus, counterpoint_voice, downbeat_position
200
+ )
201
+
202
+ # Resolution: the note that follows the sustained note
203
+ resolution_note = counterpoint_voice.note_following(downbeat_position)
204
+ ```
205
+
206
+ ### Guidelines: Annotation and Mark
207
+
208
+ Guidelines inherit from `HeadMusic::Style::Annotation` and override the `marks` method, returning an array of `HeadMusic::Style::Mark` objects.
209
+
210
+ ```ruby
211
+ class MyGuideline < HeadMusic::Style::Annotation
212
+ MESSAGE = "Description of the rule."
213
+
214
+ def marks
215
+ # Collect placements that violate the rule.
216
+ # Return Mark objects for each violation.
217
+ violating_placements.map do |placement|
218
+ HeadMusic::Style::Mark.for(placement, fitness: 0)
219
+ end
220
+ end
221
+ end
222
+ ```
223
+
224
+ `Mark.for(placement)` creates a mark spanning `placement.position` to `placement.next_position`. `Mark.for_all(placements)` creates a single mark spanning a group. `Mark.for_each(placements)` creates one mark per placement.
225
+
226
+ Fitness of `0` signals a hard violation (forbidden). The default `HeadMusic::PENALTY_FACTOR` is used for soft violations (discouraged but not forbidden).
227
+
228
+ ### Guideline Classification for Fourth Species
229
+
230
+ **Hard violations (fitness: 0):**
231
+ - Suspension not prepared as a consonance (preparation must be consonant).
232
+ - Suspension resolution not by step downward.
233
+ - Suspension resolution not to a consonance.
234
+ - Dissonance on a downbeat that is not a valid suspension.
235
+ - Off-beat dissonance in a second-species break that is not a passing tone (approached and left by step in same direction).
236
+
237
+ **Soft penalties (PENALTY_FACTOR):**
238
+ - Failure to use suspensions where they are available (too many direct consonances reduces interest).
239
+ - Resolution to a perfect consonance (prefer imperfect).
240
+ - Parallel perfect consonances on successive downbeats.
241
+
242
+ ---
243
+
244
+ ## 8. Sources
245
+
246
+ - Fux, J.J. *Gradus ad Parnassum* (1725). Trans. Alfred Mann, *The Study of Counterpoint* (W.W. Norton, 1965).
247
+ - Schenker, H. *Kontrapunkt* (1910/1922). Trans. Rothgeb & Thym (Musicalia Press, 2001).
248
+ - Salzer, F. & Schachter, C. *Counterpoint in Composition* (Columbia UP, 1969).
249
+ - Schubert, P. *Modal Counterpoint: Renaissance Style*, 2nd ed. (Oxford UP, 2008).
250
+ - [Open Music Theory -- Fourth-Species Counterpoint](https://viva.pressbooks.pub/openmusictheory/chapter/fourth-species-counterpoint/)
251
+ - [Puget Sound Music Theory -- Fourth Species](https://musictheory.pugetsound.edu/mt21c/FourthSpecies.html)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: head_music
3
3
  version: !ruby/object:Gem::Version
4
- version: 12.2.0
4
+ version: 12.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rob Head
@@ -291,6 +291,7 @@ files:
291
291
  - lib/head_music/style/guidelines/mostly_conjunct.rb
292
292
  - lib/head_music/style/guidelines/no_parallel_perfect_across_barline.rb
293
293
  - lib/head_music/style/guidelines/no_parallel_perfect_on_downbeats.rb
294
+ - lib/head_music/style/guidelines/no_parallel_perfect_with_syncopation.rb
294
295
  - lib/head_music/style/guidelines/no_rests.rb
295
296
  - lib/head_music/style/guidelines/no_strong_beat_unisons.rb
296
297
  - lib/head_music/style/guidelines/no_unisons_in_middle.rb
@@ -299,10 +300,12 @@ files:
299
300
  - lib/head_music/style/guidelines/notes_same_length.rb
300
301
  - lib/head_music/style/guidelines/one_per_bar.rb
301
302
  - lib/head_music/style/guidelines/one_to_one.rb
303
+ - lib/head_music/style/guidelines/one_to_one_with_ties.rb
302
304
  - lib/head_music/style/guidelines/prefer_contrary_motion.rb
303
305
  - lib/head_music/style/guidelines/prefer_imperfect.rb
304
306
  - lib/head_music/style/guidelines/prepare_octave_leaps.rb
305
307
  - lib/head_music/style/guidelines/recover_large_leaps.rb
308
+ - lib/head_music/style/guidelines/second_species_break.rb
306
309
  - lib/head_music/style/guidelines/singable_intervals.rb
307
310
  - lib/head_music/style/guidelines/singable_range.rb
308
311
  - lib/head_music/style/guidelines/single_large_leaps.rb
@@ -312,6 +315,7 @@ files:
312
315
  - lib/head_music/style/guidelines/step_out_of_unison.rb
313
316
  - lib/head_music/style/guidelines/step_to_final_note.rb
314
317
  - lib/head_music/style/guidelines/step_up_to_final_note.rb
318
+ - lib/head_music/style/guidelines/suspension_treatment.rb
315
319
  - lib/head_music/style/guidelines/third_species_dissonance_treatment.rb
316
320
  - lib/head_music/style/guidelines/three_per_bar.rb
317
321
  - lib/head_music/style/guidelines/triple_meter_dissonance_treatment.rb
@@ -320,6 +324,8 @@ files:
320
324
  - lib/head_music/style/guidelines/weak_beat_dissonance_treatment.rb
321
325
  - lib/head_music/style/guides/first_species_harmony.rb
322
326
  - lib/head_music/style/guides/first_species_melody.rb
327
+ - lib/head_music/style/guides/fourth_species_harmony.rb
328
+ - lib/head_music/style/guides/fourth_species_melody.rb
323
329
  - lib/head_music/style/guides/fux_cantus_firmus.rb
324
330
  - lib/head_music/style/guides/modern_cantus_firmus.rb
325
331
  - lib/head_music/style/guides/second_species_harmony.rb
@@ -348,6 +354,7 @@ files:
348
354
  - lib/head_music/utilities/case.rb
349
355
  - lib/head_music/utilities/hash_key.rb
350
356
  - lib/head_music/version.rb
357
+ - references/fourth-species-counterpoint.md
351
358
  - references/second-species-counterpoint.md
352
359
  - references/third-species-counterpoint.md
353
360
  - user_stories/backlog/notation-style.md