head_music 8.2.0 → 8.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.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +1 -1
  3. data/.github/workflows/release.yml +1 -1
  4. data/CHANGELOG.md +14 -0
  5. data/CLAUDE.md +134 -0
  6. data/Gemfile.lock +25 -25
  7. data/Rakefile +2 -2
  8. data/TODO.md +41 -150
  9. data/bin/check_instrument_consistency.rb +86 -0
  10. data/check_instrument_consistency.rb +0 -0
  11. data/head_music.gemspec +1 -1
  12. data/lib/head_music/analysis/diatonic_interval/naming.rb +1 -1
  13. data/lib/head_music/analysis/diatonic_interval.rb +28 -7
  14. data/lib/head_music/analysis/sonority.rb +1 -1
  15. data/lib/head_music/content/staff.rb +13 -3
  16. data/lib/head_music/content/voice.rb +8 -0
  17. data/lib/head_music/instruments/instrument.rb +1 -3
  18. data/lib/head_music/instruments/instrument_families.yml +37 -2
  19. data/lib/head_music/instruments/instruments.yml +352 -367
  20. data/lib/head_music/instruments/variant.rb +1 -1
  21. data/lib/head_music/locales/de.yml +20 -0
  22. data/lib/head_music/locales/en.yml +92 -0
  23. data/lib/head_music/locales/es.yml +19 -0
  24. data/lib/head_music/locales/fr.yml +19 -0
  25. data/lib/head_music/locales/it.yml +20 -0
  26. data/lib/head_music/locales/ru.yml +21 -2
  27. data/lib/head_music/rudiment/rhythmic_unit.rb +93 -26
  28. data/lib/head_music/rudiment/scale_degree.rb +8 -3
  29. data/lib/head_music/rudiment/tuning/just_intonation.rb +85 -0
  30. data/lib/head_music/rudiment/tuning/meantone.rb +87 -0
  31. data/lib/head_music/rudiment/tuning/pythagorean.rb +91 -0
  32. data/lib/head_music/rudiment/tuning.rb +17 -3
  33. data/lib/head_music/style/annotation.rb +6 -6
  34. data/lib/head_music/style/guidelines/consonant_climax.rb +2 -2
  35. data/lib/head_music/style/guidelines/notes_same_length.rb +16 -16
  36. data/lib/head_music/version.rb +1 -1
  37. data/lib/head_music.rb +3 -0
  38. data/user_stories/backlog/band-score-order.md +38 -0
  39. data/user_stories/backlog/chamber-ensemble-score-order.md +33 -0
  40. data/user_stories/backlog/consonance-dissonance-classification.md +57 -0
  41. data/user_stories/backlog/dyad-analysis.md +65 -0
  42. data/user_stories/backlog/orchestral-score-order.md +43 -0
  43. data/user_stories/backlog/pitch-class-set-analysis.md +39 -0
  44. data/user_stories/backlog/pitch-set-classification.md +62 -0
  45. data/user_stories/backlog/sonority-identification.md +47 -0
  46. metadata +18 -4
@@ -4,21 +4,45 @@ module HeadMusic::Rudiment; end
4
4
  # A rhythmic unit is a rudiment of duration consisting of doublings and divisions of a whole note.
5
5
  class HeadMusic::Rudiment::RhythmicUnit
6
6
  include HeadMusic::Named
7
+ include Comparable
7
8
 
8
- MULTIPLES = ["whole", "double whole", "longa", "maxima"].freeze
9
- FRACTIONS = [
9
+ # Note values longer than a whole note
10
+ AMERICAN_MULTIPLES_NAMES = [
11
+ "whole", "double whole", "longa", "maxima"
12
+ ].freeze
13
+
14
+ # Note values from whole note down to very short subdivisions
15
+ AMERICAN_DIVISIONS_NAMES = [
10
16
  "whole", "half", "quarter", "eighth", "sixteenth", "thirty-second",
11
17
  "sixty-fourth", "hundred twenty-eighth", "two hundred fifty-sixth"
12
18
  ].freeze
13
19
 
14
- BRITISH_MULTIPLE_NAMES = %w[semibreve breve longa maxima].freeze
15
- BRITISH_DIVISION_NAMES = %w[
20
+ # British terminology for note values longer than a whole note
21
+ BRITISH_MULTIPLES_NAMES = %w[semibreve breve longa maxima].freeze
22
+
23
+ # British terminology for standard note divisions
24
+ BRITISH_DIVISIONS_NAMES = %w[
16
25
  semibreve minim crotchet quaver semiquaver demisemiquaver
17
26
  hemidemisemiquaver semihemidemisemiquaver demisemihemidemisemiquaver
18
27
  ].freeze
19
28
 
29
+ # Notehead symbols used for different note values
30
+ NOTEHEADS = {
31
+ maxima: 8.0,
32
+ longa: 4.0,
33
+ breve: 2.0,
34
+ open: [0.5, 1.0],
35
+ closed: :default
36
+ }.freeze
37
+
20
38
  def self.for_denominator_value(denominator)
21
- get(FRACTIONS[Math.log2(denominator).to_i])
39
+ return nil unless denominator.is_a?(Numeric) && denominator > 0
40
+ return nil unless (denominator & (denominator - 1)) == 0 # Check if power of 2
41
+
42
+ index = Math.log2(denominator).to_i
43
+ return nil if index >= AMERICAN_DIVISIONS_NAMES.length
44
+
45
+ get(AMERICAN_DIVISIONS_NAMES[index])
22
46
  end
23
47
 
24
48
  attr_reader :numerator, :denominator
@@ -27,7 +51,28 @@ class HeadMusic::Rudiment::RhythmicUnit
27
51
  get_by_name(name)
28
52
  end
29
53
 
54
+ def self.all
55
+ @all ||= (AMERICAN_MULTIPLES_NAMES.reverse + AMERICAN_DIVISIONS_NAMES).uniq.map { |name| get(name) }.compact
56
+ end
57
+
58
+ # Check if a name represents a valid rhythmic unit
59
+ def self.valid_name?(name)
60
+ normalized = normalize_name(name)
61
+ all_normalized_names.include?(normalized)
62
+ end
63
+
64
+ def self.all_normalized_names
65
+ @all_normalized_names ||= (
66
+ AMERICAN_MULTIPLES_NAMES.map { |n| normalize_name(n) } +
67
+ AMERICAN_DIVISIONS_NAMES.map { |n| normalize_name(n) } +
68
+ BRITISH_MULTIPLES_NAMES.map { |n| normalize_name(n) } +
69
+ BRITISH_DIVISIONS_NAMES.map { |n| normalize_name(n) }
70
+ ).uniq
71
+ end
72
+
30
73
  def initialize(canonical_name)
74
+ raise ArgumentError, "Name cannot be nil or empty" if canonical_name.to_s.strip.empty?
75
+
31
76
  self.name = canonical_name
32
77
  @numerator = 2**numerator_exponent
33
78
  @denominator = 2**denominator_exponent
@@ -42,65 +87,87 @@ class HeadMusic::Rudiment::RhythmicUnit
42
87
  end
43
88
 
44
89
  def notehead
45
- return :maxima if relative_value == 8
46
- return :longa if relative_value == 4
47
- return :breve if relative_value == 2
48
- return :open if [0.5, 1].include? relative_value
90
+ value = relative_value
91
+ return :maxima if value == NOTEHEADS[:maxima]
92
+ return :longa if value == NOTEHEADS[:longa]
93
+ return :breve if value == NOTEHEADS[:breve]
94
+ return :open if NOTEHEADS[:open].include?(value)
49
95
 
50
96
  :closed
51
97
  end
52
98
 
53
99
  def flags
54
- FRACTIONS.include?(name) ? [FRACTIONS.index(name) - 2, 0].max : 0
100
+ AMERICAN_DIVISIONS_NAMES.include?(name) ? [AMERICAN_DIVISIONS_NAMES.index(name) - 2, 0].max : 0
55
101
  end
56
102
 
57
103
  def stemmed?
58
104
  relative_value < 1
59
105
  end
60
106
 
107
+ # Returns true if this note value is commonly used in modern notation
108
+ def common?
109
+ AMERICAN_DIVISIONS_NAMES[0..6].include?(name) || BRITISH_DIVISIONS_NAMES[0..6].include?(name)
110
+ end
111
+
112
+ def <=>(other)
113
+ return nil unless other.is_a?(self.class)
114
+
115
+ relative_value <=> other.relative_value
116
+ end
117
+
61
118
  def british_name
62
- if multiple?
63
- BRITISH_MULTIPLE_NAMES[MULTIPLES.index(name)]
64
- elsif fraction?
65
- BRITISH_DIVISION_NAMES[FRACTIONS.index(name)]
66
- elsif BRITISH_MULTIPLE_NAMES.include?(name) || BRITISH_DIVISION_NAMES.include?(name)
67
- name
119
+ if has_american_multiple_name?
120
+ index = AMERICAN_MULTIPLES_NAMES.index(name)
121
+ return BRITISH_MULTIPLES_NAMES[index] unless index.nil?
122
+ elsif has_american_division_name?
123
+ index = AMERICAN_DIVISIONS_NAMES.index(name)
124
+ return BRITISH_DIVISIONS_NAMES[index] unless index.nil?
125
+ elsif BRITISH_MULTIPLES_NAMES.include?(name) || BRITISH_DIVISIONS_NAMES.include?(name)
126
+ return name
68
127
  end
128
+
129
+ nil # Return nil if no British equivalent found
69
130
  end
70
131
 
71
132
  private_class_method :new
72
133
 
134
+ def self.normalize_name(name)
135
+ name.to_s.gsub(/\W+/, "_")
136
+ end
137
+
73
138
  private
74
139
 
75
- def multiple?
76
- MULTIPLES.include?(name)
140
+ def has_american_multiple_name?
141
+ AMERICAN_MULTIPLES_NAMES.include?(name)
77
142
  end
78
143
 
79
- def fraction?
80
- FRACTIONS.include?(name)
144
+ def has_american_division_name?
145
+ AMERICAN_DIVISIONS_NAMES.include?(name)
81
146
  end
82
147
 
83
148
  def numerator_exponent
84
- multiples_keys.index(name.gsub(/\W+/, "_")) || british_multiples_keys.index(name.gsub(/\W+/, "_")) || 0
149
+ normalized_name = self.class.normalize_name(name)
150
+ multiples_keys.index(normalized_name) || british_multiples_keys.index(normalized_name) || 0
85
151
  end
86
152
 
87
153
  def multiples_keys
88
- MULTIPLES.map { |multiple| multiple.gsub(/\W+/, "_") }
154
+ AMERICAN_MULTIPLES_NAMES.map { |multiple| self.class.normalize_name(multiple) }
89
155
  end
90
156
 
91
157
  def british_multiples_keys
92
- BRITISH_MULTIPLE_NAMES.map { |multiple| multiple.gsub(/\W+/, "_") }
158
+ BRITISH_MULTIPLES_NAMES.map { |multiple| self.class.normalize_name(multiple) }
93
159
  end
94
160
 
95
161
  def denominator_exponent
96
- fractions_keys.index(name.gsub(/\W+/, "_")) || british_fractions_keys.index(name.gsub(/\W+/, "_")) || 0
162
+ normalized_name = self.class.normalize_name(name)
163
+ fractions_keys.index(normalized_name) || british_fractions_keys.index(normalized_name) || 0
97
164
  end
98
165
 
99
166
  def fractions_keys
100
- FRACTIONS.map { |fraction| fraction.gsub(/\W+/, "_") }
167
+ AMERICAN_DIVISIONS_NAMES.map { |fraction| self.class.normalize_name(fraction) }
101
168
  end
102
169
 
103
170
  def british_fractions_keys
104
- BRITISH_DIVISION_NAMES.map { |fraction| fraction.gsub(/\W+/, "_") }
171
+ BRITISH_DIVISIONS_NAMES.map { |fraction| self.class.normalize_name(fraction) }
105
172
  end
106
173
  end
@@ -38,11 +38,16 @@ class HeadMusic::Rudiment::ScaleDegree
38
38
  end
39
39
 
40
40
  def <=>(other)
41
- if other.is_a?(HeadMusic::Rudiment::ScaleDegree)
41
+ case other
42
+ when HeadMusic::Rudiment::ScaleDegree
42
43
  [degree, alteration_semitones] <=> [other.degree, other.alteration_semitones]
43
- else
44
- # TODO: Improve this
44
+ when Numeric
45
+ degree <=> other
46
+ when String
45
47
  to_s <=> other.to_s
48
+ else
49
+ # If we can't meaningfully compare, return nil (Ruby standard)
50
+ nil
46
51
  end
47
52
  end
48
53
 
@@ -0,0 +1,85 @@
1
+ # A module for music rudiments
2
+ module HeadMusic::Rudiment; end
3
+
4
+ # Just Intonation tuning system based on whole number frequency ratios
5
+ class HeadMusic::Rudiment::Tuning::JustIntonation < HeadMusic::Rudiment::Tuning
6
+ # Frequency ratios for intervals in just intonation (relative to tonic)
7
+ # Based on the major scale with pure intervals
8
+ INTERVAL_RATIOS = {
9
+ unison: Rational(1, 1),
10
+ minor_second: Rational(16, 15),
11
+ major_second: Rational(9, 8),
12
+ minor_third: Rational(6, 5),
13
+ major_third: Rational(5, 4),
14
+ perfect_fourth: Rational(4, 3),
15
+ tritone: Rational(45, 32),
16
+ perfect_fifth: Rational(3, 2),
17
+ minor_sixth: Rational(8, 5),
18
+ major_sixth: Rational(5, 3),
19
+ minor_seventh: Rational(16, 9),
20
+ major_seventh: Rational(15, 8),
21
+ octave: Rational(2, 1)
22
+ }.freeze
23
+
24
+ attr_reader :tonal_center
25
+
26
+ 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
45
+ end
46
+
47
+ private
48
+
49
+ def calculate_tonal_center_frequency
50
+ # Use equal temperament to get the tonal center frequency from the reference pitch
51
+ interval_to_tonal_center = (tonal_center - reference_pitch.pitch).semitones
52
+ reference_pitch_frequency * (2**(interval_to_tonal_center / 12.0))
53
+ end
54
+
55
+ def ratio_for_interval(semitones)
56
+ # Handle octaves
57
+ octaves = semitones / 12
58
+ interval_within_octave = semitones % 12
59
+
60
+ # Make sure we handle negative intervals
61
+ if interval_within_octave < 0
62
+ interval_within_octave += 12
63
+ octaves -= 1
64
+ end
65
+
66
+ # Get the base ratio
67
+ base_ratio = case interval_within_octave
68
+ when 0 then INTERVAL_RATIOS[:unison]
69
+ when 1 then INTERVAL_RATIOS[:minor_second]
70
+ when 2 then INTERVAL_RATIOS[:major_second]
71
+ when 3 then INTERVAL_RATIOS[:minor_third]
72
+ when 4 then INTERVAL_RATIOS[:major_third]
73
+ when 5 then INTERVAL_RATIOS[:perfect_fourth]
74
+ when 6 then INTERVAL_RATIOS[:tritone]
75
+ when 7 then INTERVAL_RATIOS[:perfect_fifth]
76
+ when 8 then INTERVAL_RATIOS[:minor_sixth]
77
+ when 9 then INTERVAL_RATIOS[:major_sixth]
78
+ when 10 then INTERVAL_RATIOS[:minor_seventh]
79
+ when 11 then INTERVAL_RATIOS[:major_seventh]
80
+ end
81
+
82
+ # Apply octave adjustments
83
+ base_ratio * (2**octaves)
84
+ end
85
+ end
@@ -0,0 +1,87 @@
1
+ # A module for music rudiments
2
+ module HeadMusic::Rudiment; end
3
+
4
+ # Quarter-comma meantone temperament
5
+ # Optimizes major thirds to be pure (5:4) at the expense of perfect fifths
6
+ class HeadMusic::Rudiment::Tuning::Meantone < HeadMusic::Rudiment::Tuning
7
+ # Frequency ratios for intervals in quarter-comma meantone temperament
8
+ # The defining characteristic is that major thirds are pure (5:4)
9
+ # and the syntonic comma is distributed equally among the four fifths
10
+ INTERVAL_RATIOS = {
11
+ unison: Rational(1, 1),
12
+ minor_second: 5.0**(1.0 / 4) / 2.0**(1.0 / 2), # ~1.0697
13
+ major_second: 5.0**(1.0 / 4), # ~1.1892 (fourth root of 5)
14
+ minor_third: 5.0**(1.0 / 2) / 2.0**(1.0 / 2), # ~1.5811
15
+ major_third: Rational(5, 4), # Pure major third (1.25)
16
+ perfect_fourth: 2.0**(1.0 / 2) / 5.0**(1.0 / 4), # ~1.3375
17
+ tritone: 5.0**(3.0 / 4) / 2.0**(1.0 / 2), # ~1.6719
18
+ perfect_fifth: Rational(3, 2), # ~1.4953 (slightly flat)
19
+ minor_sixth: 2.0**(3.0 / 2) / 5.0**(1.0 / 4), # ~1.6818
20
+ major_sixth: 5.0**(3.0 / 4), # ~1.8877
21
+ minor_seventh: 2.0**(3.0 / 2) / 5.0**(1.0 / 2), # ~1.8877
22
+ major_seventh: Rational(25, 16), # ~1.5625
23
+ octave: Rational(2, 1) # Octave (2.0)
24
+ }.freeze
25
+
26
+ attr_reader :tonal_center
27
+
28
+ 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
47
+ end
48
+
49
+ private
50
+
51
+ def calculate_tonal_center_frequency
52
+ # Use equal temperament to get the tonal center frequency from the reference pitch
53
+ interval_to_tonal_center = (tonal_center - reference_pitch.pitch).semitones
54
+ reference_pitch_frequency * (2**(interval_to_tonal_center / 12.0))
55
+ end
56
+
57
+ def ratio_for_interval(semitones)
58
+ # Handle octaves
59
+ octaves = semitones / 12
60
+ interval_within_octave = semitones % 12
61
+
62
+ # Make sure we handle negative intervals
63
+ if interval_within_octave < 0
64
+ interval_within_octave += 12
65
+ octaves -= 1
66
+ end
67
+
68
+ # Get the base ratio
69
+ base_ratio = case interval_within_octave
70
+ when 0 then INTERVAL_RATIOS[:unison]
71
+ when 1 then INTERVAL_RATIOS[:minor_second]
72
+ when 2 then INTERVAL_RATIOS[:major_second]
73
+ when 3 then INTERVAL_RATIOS[:minor_third]
74
+ when 4 then INTERVAL_RATIOS[:major_third]
75
+ when 5 then INTERVAL_RATIOS[:perfect_fourth]
76
+ when 6 then INTERVAL_RATIOS[:tritone]
77
+ when 7 then INTERVAL_RATIOS[:perfect_fifth]
78
+ when 8 then INTERVAL_RATIOS[:minor_sixth]
79
+ when 9 then INTERVAL_RATIOS[:major_sixth]
80
+ when 10 then INTERVAL_RATIOS[:minor_seventh]
81
+ when 11 then INTERVAL_RATIOS[:major_seventh]
82
+ end
83
+
84
+ # Apply octave adjustments
85
+ base_ratio * (2**octaves)
86
+ end
87
+ end
@@ -0,0 +1,91 @@
1
+ # A module for music rudiments
2
+ module HeadMusic::Rudiment; end
3
+
4
+ # Pythagorean tuning system based on stacking perfect fifths (3:2 ratio)
5
+ class HeadMusic::Rudiment::Tuning::Pythagorean < HeadMusic::Rudiment::Tuning
6
+ # Frequency ratios for intervals in Pythagorean tuning (relative to tonic)
7
+ # Generated by stacking perfect fifths and reducing to within one octave
8
+ INTERVAL_RATIOS = {
9
+ unison: Rational(1, 1),
10
+ minor_second: Rational(256, 243), # Pythagorean minor second
11
+ major_second: Rational(9, 8), # Pythagorean major second
12
+ minor_third: Rational(32, 27), # Pythagorean minor third
13
+ major_third: Rational(81, 64), # Pythagorean major third (ditone)
14
+ perfect_fourth: Rational(4, 3), # Perfect fourth
15
+ tritone: Rational(729, 512), # Pythagorean tritone (augmented fourth)
16
+ perfect_fifth: Rational(3, 2), # Perfect fifth
17
+ minor_sixth: Rational(128, 81), # Pythagorean minor sixth
18
+ major_sixth: Rational(27, 16), # Pythagorean major sixth
19
+ minor_seventh: Rational(16, 9), # Pythagorean minor seventh
20
+ major_seventh: Rational(243, 128), # Pythagorean major seventh
21
+ octave: Rational(2, 1) # Octave
22
+ }.freeze
23
+
24
+ # Additional chromatic intervals for enharmonic equivalents
25
+ CHROMATIC_RATIOS = {
26
+ augmented_unison: Rational(2187, 2048), # Pythagorean augmented unison (sharp)
27
+ diminished_second: Rational(256, 243) # Same as minor second in Pythagorean
28
+ }.freeze
29
+
30
+ attr_reader :tonal_center
31
+
32
+ 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
51
+ end
52
+
53
+ private
54
+
55
+ def calculate_tonal_center_frequency
56
+ # Use equal temperament to get the tonal center frequency from the reference pitch
57
+ interval_to_tonal_center = (tonal_center - reference_pitch.pitch).semitones
58
+ reference_pitch_frequency * (2**(interval_to_tonal_center / 12.0))
59
+ end
60
+
61
+ def ratio_for_interval(semitones)
62
+ # Handle octaves
63
+ octaves = semitones / 12
64
+ interval_within_octave = semitones % 12
65
+
66
+ # Make sure we handle negative intervals
67
+ if interval_within_octave < 0
68
+ interval_within_octave += 12
69
+ octaves -= 1
70
+ end
71
+
72
+ # Get the base ratio
73
+ base_ratio = case interval_within_octave
74
+ when 0 then INTERVAL_RATIOS[:unison]
75
+ when 1 then INTERVAL_RATIOS[:minor_second]
76
+ when 2 then INTERVAL_RATIOS[:major_second]
77
+ when 3 then INTERVAL_RATIOS[:minor_third]
78
+ when 4 then INTERVAL_RATIOS[:major_third]
79
+ when 5 then INTERVAL_RATIOS[:perfect_fourth]
80
+ when 6 then INTERVAL_RATIOS[:tritone]
81
+ when 7 then INTERVAL_RATIOS[:perfect_fifth]
82
+ when 8 then INTERVAL_RATIOS[:minor_sixth]
83
+ when 9 then INTERVAL_RATIOS[:major_sixth]
84
+ when 10 then INTERVAL_RATIOS[:minor_seventh]
85
+ when 11 then INTERVAL_RATIOS[:major_seventh]
86
+ end
87
+
88
+ # Apply octave adjustments
89
+ base_ratio * (2**octaves)
90
+ end
91
+ end
@@ -8,8 +8,24 @@ class HeadMusic::Rudiment::Tuning
8
8
 
9
9
  delegate :pitch, :frequency, to: :reference_pitch, prefix: true
10
10
 
11
- def initialize(reference_pitch: :a440)
11
+ def self.get(tuning_type = :equal_temperament, **options)
12
+ case tuning_type.to_s.downcase
13
+ when "just_intonation", "just", "ji"
14
+ HeadMusic::Rudiment::Tuning::JustIntonation.new(**options)
15
+ when "pythagorean", "pythag"
16
+ HeadMusic::Rudiment::Tuning::Pythagorean.new(**options)
17
+ when "meantone", "quarter_comma_meantone", "1/4_comma"
18
+ HeadMusic::Rudiment::Tuning::Meantone.new(**options)
19
+ when "equal_temperament", "equal", "et", "12tet"
20
+ new(**options)
21
+ else
22
+ new(**options)
23
+ end
24
+ end
25
+
26
+ def initialize(reference_pitch: :a440, tonal_center: nil)
12
27
  @reference_pitch = HeadMusic::Rudiment::ReferencePitch.get(reference_pitch)
28
+ @tonal_center = tonal_center
13
29
  end
14
30
 
15
31
  def frequency_for(pitch)
@@ -17,5 +33,3 @@ class HeadMusic::Rudiment::Tuning
17
33
  reference_pitch_frequency * (2**(1.0 / 12))**(pitch - reference_pitch.pitch).semitones
18
34
  end
19
35
  end
20
-
21
- # TODO: other tunings
@@ -35,8 +35,8 @@ class HeadMusic::Style::Annotation
35
35
  fitness == 1
36
36
  end
37
37
 
38
- def notes?
39
- first_note
38
+ def has_notes?
39
+ !!first_note
40
40
  end
41
41
 
42
42
  def start_position
@@ -51,16 +51,16 @@ class HeadMusic::Style::Annotation
51
51
  self.class::MESSAGE
52
52
  end
53
53
 
54
- protected
55
-
56
54
  def first_note
57
- notes&.first
55
+ notes.first
58
56
  end
59
57
 
60
58
  def last_note
61
- notes&.last
59
+ notes.last
62
60
  end
63
61
 
62
+ protected
63
+
64
64
  def voices
65
65
  @voices ||= voice.composition.voices
66
66
  end
@@ -16,12 +16,12 @@ class HeadMusic::Style::Guidelines::ConsonantClimax < HeadMusic::Style::Annotati
16
16
  end
17
17
 
18
18
  def adherent_high_pitch?
19
- notes? && highest_pitch_consonant_with_tonic? &&
19
+ has_notes? && highest_pitch_consonant_with_tonic? &&
20
20
  (highest_pitch_appears_once? || highest_pitch_appears_twice_with_step_between?)
21
21
  end
22
22
 
23
23
  def adherent_low_pitch?
24
- notes? &&
24
+ has_notes? &&
25
25
  lowest_pitch_consonant_with_tonic? &&
26
26
  (lowest_pitch_appears_once? || lowest_pitch_appears_twice_with_step_between?)
27
27
  end
@@ -9,6 +9,22 @@ class HeadMusic::Style::Guidelines::NotesSameLength < HeadMusic::Style::Annotati
9
9
  HeadMusic::Style::Mark.for_each(all_wrong_length_notes)
10
10
  end
11
11
 
12
+ def first_most_common_rhythmic_value
13
+ @first_most_common_rhythmic_value ||= begin
14
+ candidates = most_common_rhythmic_values
15
+ first_match = notes.detect { |note| candidates.include?(note.rhythmic_value) }
16
+ first_match&.rhythmic_value
17
+ end
18
+ end
19
+
20
+ def most_common_rhythmic_values
21
+ return [] if notes.empty?
22
+
23
+ occurrences = occurrences_by_rhythmic_value
24
+ highest_count = occurrences.values.max
25
+ occurrences.select { |_rhythmic_value, count| count == highest_count }.keys
26
+ end
27
+
12
28
  private
13
29
 
14
30
  def all_wrong_length_notes
@@ -37,22 +53,6 @@ class HeadMusic::Style::Guidelines::NotesSameLength < HeadMusic::Style::Annotati
37
53
  notes[0..-2]
38
54
  end
39
55
 
40
- def first_most_common_rhythmic_value
41
- @first_most_common_rhythmic_value ||= begin
42
- candidates = most_common_rhythmic_values
43
- first_match = notes.detect { |note| candidates.include?(note.rhythmic_value) }
44
- first_match&.rhythmic_value
45
- end
46
- end
47
-
48
- def most_common_rhythmic_values
49
- return [] if notes.empty?
50
-
51
- occurrences = occurrences_by_rhythmic_value
52
- highest_count = occurrences.values.max
53
- occurrences.select { |_rhythmic_value, count| count == highest_count }.keys
54
- end
55
-
56
56
  def occurrences_by_rhythmic_value
57
57
  rhythmic_values.each_with_object(Hash.new(0)) { |value, hash| hash[value] += 1 }
58
58
  end
@@ -1,3 +1,3 @@
1
1
  module HeadMusic
2
- VERSION = "8.2.0"
2
+ VERSION = "8.3.0"
3
3
  end
data/lib/head_music.rb CHANGED
@@ -56,6 +56,9 @@ require "head_music/rudiment/scale_type"
56
56
  require "head_music/rudiment/solmization"
57
57
  require "head_music/rudiment/spelling"
58
58
  require "head_music/rudiment/tuning"
59
+ require "head_music/rudiment/tuning/just_intonation"
60
+ require "head_music/rudiment/tuning/pythagorean"
61
+ require "head_music/rudiment/tuning/meantone"
59
62
 
60
63
  # instruments
61
64
  require "head_music/instruments/instrument_family"
@@ -0,0 +1,38 @@
1
+ # Band Score Order
2
+
3
+ As a band director or arranger
4
+
5
+ I want to organize instruments in band score order
6
+
7
+ So that my scores follow standard concert band conventions
8
+
9
+ ## Scenario: Display instruments in band order
10
+
11
+ Given I have a composition for concert band
12
+
13
+ When I request the score order for a band arrangement
14
+
15
+ Then the instruments should be ordered as follows:
16
+ - Flutes
17
+ - Oboes
18
+ - Bassoons
19
+ - Clarinets
20
+ - Saxophones
21
+ - Cornets
22
+ - Trumpets
23
+ - Horns
24
+ - Trombones
25
+ - Euphoniums
26
+ - Tubas
27
+ - Timpani
28
+ - Percussion
29
+
30
+ ## Scenario: Recognize different percussion placement
31
+
32
+ Given I am working with both orchestral and band scores
33
+
34
+ When I compare the score orders
35
+
36
+ Then I should note that percussion placement differs:
37
+ - In orchestral scores: percussion appears after brass
38
+ - In band scores: percussion appears at the bottom