head_music 8.3.0 → 11.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 (138) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +9 -3
  3. data/CHANGELOG.md +71 -0
  4. data/CLAUDE.md +62 -25
  5. data/Gemfile +7 -1
  6. data/Gemfile.lock +91 -3
  7. data/MUSIC_THEORY.md +120 -0
  8. data/README.md +18 -0
  9. data/Rakefile +7 -2
  10. data/head_music.gemspec +1 -1
  11. data/lib/head_music/analysis/diatonic_interval.rb +29 -27
  12. data/lib/head_music/analysis/dyad.rb +229 -0
  13. data/lib/head_music/analysis/interval_consonance.rb +51 -0
  14. data/lib/head_music/analysis/melodic_interval.rb +1 -1
  15. data/lib/head_music/analysis/pitch_class_set.rb +111 -14
  16. data/lib/head_music/analysis/{pitch_set.rb → pitch_collection.rb} +11 -5
  17. data/lib/head_music/analysis/sonority.rb +50 -12
  18. data/lib/head_music/content/note.rb +1 -1
  19. data/lib/head_music/content/placement.rb +1 -1
  20. data/lib/head_music/content/position.rb +1 -1
  21. data/lib/head_music/content/voice.rb +1 -1
  22. data/lib/head_music/instruments/alternate_tuning.rb +102 -0
  23. data/lib/head_music/instruments/alternate_tunings.yml +78 -0
  24. data/lib/head_music/instruments/instrument.rb +231 -72
  25. data/lib/head_music/instruments/instrument_configuration.rb +66 -0
  26. data/lib/head_music/instruments/instrument_configuration_option.rb +38 -0
  27. data/lib/head_music/instruments/instrument_configurations.yml +288 -0
  28. data/lib/head_music/instruments/instrument_families.yml +77 -0
  29. data/lib/head_music/instruments/instrument_family.rb +15 -5
  30. data/lib/head_music/instruments/instruments.yml +795 -965
  31. data/lib/head_music/instruments/playing_technique.rb +75 -0
  32. data/lib/head_music/instruments/playing_techniques.yml +826 -0
  33. data/lib/head_music/instruments/score_order.rb +136 -0
  34. data/lib/head_music/instruments/score_orders.yml +130 -0
  35. data/lib/head_music/instruments/staff.rb +61 -1
  36. data/lib/head_music/instruments/staff_scheme.rb +6 -4
  37. data/lib/head_music/instruments/stringing.rb +115 -0
  38. data/lib/head_music/instruments/stringing_course.rb +58 -0
  39. data/lib/head_music/instruments/stringings.yml +168 -0
  40. data/lib/head_music/instruments/variant.rb +6 -1
  41. data/lib/head_music/locales/de.yml +29 -0
  42. data/lib/head_music/locales/en.yml +106 -0
  43. data/lib/head_music/locales/es.yml +29 -0
  44. data/lib/head_music/locales/fr.yml +29 -0
  45. data/lib/head_music/locales/it.yml +29 -0
  46. data/lib/head_music/locales/ru.yml +29 -0
  47. data/lib/head_music/{rudiment → notation}/musical_symbol.rb +3 -3
  48. data/lib/head_music/notation/staff_mapping.rb +70 -0
  49. data/lib/head_music/notation/staff_position.rb +62 -0
  50. data/lib/head_music/notation.rb +7 -0
  51. data/lib/head_music/rudiment/alteration.rb +34 -49
  52. data/lib/head_music/rudiment/alterations.yml +32 -0
  53. data/lib/head_music/rudiment/base.rb +9 -0
  54. data/lib/head_music/rudiment/chromatic_interval.rb +4 -7
  55. data/lib/head_music/rudiment/clef.rb +2 -2
  56. data/lib/head_music/rudiment/consonance.rb +39 -5
  57. data/lib/head_music/rudiment/diatonic_context.rb +25 -0
  58. data/lib/head_music/rudiment/key.rb +77 -0
  59. data/lib/head_music/rudiment/key_signature/enharmonic_equivalence.rb +1 -1
  60. data/lib/head_music/rudiment/key_signature.rb +21 -8
  61. data/lib/head_music/rudiment/letter_name.rb +3 -3
  62. data/lib/head_music/rudiment/meter.rb +19 -9
  63. data/lib/head_music/rudiment/mode.rb +92 -0
  64. data/lib/head_music/rudiment/note.rb +112 -0
  65. data/lib/head_music/rudiment/pitch/parser.rb +52 -0
  66. data/lib/head_music/rudiment/pitch.rb +5 -6
  67. data/lib/head_music/rudiment/pitch_class.rb +1 -1
  68. data/lib/head_music/rudiment/quality.rb +1 -1
  69. data/lib/head_music/rudiment/reference_pitch.rb +1 -1
  70. data/lib/head_music/rudiment/register.rb +4 -1
  71. data/lib/head_music/rudiment/rest.rb +36 -0
  72. data/lib/head_music/rudiment/rhythmic_element.rb +53 -0
  73. data/lib/head_music/rudiment/rhythmic_unit/parser.rb +86 -0
  74. data/lib/head_music/rudiment/rhythmic_unit.rb +13 -5
  75. data/lib/head_music/rudiment/rhythmic_units.yml +80 -0
  76. data/lib/head_music/rudiment/rhythmic_value/parser.rb +77 -0
  77. data/lib/head_music/{content → rudiment}/rhythmic_value.rb +23 -5
  78. data/lib/head_music/rudiment/scale.rb +4 -5
  79. data/lib/head_music/rudiment/scale_degree.rb +1 -1
  80. data/lib/head_music/rudiment/scale_type.rb +9 -3
  81. data/lib/head_music/rudiment/solmization.rb +1 -1
  82. data/lib/head_music/rudiment/spelling.rb +8 -4
  83. data/lib/head_music/rudiment/tempo.rb +85 -0
  84. data/lib/head_music/rudiment/tonal_context.rb +35 -0
  85. data/lib/head_music/rudiment/tuning/just_intonation.rb +0 -39
  86. data/lib/head_music/rudiment/tuning/meantone.rb +0 -39
  87. data/lib/head_music/rudiment/tuning/pythagorean.rb +0 -39
  88. data/lib/head_music/rudiment/tuning.rb +21 -1
  89. data/lib/head_music/rudiment/unpitched_note.rb +62 -0
  90. data/lib/head_music/style/guidelines/consonant_climax.rb +2 -2
  91. data/lib/head_music/style/medieval_tradition.rb +26 -0
  92. data/lib/head_music/style/modern_tradition.rb +31 -0
  93. data/lib/head_music/style/renaissance_tradition.rb +26 -0
  94. data/lib/head_music/style/tradition.rb +21 -0
  95. data/lib/head_music/time/clock_position.rb +84 -0
  96. data/lib/head_music/time/conductor.rb +264 -0
  97. data/lib/head_music/time/meter_event.rb +37 -0
  98. data/lib/head_music/time/meter_map.rb +173 -0
  99. data/lib/head_music/time/musical_position.rb +188 -0
  100. data/lib/head_music/time/smpte_timecode.rb +164 -0
  101. data/lib/head_music/time/tempo_event.rb +40 -0
  102. data/lib/head_music/time/tempo_map.rb +187 -0
  103. data/lib/head_music/time.rb +32 -0
  104. data/lib/head_music/utilities/case.rb +27 -0
  105. data/lib/head_music/utilities/hash_key.rb +34 -2
  106. data/lib/head_music/version.rb +1 -1
  107. data/lib/head_music.rb +71 -22
  108. data/user_stories/active/string-pitches.md +41 -0
  109. data/user_stories/backlog/notation-style.md +183 -0
  110. data/user_stories/backlog/organizing-content.md +80 -0
  111. data/user_stories/done/consonance-dissonance-classification.md +117 -0
  112. data/user_stories/{backlog → done}/dyad-analysis.md +6 -16
  113. data/user_stories/done/epic--score-order/PLAN.md +244 -0
  114. data/user_stories/done/expand-playing-techniques.md +38 -0
  115. data/user_stories/done/handle-time.md +7 -0
  116. data/user_stories/done/handle-time.rb +163 -0
  117. data/user_stories/done/instrument-architecture.md +238 -0
  118. data/user_stories/done/instrument-variant.md +65 -0
  119. data/user_stories/done/move-musical-symbol-to-notation.md +161 -0
  120. data/user_stories/done/move-staff-mapping-to-notation.md +158 -0
  121. data/user_stories/done/move-staff-position-to-notation.md +141 -0
  122. data/user_stories/done/notation-module-foundation.md +102 -0
  123. data/user_stories/done/percussion_set.md +260 -0
  124. data/user_stories/done/sonority-identification.md +37 -0
  125. data/user_stories/done/superclass-for-note.md +30 -0
  126. data/user_stories/epics/notation-module.md +135 -0
  127. data/user_stories/visioning/agentic-daw.md +2 -0
  128. metadata +84 -18
  129. data/TODO.md +0 -109
  130. data/check_instrument_consistency.rb +0 -0
  131. data/test_translations.rb +0 -15
  132. data/user_stories/backlog/consonance-dissonance-classification.md +0 -57
  133. data/user_stories/backlog/pitch-set-classification.md +0 -62
  134. data/user_stories/backlog/sonority-identification.md +0 -47
  135. /data/user_stories/{backlog → done/epic--score-order}/band-score-order.md +0 -0
  136. /data/user_stories/{backlog → done/epic--score-order}/chamber-ensemble-score-order.md +0 -0
  137. /data/user_stories/{backlog → done/epic--score-order}/orchestral-score-order.md +0 -0
  138. /data/user_stories/{backlog → done}/pitch-class-set-analysis.md +0 -0
@@ -2,13 +2,12 @@
2
2
  module HeadMusic::Rudiment; end
3
3
 
4
4
  # A scale contains ordered pitches starting at a tonal center.
5
- class HeadMusic::Rudiment::Scale
5
+ class HeadMusic::Rudiment::Scale < HeadMusic::Rudiment::Base
6
6
  SCALE_REGEX = /^[A-G][#b]?\s+\w+$/
7
7
 
8
8
  def self.get(root_pitch, scale_type = nil)
9
- root_pitch, scale_type = root_pitch.split(/\s+/) if root_pitch.is_a?(String) && scale_type =~ SCALE_REGEX
10
9
  root_pitch = HeadMusic::Rudiment::Pitch.get(root_pitch)
11
- scale_type = HeadMusic::Rudiment::ScaleType.get(scale_type || :major)
10
+ scale_type = HeadMusic::Rudiment::ScaleType.get(scale_type || HeadMusic::Rudiment::ScaleType::DEFAULT)
12
11
  @scales ||= {}
13
12
  hash_key = HeadMusic::Utilities::HashKey.for(
14
13
  [root_pitch, scale_type].join(" ").gsub(/#|♯/, "sharp").gsub(/(\w)[b♭]/, '\\1flat')
@@ -36,7 +35,7 @@ class HeadMusic::Rudiment::Scale
36
35
  end
37
36
 
38
37
  def spellings(direction: :ascending, octaves: 1)
39
- pitches(direction: direction, octaves: octaves).map(&:spelling).map(&:to_s)
38
+ pitches(direction: direction, octaves: octaves).map(&:spelling)
40
39
  end
41
40
 
42
41
  def pitch_names(direction: :ascending, octaves: 1)
@@ -83,7 +82,7 @@ class HeadMusic::Rudiment::Scale
83
82
  end
84
83
 
85
84
  def parent_scale_pitches
86
- HeadMusic::Rudiment::Scale.get(root_pitch, scale_type.parent_name).pitches if scale_type.parent
85
+ HeadMusic::Rudiment::Scale.get(root_pitch, scale_type.parent_name).pitches
87
86
  end
88
87
 
89
88
  def parent_scale_pitch_for(semitones_from_root)
@@ -3,7 +3,7 @@ module HeadMusic::Rudiment; end
3
3
 
4
4
  # A scale degree is a number indicating the ordinality of the spelling in the key.
5
5
  # TODO: Rewrite to accept a tonal_center and a scale type.
6
- class HeadMusic::Rudiment::ScaleDegree
6
+ class HeadMusic::Rudiment::ScaleDegree < HeadMusic::Rudiment::Base
7
7
  include Comparable
8
8
 
9
9
  NAME_FOR_DIATONIC_DEGREE = [nil, "tonic", "supertonic", "mediant", "subdominant", "dominant", "submediant"].freeze
@@ -2,9 +2,13 @@
2
2
  module HeadMusic::Rudiment; end
3
3
 
4
4
  # A ScaleType represents a particular scale pattern, such as major, lydian, or minor pentatonic.
5
- class HeadMusic::Rudiment::ScaleType
6
- H = 1 # whole step
7
- W = 2 # half step
5
+ class HeadMusic::Rudiment::ScaleType < HeadMusic::Rudiment::Base
6
+ # TODO: load scale types from yaml configuration file
7
+ # Include a system of aliasing, e.g. :natural_minor, :aeolian
8
+ # Include ascending and descending intervals for scales that differ
9
+
10
+ H = 1 # half step
11
+ W = 2 # whole step
8
12
 
9
13
  # Modal
10
14
  I = [W, W, H, W, W, W, H].freeze
@@ -33,6 +37,8 @@ class HeadMusic::Rudiment::ScaleType
33
37
 
34
38
  MINOR_PENTATONIC = [3, 2, 2, 3, 2].freeze
35
39
 
40
+ DEFAULT = :major
41
+
36
42
  def self._modes
37
43
  {}.tap do |modes|
38
44
  MODE_NAMES.each do |roman_numeral, aliases|
@@ -2,7 +2,7 @@
2
2
  module HeadMusic::Rudiment; end
3
3
 
4
4
  # A solmization is the rendering of scale degrees as syllables.
5
- class HeadMusic::Rudiment::Solmization
5
+ class HeadMusic::Rudiment::Solmization < HeadMusic::Rudiment::Base
6
6
  include HeadMusic::Named
7
7
 
8
8
  DEFAULT_SOLMIZATION = "solfège"
@@ -4,8 +4,14 @@ module HeadMusic::Rudiment; end
4
4
  # Represents the spelling of a pitch, such as C# or Db.
5
5
  # Composite of a LetterName and an optional Alteration.
6
6
  # Does not include the octave. See Pitch for that.
7
- class HeadMusic::Rudiment::Spelling
8
- MATCHER = /^\s*([A-G])(#{HeadMusic::Rudiment::Alteration.matcher}?)(-?\d+)?\s*$/i
7
+ class HeadMusic::Rudiment::Spelling < HeadMusic::Rudiment::Base
8
+ LetterName = HeadMusic::Rudiment::LetterName
9
+ Alteration = HeadMusic::Rudiment::Alteration
10
+
11
+ MATCHER = /^\s*(#{LetterName::PATTERN})(#{Alteration::PATTERN})?(-?\d+)?\s*$/i
12
+
13
+ # All chromatic spellings using single sharps and flats (ASCII notation)
14
+ CHROMATIC_SPELLINGS = %w[C C# Db D D# Eb E F F# Gb G G# Ab A A# Bb B].freeze
9
15
 
10
16
  attr_reader :pitch_class, :letter_name, :alteration
11
17
 
@@ -86,8 +92,6 @@ class HeadMusic::Rudiment::Spelling
86
92
  !alteration || alteration.natural?
87
93
  end
88
94
 
89
- private_class_method :new
90
-
91
95
  private
92
96
 
93
97
  def enharmonic_equivalence
@@ -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
+ }.freeze
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
@@ -43,43 +43,4 @@ class HeadMusic::Rudiment::Tuning::JustIntonation < HeadMusic::Rudiment::Tuning
43
43
  # Calculate the frequency
44
44
  tonal_center_frequency * ratio
45
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
46
  end
@@ -45,43 +45,4 @@ class HeadMusic::Rudiment::Tuning::Meantone < HeadMusic::Rudiment::Tuning
45
45
  # Calculate the frequency
46
46
  tonal_center_frequency * ratio
47
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
48
  end
@@ -49,43 +49,4 @@ class HeadMusic::Rudiment::Tuning::Pythagorean < HeadMusic::Rudiment::Tuning
49
49
  # Calculate the frequency
50
50
  tonal_center_frequency * ratio
51
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
52
  end
@@ -3,7 +3,7 @@ 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
@@ -32,4 +32,24 @@ class HeadMusic::Rudiment::Tuning
32
32
  pitch = HeadMusic::Rudiment::Pitch.get(pitch)
33
33
  reference_pitch_frequency * (2**(1.0 / 12))**(pitch - reference_pitch.pitch).semitones
34
34
  end
35
+
36
+ private
37
+
38
+ def calculate_tonal_center_frequency
39
+ # Use equal temperament to get the tonal center frequency from the reference pitch
40
+ interval_to_tonal_center = (tonal_center - reference_pitch.pitch).semitones
41
+ reference_pitch_frequency * (2**(interval_to_tonal_center / 12.0))
42
+ end
43
+
44
+ def ratio_for_interval(semitones)
45
+ # Handle octaves
46
+ octaves = semitones / 12
47
+ interval_within_octave = semitones % 12
48
+
49
+ # Get the base ratio from the subclass's INTERVAL_RATIOS constant
50
+ base_ratio = self.class::INTERVAL_RATIOS[self.class::INTERVAL_RATIOS.keys[interval_within_octave]]
51
+
52
+ # Apply octave adjustments
53
+ base_ratio * (2**octaves)
54
+ end
35
55
  end
@@ -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
@@ -27,11 +27,11 @@ class HeadMusic::Style::Guidelines::ConsonantClimax < HeadMusic::Style::Annotati
27
27
  end
28
28
 
29
29
  def highest_pitch_consonant_with_tonic?
30
- diatonic_interval_to_highest_pitch.consonance?(:melodic)
30
+ !diatonic_interval_to_highest_pitch.dissonant?(:melodic)
31
31
  end
32
32
 
33
33
  def lowest_pitch_consonant_with_tonic?
34
- diatonic_interval_to_lowest_pitch.consonance?(:melodic)
34
+ !diatonic_interval_to_lowest_pitch.dissonant?(:melodic)
35
35
  end
36
36
 
37
37
  def diatonic_interval_to_highest_pitch
@@ -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,31 @@
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 # 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
+ # Perfect fourth is contextual: consonant in upper voices, dissonant against bass
22
+ HeadMusic::Rudiment::Consonance::CONTEXTUAL
23
+ when 2, 10 # Major Second, Minor Seventh
24
+ HeadMusic::Rudiment::Consonance::MILD_DISSONANCE
25
+ when 1, 11 # Minor Second, Major Seventh
26
+ HeadMusic::Rudiment::Consonance::HARSH_DISSONANCE
27
+ when 6 # Tritone (Aug 4th/Dim 5th)
28
+ HeadMusic::Rudiment::Consonance::DISSONANCE
29
+ end
30
+ end
31
+ 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
+ HeadMusic::Utilities::Case.to_snake_case(self.class.name.split("::").last.sub(/Tradition$/, "")).to_sym
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HeadMusic
4
+ module Time
5
+ # A value object representing elapsed nanoseconds of clock time
6
+ #
7
+ # ClockPosition provides a high-precision representation of time elapsed
8
+ # from a reference point, stored as nanoseconds. This allows for precise
9
+ # temporal calculations in musical contexts where millisecond-level accuracy
10
+ # is required for MIDI timing, audio synchronization, and SMPTE timecode.
11
+ #
12
+ # @example Creating a position at one second
13
+ # position = HeadMusic::Time::ClockPosition.new(1_000_000_000)
14
+ # position.to_seconds # => 1.0
15
+ #
16
+ # @example Adding two positions together
17
+ # pos1 = HeadMusic::Time::ClockPosition.new(500_000_000)
18
+ # pos2 = HeadMusic::Time::ClockPosition.new(300_000_000)
19
+ # result = pos1 + pos2
20
+ # result.to_milliseconds # => 800.0
21
+ #
22
+ # @example Comparing positions
23
+ # early = HeadMusic::Time::ClockPosition.new(1_000_000_000)
24
+ # late = HeadMusic::Time::ClockPosition.new(2_000_000_000)
25
+ # early < late # => true
26
+ class ClockPosition
27
+ include Comparable
28
+
29
+ # @return [Integer] the number of nanoseconds since the reference point
30
+ attr_reader :nanoseconds
31
+
32
+ # Create a new clock position
33
+ #
34
+ # @param nanoseconds [Integer] the number of nanoseconds elapsed
35
+ def initialize(nanoseconds)
36
+ @nanoseconds = nanoseconds
37
+ end
38
+
39
+ # Convert to integer representation (nanoseconds)
40
+ #
41
+ # @return [Integer] nanoseconds
42
+ def to_i
43
+ nanoseconds
44
+ end
45
+
46
+ # Convert nanoseconds to microseconds
47
+ #
48
+ # @return [Float] elapsed microseconds
49
+ def to_microseconds
50
+ nanoseconds / 1_000.0
51
+ end
52
+
53
+ # Convert nanoseconds to milliseconds
54
+ #
55
+ # @return [Float] elapsed milliseconds
56
+ def to_milliseconds
57
+ nanoseconds / 1_000_000.0
58
+ end
59
+
60
+ # Convert nanoseconds to seconds
61
+ #
62
+ # @return [Float] elapsed seconds
63
+ def to_seconds
64
+ nanoseconds / 1_000_000_000.0
65
+ end
66
+
67
+ # Add another clock position to this one
68
+ #
69
+ # @param other [ClockPosition, #to_i] another position or value with nanoseconds
70
+ # @return [ClockPosition] a new position with the combined duration
71
+ def +(other)
72
+ self.class.new(nanoseconds + other.to_i)
73
+ end
74
+
75
+ # Compare this position to another
76
+ #
77
+ # @param other [ClockPosition, #to_i] another position to compare
78
+ # @return [Integer] -1 if less than, 0 if equal, 1 if greater than
79
+ def <=>(other)
80
+ nanoseconds <=> other.to_i
81
+ end
82
+ end
83
+ end
84
+ end