head_music 8.2.1 → 9.0.1

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 (99) 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 +53 -0
  5. data/CLAUDE.md +151 -0
  6. data/Gemfile.lock +25 -25
  7. data/MUSIC_THEORY.md +120 -0
  8. data/Rakefile +2 -2
  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 +50 -27
  14. data/lib/head_music/analysis/interval_consonance.rb +51 -0
  15. data/lib/head_music/content/note.rb +1 -1
  16. data/lib/head_music/content/placement.rb +1 -1
  17. data/lib/head_music/content/position.rb +1 -1
  18. data/lib/head_music/content/staff.rb +1 -1
  19. data/lib/head_music/instruments/instrument.rb +103 -113
  20. data/lib/head_music/instruments/instrument_families.yml +10 -9
  21. data/lib/head_music/instruments/instrument_family.rb +13 -2
  22. data/lib/head_music/instruments/instrument_type.rb +188 -0
  23. data/lib/head_music/instruments/instruments.yml +350 -368
  24. data/lib/head_music/instruments/score_order.rb +139 -0
  25. data/lib/head_music/instruments/score_orders.yml +130 -0
  26. data/lib/head_music/instruments/variant.rb +6 -0
  27. data/lib/head_music/locales/de.yml +6 -0
  28. data/lib/head_music/locales/en.yml +98 -0
  29. data/lib/head_music/locales/es.yml +6 -0
  30. data/lib/head_music/locales/fr.yml +6 -0
  31. data/lib/head_music/locales/it.yml +6 -0
  32. data/lib/head_music/locales/ru.yml +6 -0
  33. data/lib/head_music/rudiment/alteration.rb +23 -8
  34. data/lib/head_music/rudiment/base.rb +9 -0
  35. data/lib/head_music/rudiment/chromatic_interval.rb +3 -6
  36. data/lib/head_music/rudiment/clef.rb +1 -1
  37. data/lib/head_music/rudiment/consonance.rb +37 -4
  38. data/lib/head_music/rudiment/diatonic_context.rb +25 -0
  39. data/lib/head_music/rudiment/key.rb +77 -0
  40. data/lib/head_music/rudiment/key_signature/enharmonic_equivalence.rb +1 -1
  41. data/lib/head_music/rudiment/key_signature.rb +46 -7
  42. data/lib/head_music/rudiment/letter_name.rb +3 -3
  43. data/lib/head_music/rudiment/meter.rb +19 -9
  44. data/lib/head_music/rudiment/mode.rb +92 -0
  45. data/lib/head_music/rudiment/musical_symbol.rb +1 -1
  46. data/lib/head_music/rudiment/note.rb +112 -0
  47. data/lib/head_music/rudiment/pitch/parser.rb +52 -0
  48. data/lib/head_music/rudiment/pitch.rb +5 -6
  49. data/lib/head_music/rudiment/pitch_class.rb +1 -1
  50. data/lib/head_music/rudiment/quality.rb +1 -1
  51. data/lib/head_music/rudiment/reference_pitch.rb +1 -1
  52. data/lib/head_music/rudiment/register.rb +4 -1
  53. data/lib/head_music/rudiment/rest.rb +36 -0
  54. data/lib/head_music/rudiment/rhythmic_element.rb +53 -0
  55. data/lib/head_music/rudiment/rhythmic_unit/parser.rb +86 -0
  56. data/lib/head_music/rudiment/rhythmic_unit.rb +104 -29
  57. data/lib/head_music/rudiment/rhythmic_units.yml +80 -0
  58. data/lib/head_music/rudiment/rhythmic_value/parser.rb +77 -0
  59. data/lib/head_music/{content → rudiment}/rhythmic_value.rb +23 -5
  60. data/lib/head_music/rudiment/scale.rb +4 -5
  61. data/lib/head_music/rudiment/scale_degree.rb +9 -4
  62. data/lib/head_music/rudiment/scale_type.rb +9 -3
  63. data/lib/head_music/rudiment/solmization.rb +1 -1
  64. data/lib/head_music/rudiment/spelling.rb +5 -4
  65. data/lib/head_music/rudiment/tempo.rb +85 -0
  66. data/lib/head_music/rudiment/tonal_context.rb +35 -0
  67. data/lib/head_music/rudiment/tuning/just_intonation.rb +85 -0
  68. data/lib/head_music/rudiment/tuning/meantone.rb +87 -0
  69. data/lib/head_music/rudiment/tuning/pythagorean.rb +91 -0
  70. data/lib/head_music/rudiment/tuning.rb +18 -4
  71. data/lib/head_music/rudiment/unpitched_note.rb +62 -0
  72. data/lib/head_music/style/annotation.rb +4 -4
  73. data/lib/head_music/style/guidelines/notes_same_length.rb +16 -16
  74. data/lib/head_music/style/medieval_tradition.rb +26 -0
  75. data/lib/head_music/style/modern_tradition.rb +34 -0
  76. data/lib/head_music/style/renaissance_tradition.rb +26 -0
  77. data/lib/head_music/style/tradition.rb +21 -0
  78. data/lib/head_music/utilities/hash_key.rb +34 -2
  79. data/lib/head_music/version.rb +1 -1
  80. data/lib/head_music.rb +33 -9
  81. data/user_stories/active/handle-time.md +7 -0
  82. data/user_stories/active/handle-time.rb +177 -0
  83. data/user_stories/done/epic--score-order/PLAN.md +244 -0
  84. data/user_stories/done/epic--score-order/band-score-order.md +38 -0
  85. data/user_stories/done/epic--score-order/chamber-ensemble-score-order.md +33 -0
  86. data/user_stories/done/epic--score-order/orchestral-score-order.md +43 -0
  87. data/user_stories/done/instrument-variant.md +65 -0
  88. data/user_stories/done/superclass-for-note.md +30 -0
  89. data/user_stories/todo/agentic-daw.md +3 -0
  90. data/user_stories/todo/consonance-dissonance-classification.md +57 -0
  91. data/user_stories/todo/dyad-analysis.md +57 -0
  92. data/user_stories/todo/material-and-scores.md +10 -0
  93. data/user_stories/todo/organizing-content.md +72 -0
  94. data/user_stories/todo/percussion_set.md +1 -0
  95. data/user_stories/todo/pitch-class-set-analysis.md +79 -0
  96. data/user_stories/todo/pitch-set-classification.md +72 -0
  97. data/user_stories/todo/sonority-identification.md +67 -0
  98. metadata +51 -6
  99. data/TODO.md +0 -218
@@ -0,0 +1,85 @@
1
+ module HeadMusic::Rudiment; end
2
+
3
+ # Represents a musical tempo with a beat value and beats per minute
4
+ class HeadMusic::Rudiment::Tempo
5
+ SECONDS_PER_MINUTE = 60
6
+ NANOSECONDS_PER_SECOND = 1_000_000_000
7
+ NANOSECONDS_PER_MINUTE = (NANOSECONDS_PER_SECOND * SECONDS_PER_MINUTE).freeze
8
+
9
+ attr_reader :beat_value, :beats_per_minute
10
+
11
+ delegate :ticks, to: :beat_value, prefix: true
12
+ alias_method :ticks_per_beat, :beat_value_ticks
13
+
14
+ NAMED_TEMPO_DEFAULTS = {
15
+ larghissimo: ["quarter", 24], # 24–40 bpm
16
+ adagissimo: ["quarter", 32], # 24–40 bpm
17
+ grave: ["quarter", 32], # 24–40 bpm
18
+ largo: ["quarter", 54], # 40–66 bpm
19
+ larghetto: ["quarter", 54], # 44–66 bpm
20
+ adagio: ["quarter", 60], # 44–66 bpm
21
+ adagietto: ["quarter", 68], # 46–80 bpm
22
+ lento: ["quarter", 72], # 52–108 bpm
23
+ marcia_moderato: ["quarter", 72], # 66–80 bpm
24
+ andante: ["quarter", 78], # 56–108 bpm
25
+ andante_moderato: ["quarter", 88], # 80–108 bpm
26
+ andantino: ["quarter", 92], # 80–108 bpm
27
+ moderato: ["quarter", 108], # 108–120 bpm
28
+ allegretto: ["quarter", 112], # 112–120 bpm
29
+ allegro_moderato: ["quarter", 116], # 116–120 bpm
30
+ allegro: ["quarter", 120], # 120–156 bpm
31
+ molto_allegro: ["quarter", 132], # 124–156 bpm
32
+ allegro_vivace: ["quarter", 132], # 124–156 bpm
33
+ vivace: ["quarter", 156], # 156–176 bpm
34
+ vivacissimo: ["quarter", 172], # 172–176 bpm
35
+ allegrissimo: ["quarter", 172], # 172–176 bpm
36
+ presto: ["quarter", 180], # 168–200 bpm
37
+ prestissimo: ["quarter", 200] # 200 bpm and over
38
+ }
39
+
40
+ def self.get(identifier)
41
+ @tempos ||= {}
42
+ key = HeadMusic::Utilities::HashKey.for(identifier)
43
+ if NAMED_TEMPO_DEFAULTS.key?(identifier.to_s.to_sym)
44
+ beat_value, beats_per_minute = NAMED_TEMPO_DEFAULTS[identifier.to_s.to_sym]
45
+ @tempos[key] ||= new(beat_value, beats_per_minute)
46
+ elsif identifier.to_s.match?(/=|at/)
47
+ parts = identifier.to_s.split(/\s*(=|at)\s*/)
48
+ unit = parts[0]
49
+ bpm = parts[2] || parts[1] # Handle both "q = 120" and "q at 120bpm"
50
+ bpm_value = bpm.to_s.gsub(/[^0-9]/, "").to_i
51
+ @tempos[key] ||= new(standardized_unit(unit), bpm_value)
52
+ else
53
+ @tempos[key] ||= new("quarter", 120)
54
+ end
55
+ @tempos[key]
56
+ end
57
+
58
+ def initialize(beat_value, beats_per_minute)
59
+ @beat_value = HeadMusic::Rudiment::RhythmicValue.get(beat_value)
60
+ @beats_per_minute = beats_per_minute.to_f
61
+ end
62
+
63
+ def beat_duration_in_seconds
64
+ @beat_duration_in_seconds ||=
65
+ SECONDS_PER_MINUTE / beats_per_minute
66
+ end
67
+
68
+ def beat_duration_in_nanoseconds
69
+ @beat_duration_in_nanoseconds ||=
70
+ NANOSECONDS_PER_MINUTE / beats_per_minute
71
+ end
72
+
73
+ def tick_duration_in_nanoseconds
74
+ @tick_duration_in_nanoseconds ||=
75
+ beat_duration_in_nanoseconds / ticks_per_beat
76
+ end
77
+
78
+ def self.standardized_unit(unit)
79
+ return "quarter" if unit.nil?
80
+
81
+ # Use RhythmicValue parser to handle all formats (shorthand, fractions, British names, dots, etc.)
82
+ rhythmic_value = HeadMusic::Rudiment::RhythmicValue.get(unit)
83
+ rhythmic_value&.unit ? rhythmic_value.to_s : "quarter"
84
+ end
85
+ end
@@ -0,0 +1,35 @@
1
+ # A module for music rudiments
2
+ module HeadMusic::Rudiment; end
3
+
4
+ # Abstract base class representing a tonal context (system of pitches with a tonal center)
5
+ class HeadMusic::Rudiment::TonalContext < HeadMusic::Rudiment::Base
6
+ attr_reader :tonic_spelling
7
+
8
+ def initialize(tonic_spelling)
9
+ @tonic_spelling = HeadMusic::Rudiment::Spelling.get(tonic_spelling)
10
+ end
11
+
12
+ def tonic_pitch(octave = 4)
13
+ HeadMusic::Rudiment::Pitch.get("#{tonic_spelling}#{octave}")
14
+ end
15
+
16
+ def scale
17
+ raise NotImplementedError, "Subclasses must implement #scale"
18
+ end
19
+
20
+ def pitches(octave = nil)
21
+ scale.pitches(direction: :ascending, octaves: 1)
22
+ end
23
+
24
+ def pitch_classes
25
+ scale.pitch_classes
26
+ end
27
+
28
+ def spellings
29
+ scale.spellings
30
+ end
31
+
32
+ def key_signature
33
+ raise NotImplementedError, "Subclasses must implement #key_signature"
34
+ end
35
+ end
@@ -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
@@ -3,13 +3,29 @@ module HeadMusic::Rudiment; end
3
3
 
4
4
  # A tuning has a reference pitch and frequency and provides frequencies for all pitches
5
5
  # The base class assumes equal temperament tuning. By default, A4 = 440.0 Hz
6
- class HeadMusic::Rudiment::Tuning
6
+ class HeadMusic::Rudiment::Tuning < HeadMusic::Rudiment::Base
7
7
  attr_accessor :reference_pitch
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
@@ -0,0 +1,62 @@
1
+ # A module for music rudiments
2
+ module HeadMusic::Rudiment; end
3
+
4
+ # An UnpitchedNote represents a percussion note with rhythm but no specific pitch.
5
+ # It inherits from RhythmicElement and can optionally have an instrument name.
6
+ class HeadMusic::Rudiment::UnpitchedNote < HeadMusic::Rudiment::RhythmicElement
7
+ include HeadMusic::Named
8
+
9
+ attr_reader :instrument_name
10
+
11
+ # Make new public for this concrete class
12
+ public_class_method :new
13
+
14
+ def self.get(rhythmic_value, instrument: nil)
15
+ return rhythmic_value if rhythmic_value.is_a?(HeadMusic::Rudiment::UnpitchedNote)
16
+
17
+ rhythmic_value = HeadMusic::Rudiment::RhythmicValue.get(rhythmic_value)
18
+ return nil unless rhythmic_value
19
+
20
+ fetch_or_create(rhythmic_value, instrument)
21
+ end
22
+
23
+ def self.fetch_or_create(rhythmic_value, instrument_name)
24
+ @unpitched_notes ||= {}
25
+ hash_key = [rhythmic_value.to_s, instrument_name].compact.join("_")
26
+ @unpitched_notes[hash_key] ||= new(rhythmic_value, instrument_name)
27
+ end
28
+
29
+ def initialize(rhythmic_value, instrument_name = nil)
30
+ super(rhythmic_value)
31
+ @instrument_name = instrument_name
32
+ end
33
+
34
+ def name
35
+ if instrument_name
36
+ "#{rhythmic_value} #{instrument_name}"
37
+ else
38
+ "#{rhythmic_value} unpitched note"
39
+ end
40
+ end
41
+
42
+ # Override with_rhythmic_value to preserve instrument
43
+ def with_rhythmic_value(new_rhythmic_value)
44
+ self.class.get(new_rhythmic_value, instrument: instrument_name)
45
+ end
46
+
47
+ # Create a new unpitched note with a different instrument
48
+ def with_instrument(new_instrument_name)
49
+ self.class.get(rhythmic_value, instrument: new_instrument_name)
50
+ end
51
+
52
+ def ==(other)
53
+ return false unless other.is_a?(self.class)
54
+ rhythmic_value == other.rhythmic_value && instrument_name == other.instrument_name
55
+ end
56
+
57
+ def sounded?
58
+ true
59
+ end
60
+
61
+ private_class_method :fetch_or_create
62
+ end
@@ -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
@@ -0,0 +1,26 @@
1
+ # Medieval tradition for interval consonance classification
2
+ class HeadMusic::Style::MedievalTradition < HeadMusic::Style::Tradition
3
+ def consonance_classification(interval)
4
+ interval_mod = interval.simple_semitones
5
+
6
+ # Check for augmented or diminished intervals
7
+ if interval.augmented? || interval.diminished?
8
+ return HeadMusic::Rudiment::Consonance::DISSONANCE
9
+ end
10
+
11
+ case interval_mod
12
+ when 0, 12 # Unison, Octave
13
+ HeadMusic::Rudiment::Consonance::PERFECT_CONSONANCE
14
+ when 7 # Perfect Fifth
15
+ HeadMusic::Rudiment::Consonance::PERFECT_CONSONANCE
16
+ when 5 # Perfect Fourth - consonant in medieval music
17
+ HeadMusic::Rudiment::Consonance::PERFECT_CONSONANCE
18
+ when 3, 4 # Minor Third, Major Third
19
+ HeadMusic::Rudiment::Consonance::IMPERFECT_CONSONANCE
20
+ when 8, 9 # Minor Sixth, Major Sixth
21
+ HeadMusic::Rudiment::Consonance::IMPERFECT_CONSONANCE
22
+ else
23
+ HeadMusic::Rudiment::Consonance::DISSONANCE
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,34 @@
1
+ # Modern/standard practice tradition for interval consonance classification
2
+ class HeadMusic::Style::ModernTradition < HeadMusic::Style::Tradition
3
+ def consonance_classification(interval)
4
+ interval_mod = interval.simple_semitones
5
+
6
+ # Check for augmented or diminished intervals (except diminished fifth/augmented fourth)
7
+ if (interval.augmented? || interval.diminished?) && interval_mod != 6
8
+ return HeadMusic::Rudiment::Consonance::DISSONANCE
9
+ end
10
+
11
+ case interval_mod
12
+ when 0, 12 # Unison, Octave
13
+ HeadMusic::Rudiment::Consonance::PERFECT_CONSONANCE
14
+ when 7 # Perfect Fifth
15
+ HeadMusic::Rudiment::Consonance::PERFECT_CONSONANCE
16
+ when 3, 4 # Minor Third, Major Third
17
+ HeadMusic::Rudiment::Consonance::IMPERFECT_CONSONANCE
18
+ when 8, 9 # Minor Sixth, Major Sixth
19
+ HeadMusic::Rudiment::Consonance::IMPERFECT_CONSONANCE
20
+ when 5 # Perfect Fourth
21
+ # In standard practice, perfect fourth is considered consonant
22
+ # but contextual would be more accurate
23
+ HeadMusic::Rudiment::Consonance::PERFECT_CONSONANCE
24
+ when 2, 10 # Major Second, Minor Seventh
25
+ HeadMusic::Rudiment::Consonance::MILD_DISSONANCE
26
+ when 1, 11 # Minor Second, Major Seventh
27
+ HeadMusic::Rudiment::Consonance::HARSH_DISSONANCE
28
+ when 6 # Tritone (Aug 4th/Dim 5th)
29
+ HeadMusic::Rudiment::Consonance::DISSONANCE
30
+ else
31
+ HeadMusic::Rudiment::Consonance::DISSONANCE
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,26 @@
1
+ # Renaissance counterpoint tradition for interval consonance classification
2
+ class HeadMusic::Style::RenaissanceTradition < HeadMusic::Style::Tradition
3
+ def consonance_classification(interval)
4
+ interval_mod = interval.simple_semitones
5
+
6
+ # Check for augmented or diminished intervals
7
+ if interval.augmented? || interval.diminished?
8
+ return HeadMusic::Rudiment::Consonance::DISSONANCE
9
+ end
10
+
11
+ case interval_mod
12
+ when 0, 12 # Unison, Octave
13
+ HeadMusic::Rudiment::Consonance::PERFECT_CONSONANCE
14
+ when 7 # Perfect Fifth
15
+ HeadMusic::Rudiment::Consonance::PERFECT_CONSONANCE
16
+ when 3, 4 # Minor Third, Major Third
17
+ HeadMusic::Rudiment::Consonance::IMPERFECT_CONSONANCE
18
+ when 8, 9 # Minor Sixth, Major Sixth
19
+ HeadMusic::Rudiment::Consonance::IMPERFECT_CONSONANCE
20
+ when 5 # Perfect Fourth - dissonant in Renaissance counterpoint
21
+ HeadMusic::Rudiment::Consonance::DISSONANCE
22
+ else
23
+ HeadMusic::Rudiment::Consonance::DISSONANCE
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,21 @@
1
+ # A style tradition represents a historical or theoretical approach to music
2
+ module HeadMusic::Style
3
+ class Tradition
4
+ def self.get(name)
5
+ case name&.to_sym
6
+ when :modern, :standard_practice then ModernTradition.new
7
+ when :renaissance_counterpoint, :two_part_harmony then RenaissanceTradition.new
8
+ when :medieval then MedievalTradition.new
9
+ else ModernTradition.new
10
+ end
11
+ end
12
+
13
+ def consonance_classification(interval)
14
+ raise NotImplementedError, "#{self.class} must implement consonance_classification"
15
+ end
16
+
17
+ def name
18
+ self.class.name.split("::").last.sub(/Tradition$/, "").downcase.gsub(" ", "_").to_sym
19
+ end
20
+ end
21
+ end