head_music 11.8.0 → 12.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +1 -1
  3. data/lib/head_music/analysis/dyad.rb +55 -106
  4. data/lib/head_music/analysis/pitch_collection.rb +15 -4
  5. data/lib/head_music/content/voice.rb +3 -2
  6. data/lib/head_music/instruments/instrument.rb +18 -29
  7. data/lib/head_music/rudiment/pitch.rb +9 -6
  8. data/lib/head_music/rudiment/tuning/just_intonation.rb +1 -20
  9. data/lib/head_music/rudiment/tuning/meantone.rb +1 -20
  10. data/lib/head_music/rudiment/tuning/pythagorean.rb +1 -20
  11. data/lib/head_music/rudiment/tuning.rb +13 -2
  12. data/lib/head_music/style/guidelines/directional_step_to_final_note.rb +28 -0
  13. data/lib/head_music/style/guidelines/four_to_one.rb +4 -49
  14. data/lib/head_music/style/guidelines/note_count_per_bar.rb +54 -0
  15. data/lib/head_music/style/guidelines/singable_intervals.rb +2 -2
  16. data/lib/head_music/style/guidelines/step_down_to_final_note.rb +2 -19
  17. data/lib/head_music/style/guidelines/step_up_to_final_note.rb +2 -19
  18. data/lib/head_music/style/guidelines/third_species_dissonance_treatment.rb +10 -83
  19. data/lib/head_music/style/guidelines/three_to_one.rb +57 -0
  20. data/lib/head_music/style/guidelines/triple_meter_dissonance_treatment.rb +26 -0
  21. data/lib/head_music/style/guidelines/two_to_one.rb +5 -49
  22. data/lib/head_music/style/guidelines/weak_beat_dissonance_treatment.rb +11 -3
  23. data/lib/head_music/style/guides/first_species_harmony.rb +1 -8
  24. data/lib/head_music/style/guides/first_species_melody.rb +1 -5
  25. data/lib/head_music/style/guides/fux_cantus_firmus.rb +1 -5
  26. data/lib/head_music/style/guides/modern_cantus_firmus.rb +1 -5
  27. data/lib/head_music/style/guides/second_species_harmony.rb +1 -8
  28. data/lib/head_music/style/guides/second_species_melody.rb +1 -5
  29. data/lib/head_music/style/guides/species_harmony.rb +9 -0
  30. data/lib/head_music/style/guides/species_melody.rb +9 -0
  31. data/lib/head_music/style/guides/third_species_harmony.rb +1 -8
  32. data/lib/head_music/style/guides/third_species_melody.rb +1 -5
  33. data/lib/head_music/style/guides/triple_meter_harmony.rb +15 -0
  34. data/lib/head_music/style/guides/triple_meter_melody.rb +22 -0
  35. data/lib/head_music/time/event_map_support.rb +29 -0
  36. data/lib/head_music/time/meter_map.rb +2 -92
  37. data/lib/head_music/time/tempo_map.rb +14 -96
  38. data/lib/head_music/time.rb +1 -0
  39. data/lib/head_music/version.rb +1 -1
  40. data/lib/head_music.rb +9 -1
  41. metadata +11 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2683c0fce1a231c327156e69951df7590f9ba507a08643d9838bb7c67371dc4d
4
- data.tar.gz: 120a035414104720af26cf0cdada21785da646f8827f4354f37c12e45c263f89
3
+ metadata.gz: c2be29f6b7e43ea3463fa98fba14bbdeea632fe94472df74b5c286b0952b322e
4
+ data.tar.gz: 5ec5d730b43357a8aa2283e3ea73d23596b21a983d5b9bc93e80bfd910eb8a92
5
5
  SHA512:
6
- metadata.gz: 78fd1c9698ae12bf14e9db26131f4d642b241251a9f50ce29aabcf7c631888c2a558572977ed9240e26719b5f7feea28dd1274c2e3985eec0f1c7a922af2c5b7
7
- data.tar.gz: 236fd7f24ee4a4ad073cca5dae89da522b62ba8d34601d75d40523ad274b9c427d210f34e933e145c055f8485ff2c7a03d780fa967892340c2c4ec67c7c016f6
6
+ metadata.gz: 17d1a2575d8b32a6c5ce22c4e522ca7c84bbcc6ffc66d22c3cd76d7af49d2c82628b096eeeee40e4d38def2eaeed5123a4281131c35b5eb432da680a08631eab
7
+ data.tar.gz: ccc33122bdadc8deccfd4258abebf7723fd12ec15dddeacc164529c4477dd69c0d5a575fe9c0a7251199c588e374896c1288a6991e883eb60893aa18443598f6
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- head_music (11.8.0)
4
+ head_music (12.0.0)
5
5
  activesupport (>= 7.0, < 10)
6
6
  humanize (~> 2.0)
7
7
  i18n (~> 1.8)
@@ -68,95 +68,59 @@ class HeadMusic::Analysis::Dyad
68
68
 
69
69
  private
70
70
 
71
- def generate_possible_trichords
72
- trichords = []
73
- pitch_classes = [lower_pitch.pitch_class, upper_pitch.pitch_class]
74
-
75
- HeadMusic::Rudiment::Spelling::CHROMATIC_SPELLINGS.each do |root_spelling|
76
- root_pitch = HeadMusic::Rudiment::Pitch.get("#{root_spelling}4")
77
-
78
- # Try all common trichord types from this root
79
- trichord_intervals = [
80
- %w[M3 P5], # major triad
81
- %w[m3 P5], # minor triad
82
- %w[m3 d5], # diminished triad
83
- %w[M3 A5], # augmented triad
84
- %w[P4 P5], # sus4 (not a triad)
85
- %w[M2 P5] # sus2 (not a triad)
86
- ]
87
-
88
- trichord_intervals.each do |intervals|
89
- trichord_pitches = [root_pitch]
90
-
91
- # Each interval is FROM THE ROOT, not consecutive
92
- intervals.each do |interval_name|
93
- interval = HeadMusic::Analysis::DiatonicInterval.get(interval_name)
94
- next_pitch = interval.above(root_pitch)
95
- trichord_pitches << next_pitch
96
- end
97
-
98
- pitch_collection = HeadMusic::Analysis::PitchCollection.new(trichord_pitches)
99
- trichord_pitch_classes = pitch_collection.pitch_classes
71
+ TRICHORD_INTERVALS = [
72
+ %w[M3 P5], # major triad
73
+ %w[m3 P5], # minor triad
74
+ %w[m3 d5], # diminished triad
75
+ %w[M3 A5], # augmented triad
76
+ %w[P4 P5], # sus4 (not a triad)
77
+ %w[M2 P5] # sus2 (not a triad)
78
+ ].freeze
79
+
80
+ SEVENTH_CHORD_INTERVALS = [
81
+ %w[M3 P5 M7], # major seventh
82
+ %w[M3 P5 m7], # dominant seventh (major-minor)
83
+ %w[m3 P5 m7], # minor seventh
84
+ %w[m3 P5 M7], # minor-major seventh
85
+ %w[m3 d5 m7], # half-diminished seventh
86
+ %w[m3 d5 d7], # diminished seventh
87
+ %w[M2 M3 P5 m7], # dominant ninth
88
+ %w[m2 M3 P5 m7], # dominant minor ninth
89
+ %w[M2 m3 P5 m7], # minor ninth
90
+ %w[M2 M3 P5 M7] # major ninth
91
+ ].freeze
100
92
 
101
- # Check if this trichord contains both pitches from our dyad
102
- if pitch_classes.all? { |pc| trichord_pitch_classes.include?(pc) }
103
- trichords << pitch_collection
104
- end
105
- end
106
- end
107
-
108
- trichords.uniq { |t| t.pitch_classes.sort.map(&:to_i) }
93
+ def generate_possible_trichords
94
+ generate_possible_chords(TRICHORD_INTERVALS)
109
95
  end
110
96
 
111
97
  def generate_possible_seventh_chords
112
- seventh_chords = []
113
- pitch_classes = [lower_pitch.pitch_class, upper_pitch.pitch_class]
98
+ generate_possible_chords(SEVENTH_CHORD_INTERVALS)
99
+ end
100
+
101
+ def generate_possible_chords(interval_sets)
102
+ dyad_pitch_classes = [lower_pitch.pitch_class, upper_pitch.pitch_class]
103
+ chords = []
114
104
 
115
105
  HeadMusic::Rudiment::Spelling::CHROMATIC_SPELLINGS.each do |root_spelling|
116
106
  root_pitch = HeadMusic::Rudiment::Pitch.get("#{root_spelling}4")
117
107
 
118
- # Try all common seventh chord types from this root
119
- seventh_chord_intervals = [
120
- %w[M3 P5 M7], # major seventh
121
- %w[M3 P5 m7], # dominant seventh (major-minor)
122
- %w[m3 P5 m7], # minor seventh
123
- %w[m3 P5 M7], # minor-major seventh
124
- %w[m3 d5 m7], # half-diminished seventh
125
- %w[m3 d5 d7], # diminished seventh
126
- %w[M2 M3 P5 m7], # dominant ninth
127
- %w[m2 M3 P5 m7], # dominant minor ninth
128
- %w[M2 m3 P5 m7], # minor ninth
129
- %w[M2 M3 P5 M7] # major ninth
130
- ]
131
-
132
- seventh_chord_intervals.each do |intervals|
133
- chord_pitches = [root_pitch]
134
-
135
- # Each interval is FROM THE ROOT, not consecutive
136
- intervals.each do |interval_name|
137
- interval = HeadMusic::Analysis::DiatonicInterval.get(interval_name)
138
- next_pitch = interval.above(root_pitch)
139
- chord_pitches << next_pitch
140
- end
141
-
108
+ interval_sets.each do |intervals|
109
+ chord_pitches = [root_pitch] + intervals.map { |name| HeadMusic::Analysis::DiatonicInterval.get(name).above(root_pitch) }
142
110
  pitch_collection = HeadMusic::Analysis::PitchCollection.new(chord_pitches)
143
- chord_pitch_classes = pitch_collection.pitch_classes
144
111
 
145
- # Check if this chord contains both pitches from our dyad
146
- if pitch_classes.all? { |pc| chord_pitch_classes.include?(pc) }
147
- seventh_chords << pitch_collection
112
+ if dyad_pitch_classes.all? { |pc| pitch_collection.pitch_classes.include?(pc) }
113
+ chords << pitch_collection
148
114
  end
149
115
  end
150
116
  end
151
117
 
152
- seventh_chords.uniq { |c| c.pitch_classes.sort.map(&:to_i) }
118
+ chords.uniq { |chord| chord.pitch_classes.sort.map(&:to_i) }
153
119
  end
154
120
 
155
121
  def filter_by_key(pitch_collections)
156
122
  return pitch_collections unless key
157
123
 
158
- diatonic_spellings = key.scale.spellings
159
-
160
124
  pitch_collections.select do |pitch_collection|
161
125
  pitch_collection.pitches.all? { |pitch| diatonic_spellings.include?(pitch.spelling) }
162
126
  end
@@ -165,65 +129,50 @@ class HeadMusic::Analysis::Dyad
165
129
  def sort_by_diatonic_agreement(pitch_collections)
166
130
  return pitch_collections unless key
167
131
 
168
- diatonic_spellings = key.scale.spellings
169
-
170
132
  pitch_collections.sort_by do |pitch_collection|
171
- # Count how many pitches match diatonic spellings (lower is better for sort)
172
- diatonic_count = pitch_collection.pitches.count { |pitch| diatonic_spellings.include?(pitch.spelling) }
173
- -diatonic_count # Negative so higher counts come first
133
+ -pitch_collection.pitches.count { |pitch| diatonic_spellings.include?(pitch.spelling) }
174
134
  end
175
135
  end
176
136
 
137
+ def diatonic_spellings
138
+ @diatonic_spellings ||= key.scale.spellings
139
+ end
140
+
177
141
  def generate_enharmonic_respellings
178
142
  respellings = []
179
143
 
180
144
  # Get enharmonic equivalents for each pitch
181
- pitch1_equivalents = get_enharmonic_equivalents(pitch1)
182
- pitch2_equivalents = get_enharmonic_equivalents(pitch2)
145
+ pitch1_equivalents = enharmonic_equivalents_for(pitch1)
146
+ pitch2_equivalents = enharmonic_equivalents_for(pitch2)
183
147
 
184
148
  # Generate all combinations
185
- pitch1_equivalents.each do |p1|
186
- pitch2_equivalents.each do |p2|
187
- # Skip the original combination
188
- next if p1.spelling == pitch1.spelling && p2.spelling == pitch2.spelling
149
+ pitch1_equivalents.each do |lower|
150
+ pitch2_equivalents.each do |upper|
151
+ next if lower.spelling == pitch1.spelling && upper.spelling == pitch2.spelling
189
152
 
190
- # Create new dyad with same key context
191
- respellings << self.class.new(p1, p2, key: key)
153
+ respellings << self.class.new(lower, upper, key: key)
192
154
  end
193
155
  end
194
156
 
195
157
  respellings
196
158
  end
197
159
 
198
- def get_enharmonic_equivalents(pitch)
199
- equivalents = [pitch]
160
+ ALTERATION_SIGNS = {-2 => "bb", -1 => "b", 0 => "", 1 => "#", 2 => "##"}.freeze
200
161
 
201
- # Get common enharmonic spellings
202
- pitch_class = pitch.pitch_class
203
- letter_names = HeadMusic::Rudiment::LetterName.all
162
+ def enharmonic_equivalents_for(pitch)
163
+ target_pitch_class = pitch.pitch_class
164
+ equivalents = [pitch]
204
165
 
205
- letter_names.each do |letter_name|
206
- [-2, -1, 0, 1, 2].each do |alteration_semitones|
207
- spelling = HeadMusic::Rudiment::Spelling.get("#{letter_name}#{alteration_sign(alteration_semitones)}")
208
- next unless spelling
166
+ HeadMusic::Rudiment::LetterName.all.each do |letter_name|
167
+ ALTERATION_SIGNS.each_value do |sign|
168
+ spelling = HeadMusic::Rudiment::Spelling.get("#{letter_name}#{sign}")
169
+ next unless spelling && spelling.pitch_class == target_pitch_class
170
+ next if equivalents.any? { |equiv| equiv.spelling == spelling }
209
171
 
210
- if spelling.pitch_class == pitch_class
211
- equivalent_pitch = HeadMusic::Rudiment::Pitch.fetch_or_create(spelling, pitch.register)
212
- equivalents << equivalent_pitch unless equivalents.any? { |p| p.spelling == spelling }
213
- end
172
+ equivalents << HeadMusic::Rudiment::Pitch.fetch_or_create(spelling, pitch.register)
214
173
  end
215
174
  end
216
175
 
217
176
  equivalents
218
177
  end
219
-
220
- def alteration_sign(semitones)
221
- case semitones
222
- when -2 then "bb"
223
- when -1 then "b"
224
- when 0 then ""
225
- when 1 then "#"
226
- when 2 then "##"
227
- end
228
- end
229
178
  end
@@ -110,20 +110,27 @@ class HeadMusic::Analysis::PitchCollection
110
110
  major_triad? || minor_triad?
111
111
  end
112
112
 
113
+ TRIAD_PATTERNS = {
114
+ major: [%w[M3 m3], %w[m3 P4], %w[P4 M3]],
115
+ minor: [%w[m3 M3], %w[M3 P4], %w[P4 m3]],
116
+ diminished: [%w[m3 m3], %w[m3 A4], %w[A4 m3]],
117
+ augmented: [%w[M3 M3], %w[M3 d4], %w[d4 M3]]
118
+ }.freeze
119
+
113
120
  def major_triad?
114
- [%w[M3 m3], %w[m3 P4], %w[P4 M3]].include? reduction_diatonic_intervals.map(&:shorthand)
121
+ triad_type?(:major)
115
122
  end
116
123
 
117
124
  def minor_triad?
118
- [%w[m3 M3], %w[M3 P4], %w[P4 m3]].include? reduction_diatonic_intervals.map(&:shorthand)
125
+ triad_type?(:minor)
119
126
  end
120
127
 
121
128
  def diminished_triad?
122
- [%w[m3 m3], %w[m3 A4], %w[A4 m3]].include? reduction_diatonic_intervals.map(&:shorthand)
129
+ triad_type?(:diminished)
123
130
  end
124
131
 
125
132
  def augmented_triad?
126
- [%w[M3 M3], %w[M3 d4], %w[d4 M3]].include? reduction_diatonic_intervals.map(&:shorthand)
133
+ triad_type?(:augmented)
127
134
  end
128
135
 
129
136
  def root_position_triad?
@@ -195,6 +202,10 @@ class HeadMusic::Analysis::PitchCollection
195
202
 
196
203
  private
197
204
 
205
+ def triad_type?(type)
206
+ TRIAD_PATTERNS[type].include?(reduction_diatonic_intervals.map(&:shorthand))
207
+ end
208
+
198
209
  def reduction_pitches
199
210
  pitches.map do |pitch|
200
211
  pitch = HeadMusic::Rudiment::Pitch.fetch_or_create(pitch.spelling, pitch.register - 1) while pitch > bass_pitch + 12
@@ -60,8 +60,8 @@ class HeadMusic::Content::Voice
60
60
  end
61
61
 
62
62
  def melodic_note_pairs
63
- @melodic_note_pairs ||= notes.each_cons(2).map do |note1, note2|
64
- HeadMusic::Content::Voice::MelodicNotePair.new(note1, note2)
63
+ @melodic_note_pairs ||= notes.each_cons(2).map do |first_note, second_note|
64
+ HeadMusic::Content::Voice::MelodicNotePair.new(first_note, second_note)
65
65
  end
66
66
  end
67
67
 
@@ -135,6 +135,7 @@ class HeadMusic::Content::Voice
135
135
  pitches.first(10).map(&:to_s).join(" ")
136
136
  end
137
137
 
138
+ # A pair of consecutive notes in a melodic line, used to analyze intervals and leaps.
138
139
  class MelodicNotePair
139
140
  attr_reader :first_note, :second_note
140
141
 
@@ -1,3 +1,4 @@
1
+ # A module for musical instruments and their properties
1
2
  module HeadMusic::Instruments; end
2
3
 
3
4
  # A musical instrument with parent-based inheritance.
@@ -36,15 +37,12 @@ class HeadMusic::Instruments::Instrument
36
37
  def get(name, variant_key = nil)
37
38
  return name if name.is_a?(self)
38
39
 
39
- # Handle two-argument form for backward compatibility
40
+ name_str = name.to_s
40
41
  if variant_key
41
- combined_name = "#{name}_#{variant_key}"
42
- result = find_valid_instrument(combined_name) || find_valid_instrument(name.to_s)
42
+ find_valid_instrument("#{name_str}_#{variant_key}") || find_valid_instrument(name_str)
43
43
  else
44
- result = find_valid_instrument(name.to_s) || find_valid_instrument(normalize_variant_name(name))
44
+ find_valid_instrument(name_str) || find_valid_instrument(normalize_variant_name(name_str))
45
45
  end
46
-
47
- result
48
46
  end
49
47
 
50
48
  def find_valid_instrument(name)
@@ -63,24 +61,14 @@ class HeadMusic::Instruments::Instrument
63
61
  # Convert shorthand variant names to full form
64
62
  # e.g., "trumpet_in_eb" -> "trumpet_in_e_flat"
65
63
  # e.g., "clarinet_in_bb" -> "clarinet_in_b_flat"
66
- def normalize_variant_name(name)
67
- name_str = name.to_s
64
+ VARIANT_PATTERN = /^(.+)_in_([a-g])([b#])$/i
68
65
 
69
- # Match patterns like "_in_eb" or "_in_bb" at the end (flat)
70
- flat_pattern = /^(.+)_in_([a-g])b$/i
71
- sharp_pattern = %r{^(.+)_in_([a-g])\#$}i
72
-
73
- if name_str =~ flat_pattern
74
- instrument = Regexp.last_match(1)
75
- note = Regexp.last_match(2).downcase
76
- "#{instrument}_in_#{note}_flat"
77
- elsif name_str =~ sharp_pattern
78
- instrument = Regexp.last_match(1)
79
- note = Regexp.last_match(2).downcase
80
- "#{instrument}_in_#{note}_sharp"
81
- else
82
- name_str
83
- end
66
+ def normalize_variant_name(name_str)
67
+ match = VARIANT_PATTERN.match(name_str.to_s)
68
+ return name_str.to_s unless match
69
+
70
+ suffix = (match[3] == "b") ? "flat" : "sharp"
71
+ "#{match[1]}_in_#{match[2].downcase}_#{suffix}"
84
72
  end
85
73
  end
86
74
 
@@ -306,13 +294,14 @@ class HeadMusic::Instruments::Instrument
306
294
  def pitch_key_to_designation
307
295
  return nil unless pitch_key
308
296
 
309
- key = pitch_key.to_s
310
- if key.end_with?("_flat")
311
- "#{key[0].upcase}b"
312
- elsif key.end_with?("_sharp")
313
- "#{key[0].upcase}#"
297
+ pitch_key_str = pitch_key.to_s
298
+ first_letter = pitch_key_str[0].upcase
299
+ if pitch_key_str.end_with?("_flat")
300
+ "#{first_letter}b"
301
+ elsif pitch_key_str.end_with?("_sharp")
302
+ "#{first_letter}#"
314
303
  else
315
- key.upcase
304
+ pitch_key_str.upcase
316
305
  end
317
306
  end
318
307
 
@@ -56,9 +56,10 @@ class HeadMusic::Rudiment::Pitch < HeadMusic::Rudiment::Base
56
56
  end
57
57
 
58
58
  def self.from_number(number)
59
- return nil unless number == number.to_i
59
+ number_int = number.to_i
60
+ return nil unless number == number_int
60
61
 
61
- fetch_or_create(HeadMusic::Rudiment::Spelling.from_number(number), (number.to_i / 12) - 1)
62
+ fetch_or_create(HeadMusic::Rudiment::Spelling.from_number(number), (number_int / 12) - 1)
62
63
  end
63
64
 
64
65
  def self.from_number_and_letter(number, letter_name)
@@ -71,9 +72,10 @@ class HeadMusic::Rudiment::Pitch < HeadMusic::Rudiment::Base
71
72
  end
72
73
 
73
74
  def self.natural_letter_pitch(number, letter_name)
75
+ number_int = number.to_i
74
76
  natural_letter_pitch = get(HeadMusic::Rudiment::LetterName.get(letter_name).pitch_class)
75
- natural_letter_pitch += 12 while (number.to_i - natural_letter_pitch.to_i) >= 6
76
- natural_letter_pitch -= 12 while (number.to_i - natural_letter_pitch.to_i) <= -6
77
+ natural_letter_pitch += 12 while (number_int - natural_letter_pitch.to_i) >= 6
78
+ natural_letter_pitch -= 12 while (number_int - natural_letter_pitch.to_i) <= -6
77
79
  get(natural_letter_pitch)
78
80
  end
79
81
 
@@ -218,9 +220,10 @@ class HeadMusic::Rudiment::Pitch < HeadMusic::Rudiment::Base
218
220
  end
219
221
 
220
222
  def helmholtz_letter_name
221
- return spelling.to_s.downcase if HeadMusic::Rudiment::Register.get(register).helmholtz_case == :lower
223
+ spelling_str = spelling.to_s
224
+ return spelling_str.downcase if HeadMusic::Rudiment::Register.get(register).helmholtz_case == :lower
222
225
 
223
- spelling.to_s
226
+ spelling_str
224
227
  end
225
228
 
226
229
  def helmholtz_marks
@@ -21,26 +21,7 @@ class HeadMusic::Rudiment::Tuning::JustIntonation < HeadMusic::Rudiment::Tuning
21
21
  octave: Rational(2, 1)
22
22
  }.freeze
23
23
 
24
- attr_reader :tonal_center
25
-
26
24
  def initialize(reference_pitch: :a440, tonal_center: nil)
27
- super
28
- @tonal_center = HeadMusic::Rudiment::Pitch.get(tonal_center || "C4")
29
- end
30
-
31
- def frequency_for(pitch)
32
- pitch = HeadMusic::Rudiment::Pitch.get(pitch)
33
-
34
- # Calculate the frequency of the tonal center using equal temperament from reference pitch
35
- tonal_center_frequency = calculate_tonal_center_frequency
36
-
37
- # Calculate the interval from the tonal center to the requested pitch
38
- interval_from_tonal_center = (pitch - tonal_center).semitones
39
-
40
- # Get the just intonation ratio for this interval
41
- ratio = ratio_for_interval(interval_from_tonal_center)
42
-
43
- # Calculate the frequency
44
- tonal_center_frequency * ratio
25
+ super(reference_pitch: reference_pitch, tonal_center: tonal_center || "C4")
45
26
  end
46
27
  end
@@ -23,26 +23,7 @@ class HeadMusic::Rudiment::Tuning::Meantone < HeadMusic::Rudiment::Tuning
23
23
  octave: Rational(2, 1) # Octave (2.0)
24
24
  }.freeze
25
25
 
26
- attr_reader :tonal_center
27
-
28
26
  def initialize(reference_pitch: :a440, tonal_center: nil)
29
- super
30
- @tonal_center = HeadMusic::Rudiment::Pitch.get(tonal_center || "C4")
31
- end
32
-
33
- def frequency_for(pitch)
34
- pitch = HeadMusic::Rudiment::Pitch.get(pitch)
35
-
36
- # Calculate the frequency of the tonal center using equal temperament from reference pitch
37
- tonal_center_frequency = calculate_tonal_center_frequency
38
-
39
- # Calculate the interval from the tonal center to the requested pitch
40
- interval_from_tonal_center = (pitch - tonal_center).semitones
41
-
42
- # Get the meantone ratio for this interval
43
- ratio = ratio_for_interval(interval_from_tonal_center)
44
-
45
- # Calculate the frequency
46
- tonal_center_frequency * ratio
27
+ super(reference_pitch: reference_pitch, tonal_center: tonal_center || "C4")
47
28
  end
48
29
  end
@@ -27,26 +27,7 @@ class HeadMusic::Rudiment::Tuning::Pythagorean < HeadMusic::Rudiment::Tuning
27
27
  diminished_second: Rational(256, 243) # Same as minor second in Pythagorean
28
28
  }.freeze
29
29
 
30
- attr_reader :tonal_center
31
-
32
30
  def initialize(reference_pitch: :a440, tonal_center: nil)
33
- super
34
- @tonal_center = HeadMusic::Rudiment::Pitch.get(tonal_center || "C4")
35
- end
36
-
37
- def frequency_for(pitch)
38
- pitch = HeadMusic::Rudiment::Pitch.get(pitch)
39
-
40
- # Calculate the frequency of the tonal center using equal temperament from reference pitch
41
- tonal_center_frequency = calculate_tonal_center_frequency
42
-
43
- # Calculate the interval from the tonal center to the requested pitch
44
- interval_from_tonal_center = (pitch - tonal_center).semitones
45
-
46
- # Get the Pythagorean ratio for this interval
47
- ratio = ratio_for_interval(interval_from_tonal_center)
48
-
49
- # Calculate the frequency
50
- tonal_center_frequency * ratio
31
+ super(reference_pitch: reference_pitch, tonal_center: tonal_center || "C4")
51
32
  end
52
33
  end
@@ -23,18 +23,29 @@ class HeadMusic::Rudiment::Tuning < HeadMusic::Rudiment::Base
23
23
  end
24
24
  end
25
25
 
26
+ attr_reader :tonal_center
27
+
26
28
  def initialize(reference_pitch: :a440, tonal_center: nil)
27
29
  @reference_pitch = HeadMusic::Rudiment::ReferencePitch.get(reference_pitch)
28
- @tonal_center = tonal_center
30
+ @tonal_center = tonal_center ? HeadMusic::Rudiment::Pitch.get(tonal_center) : nil
29
31
  end
30
32
 
31
33
  def frequency_for(pitch)
32
34
  pitch = HeadMusic::Rudiment::Pitch.get(pitch)
33
- reference_pitch_frequency * (2**(1.0 / 12))**(pitch - reference_pitch.pitch).semitones
35
+ return equal_temperament_frequency(pitch) unless tonal_center
36
+
37
+ tonal_center_frequency = calculate_tonal_center_frequency
38
+ interval_from_tonal_center = (pitch - tonal_center).semitones
39
+ ratio = ratio_for_interval(interval_from_tonal_center)
40
+ tonal_center_frequency * ratio
34
41
  end
35
42
 
36
43
  private
37
44
 
45
+ def equal_temperament_frequency(pitch)
46
+ reference_pitch_frequency * (2**(1.0 / 12))**(pitch - reference_pitch.pitch).semitones
47
+ end
48
+
38
49
  def calculate_tonal_center_frequency
39
50
  # Use equal temperament to get the tonal center frequency from the reference pitch
40
51
  interval_to_tonal_center = (tonal_center - reference_pitch.pitch).semitones
@@ -0,0 +1,28 @@
1
+ # Module for style guidelines.
2
+ module HeadMusic::Style::Guidelines; end
3
+
4
+ # Base class for guidelines requiring a step in a specific direction to the final note.
5
+ class HeadMusic::Style::Guidelines::DirectionalStepToFinalNote < HeadMusic::Style::Annotation
6
+ def marks
7
+ return if last_melodic_interval.nil?
8
+
9
+ fitness = 1
10
+ fitness *= HeadMusic::PENALTY_FACTOR unless step?
11
+ fitness *= HeadMusic::PENALTY_FACTOR unless expected_direction?
12
+ HeadMusic::Style::Mark.for_all(notes[-2..], fitness: fitness) if fitness < 1
13
+ end
14
+
15
+ private
16
+
17
+ def expected_direction?
18
+ raise NotImplementedError
19
+ end
20
+
21
+ def step?
22
+ last_melodic_interval&.step?
23
+ end
24
+
25
+ def last_melodic_interval
26
+ @last_melodic_interval ||= melodic_intervals.last
27
+ end
28
+ end
@@ -2,26 +2,10 @@
2
2
  module HeadMusic::Style::Guidelines; end
3
3
 
4
4
  # A counterpoint guideline
5
- class HeadMusic::Style::Guidelines::FourToOne < HeadMusic::Style::Annotation
5
+ class HeadMusic::Style::Guidelines::FourToOne < HeadMusic::Style::Guidelines::NoteCountPerBar
6
6
  MESSAGE = "Use four quarter notes against each whole note in the cantus firmus."
7
7
 
8
8
  QUARTER = HeadMusic::Rudiment::RhythmicValue.get(:quarter)
9
- WHOLE = HeadMusic::Rudiment::RhythmicValue.get(:whole)
10
-
11
- def marks
12
- return [] unless cantus_firmus&.notes&.any?
13
-
14
- cantus_firmus.notes.each_with_index.filter_map do |cf_note, index|
15
- bar_number = cf_note.position.bar_number
16
- if index == cantus_firmus.notes.length - 1
17
- check_final_bar(bar_number)
18
- elsif index == 0
19
- check_first_bar(bar_number)
20
- else
21
- check_middle_bar(bar_number)
22
- end
23
- end
24
- end
25
9
 
26
10
  private
27
11
 
@@ -42,49 +26,20 @@ class HeadMusic::Style::Guidelines::FourToOne < HeadMusic::Style::Annotation
42
26
  mark_bar(bar_number)
43
27
  end
44
28
 
45
- def check_final_bar(bar_number)
46
- bar_notes = notes_in_bar(bar_number)
47
- return if one_whole_note?(bar_notes)
48
-
49
- mark_bar(bar_number)
50
- end
51
-
52
29
  def four_quarter_notes?(bar_notes)
53
- bar_notes.length == 4 && bar_notes.all? { |n| n.rhythmic_value == QUARTER }
54
- end
55
-
56
- def one_whole_note?(bar_notes)
57
- bar_notes.length == 1 && bar_notes.first.rhythmic_value == WHOLE
30
+ bar_notes.length == 4 && bar_notes.all? { |note| note.rhythmic_value == QUARTER }
58
31
  end
59
32
 
60
33
  def rest_then_three_quarter_notes?(bar_notes, bar_rests)
61
34
  bar_notes.length == 3 &&
62
- bar_notes.all? { |n| n.rhythmic_value == QUARTER } &&
35
+ bar_notes.all? { |note| note.rhythmic_value == QUARTER } &&
63
36
  bar_rests.length == 1 &&
64
37
  bar_rests.first.rhythmic_value == QUARTER
65
38
  end
66
39
 
67
40
  def three_quarter_notes_after_downbeat?(bar_notes)
68
41
  bar_notes.length == 3 &&
69
- bar_notes.all? { |n| n.rhythmic_value == QUARTER } &&
42
+ bar_notes.all? { |note| note.rhythmic_value == QUARTER } &&
70
43
  bar_notes.first.position.count > 1
71
44
  end
72
-
73
- def notes_in_bar(bar_number)
74
- notes.select { |n| n.position.bar_number == bar_number }
75
- end
76
-
77
- def rests_in_bar(bar_number)
78
- rests.select { |r| r.position.bar_number == bar_number }
79
- end
80
-
81
- def mark_bar(bar_number)
82
- bar_placements = notes_in_bar(bar_number)
83
- if bar_placements.any?
84
- HeadMusic::Style::Mark.for_all(bar_placements)
85
- else
86
- cf_note = cantus_firmus.notes.detect { |n| n.position.bar_number == bar_number }
87
- HeadMusic::Style::Mark.for(cf_note) if cf_note
88
- end
89
- end
90
45
  end