head_music 8.2.1 → 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 (34) 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/CLAUDE.md +134 -0
  5. data/Gemfile.lock +25 -25
  6. data/Rakefile +2 -2
  7. data/TODO.md +41 -150
  8. data/bin/check_instrument_consistency.rb +86 -0
  9. data/check_instrument_consistency.rb +0 -0
  10. data/head_music.gemspec +1 -1
  11. data/lib/head_music/analysis/diatonic_interval/naming.rb +1 -1
  12. data/lib/head_music/analysis/diatonic_interval.rb +28 -7
  13. data/lib/head_music/instruments/instrument_families.yml +10 -9
  14. data/lib/head_music/instruments/instruments.yml +350 -368
  15. data/lib/head_music/locales/en.yml +92 -0
  16. data/lib/head_music/rudiment/rhythmic_unit.rb +93 -26
  17. data/lib/head_music/rudiment/scale_degree.rb +8 -3
  18. data/lib/head_music/rudiment/tuning/just_intonation.rb +85 -0
  19. data/lib/head_music/rudiment/tuning/meantone.rb +87 -0
  20. data/lib/head_music/rudiment/tuning/pythagorean.rb +91 -0
  21. data/lib/head_music/rudiment/tuning.rb +17 -3
  22. data/lib/head_music/style/annotation.rb +4 -4
  23. data/lib/head_music/style/guidelines/notes_same_length.rb +16 -16
  24. data/lib/head_music/version.rb +1 -1
  25. data/lib/head_music.rb +3 -0
  26. data/user_stories/backlog/band-score-order.md +38 -0
  27. data/user_stories/backlog/chamber-ensemble-score-order.md +33 -0
  28. data/user_stories/backlog/consonance-dissonance-classification.md +57 -0
  29. data/user_stories/backlog/dyad-analysis.md +65 -0
  30. data/user_stories/backlog/orchestral-score-order.md +43 -0
  31. data/user_stories/backlog/pitch-class-set-analysis.md +39 -0
  32. data/user_stories/backlog/pitch-set-classification.md +62 -0
  33. data/user_stories/backlog/sonority-identification.md +47 -0
  34. metadata +18 -4
@@ -14,6 +14,98 @@ en:
14
14
  minor_seventh: minor seventh
15
15
  major_seventh: major seventh
16
16
  perfect_octave: perfect octave
17
+ diatonic_intervals:
18
+ perfect_unison: perfect unison
19
+ minor_second: minor second
20
+ major_second: major second
21
+ diminished_second: diminished second
22
+ augmented_second: augmented second
23
+ doubly_diminished_second: doubly diminished second
24
+ doubly_augmented_second: doubly augmented second
25
+ minor_third: minor third
26
+ major_third: major third
27
+ diminished_third: diminished third
28
+ augmented_third: augmented third
29
+ doubly_diminished_third: doubly diminished third
30
+ doubly_augmented_third: doubly augmented third
31
+ perfect_fourth: perfect fourth
32
+ diminished_fourth: diminished fourth
33
+ augmented_fourth: augmented fourth
34
+ doubly_diminished_fourth: doubly diminished fourth
35
+ doubly_augmented_fourth: doubly augmented fourth
36
+ perfect_fifth: perfect fifth
37
+ diminished_fifth: diminished fifth
38
+ augmented_fifth: augmented fifth
39
+ doubly_diminished_fifth: doubly diminished fifth
40
+ doubly_augmented_fifth: doubly augmented fifth
41
+ minor_sixth: minor sixth
42
+ major_sixth: major sixth
43
+ diminished_sixth: diminished sixth
44
+ augmented_sixth: augmented sixth
45
+ doubly_diminished_sixth: doubly diminished sixth
46
+ doubly_augmented_sixth: doubly augmented sixth
47
+ minor_seventh: minor seventh
48
+ major_seventh: major seventh
49
+ diminished_seventh: diminished seventh
50
+ augmented_seventh: augmented seventh
51
+ doubly_diminished_seventh: doubly diminished seventh
52
+ doubly_augmented_seventh: doubly augmented seventh
53
+ perfect_octave: perfect octave
54
+ diminished_octave: diminished octave
55
+ augmented_octave: augmented octave
56
+ doubly_diminished_octave: doubly diminished octave
57
+ doubly_augmented_octave: doubly augmented octave
58
+ minor_ninth: minor ninth
59
+ major_ninth: major ninth
60
+ diminished_ninth: diminished ninth
61
+ augmented_ninth: augmented ninth
62
+ doubly_diminished_ninth: doubly diminished ninth
63
+ doubly_augmented_ninth: doubly augmented ninth
64
+ minor_tenth: minor tenth
65
+ major_tenth: major tenth
66
+ diminished_tenth: diminished tenth
67
+ augmented_tenth: augmented tenth
68
+ doubly_diminished_tenth: doubly diminished tenth
69
+ doubly_augmented_tenth: doubly augmented tenth
70
+ perfect_eleventh: perfect eleventh
71
+ diminished_eleventh: diminished eleventh
72
+ augmented_eleventh: augmented eleventh
73
+ doubly_diminished_eleventh: doubly diminished eleventh
74
+ doubly_augmented_eleventh: doubly augmented eleventh
75
+ perfect_twelfth: perfect twelfth
76
+ diminished_twelfth: diminished twelfth
77
+ augmented_twelfth: augmented twelfth
78
+ doubly_diminished_twelfth: doubly diminished twelfth
79
+ doubly_augmented_twelfth: doubly augmented twelfth
80
+ minor_thirteenth: minor thirteenth
81
+ major_thirteenth: major thirteenth
82
+ diminished_thirteenth: diminished thirteenth
83
+ augmented_thirteenth: augmented thirteenth
84
+ doubly_diminished_thirteenth: doubly diminished thirteenth
85
+ doubly_augmented_thirteenth: doubly augmented thirteenth
86
+ minor_fourteenth: minor fourteenth
87
+ major_fourteenth: major fourteenth
88
+ diminished_fourteenth: diminished fourteenth
89
+ augmented_fourteenth: augmented fourteenth
90
+ doubly_diminished_fourteenth: doubly diminished fourteenth
91
+ doubly_augmented_fourteenth: doubly augmented fourteenth
92
+ perfect_fifteenth: perfect fifteenth
93
+ diminished_fifteenth: diminished fifteenth
94
+ augmented_fifteenth: augmented fifteenth
95
+ doubly_diminished_fifteenth: doubly diminished fifteenth
96
+ doubly_augmented_fifteenth: doubly augmented fifteenth
97
+ minor_sixteenth: minor sixteenth
98
+ major_sixteenth: major sixteenth
99
+ diminished_sixteenth: diminished sixteenth
100
+ augmented_sixteenth: augmented sixteenth
101
+ doubly_diminished_sixteenth: doubly diminished sixteenth
102
+ doubly_augmented_sixteenth: doubly augmented sixteenth
103
+ minor_seventeenth: minor seventeenth
104
+ major_seventeenth: major seventeenth
105
+ diminished_seventeenth: diminished seventeenth
106
+ augmented_seventeenth: augmented seventeenth
107
+ doubly_diminished_seventeenth: doubly diminished seventeenth
108
+ doubly_augmented_seventeenth: doubly augmented seventeenth
17
109
  clefs:
18
110
  alto_clef: alto clef
19
111
  baritone_c_clef: baritone C-clef
@@ -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
@@ -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
@@ -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.1"
2
+ VERSION = "8.3.0"
3
3
  end