head_music 9.0.1 → 11.1.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 (103) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +9 -3
  3. data/CHANGELOG.md +18 -0
  4. data/CLAUDE.md +35 -15
  5. data/Gemfile +7 -1
  6. data/Gemfile.lock +91 -3
  7. data/README.md +18 -0
  8. data/Rakefile +7 -2
  9. data/head_music.gemspec +1 -1
  10. data/lib/head_music/analysis/dyad.rb +229 -0
  11. data/lib/head_music/analysis/melodic_interval.rb +1 -1
  12. data/lib/head_music/analysis/pitch_class_set.rb +111 -14
  13. data/lib/head_music/analysis/{pitch_set.rb → pitch_collection.rb} +11 -5
  14. data/lib/head_music/analysis/sonority.rb +50 -12
  15. data/lib/head_music/content/cantus_firmus_examples.rb +58 -0
  16. data/lib/head_music/content/staff.rb +1 -1
  17. data/lib/head_music/content/voice.rb +1 -1
  18. data/lib/head_music/instruments/alternate_tuning.rb +102 -0
  19. data/lib/head_music/instruments/alternate_tunings.yml +78 -0
  20. data/lib/head_music/instruments/instrument.rb +251 -82
  21. data/lib/head_music/instruments/instrument_configuration.rb +66 -0
  22. data/lib/head_music/instruments/instrument_configuration_option.rb +38 -0
  23. data/lib/head_music/instruments/instrument_configurations.yml +288 -0
  24. data/lib/head_music/instruments/instrument_families.yml +77 -0
  25. data/lib/head_music/instruments/instrument_family.rb +3 -4
  26. data/lib/head_music/instruments/instruments.yml +795 -965
  27. data/lib/head_music/instruments/playing_technique.rb +75 -0
  28. data/lib/head_music/instruments/playing_techniques.yml +826 -0
  29. data/lib/head_music/instruments/score_order.rb +2 -5
  30. data/lib/head_music/instruments/staff.rb +61 -1
  31. data/lib/head_music/instruments/staff_scheme.rb +6 -4
  32. data/lib/head_music/instruments/stringing.rb +115 -0
  33. data/lib/head_music/instruments/stringing_course.rb +58 -0
  34. data/lib/head_music/instruments/stringings.yml +168 -0
  35. data/lib/head_music/instruments/variant.rb +0 -1
  36. data/lib/head_music/locales/de.yml +23 -0
  37. data/lib/head_music/locales/en.yml +100 -0
  38. data/lib/head_music/locales/es.yml +23 -0
  39. data/lib/head_music/locales/fr.yml +23 -0
  40. data/lib/head_music/locales/it.yml +23 -0
  41. data/lib/head_music/locales/ru.yml +23 -0
  42. data/lib/head_music/{rudiment → notation}/musical_symbol.rb +3 -3
  43. data/lib/head_music/notation/staff_mapping.rb +70 -0
  44. data/lib/head_music/notation/staff_position.rb +62 -0
  45. data/lib/head_music/notation.rb +7 -0
  46. data/lib/head_music/rudiment/alteration.rb +17 -47
  47. data/lib/head_music/rudiment/alterations.yml +32 -0
  48. data/lib/head_music/rudiment/chromatic_interval.rb +1 -1
  49. data/lib/head_music/rudiment/clef.rb +1 -1
  50. data/lib/head_music/rudiment/consonance.rb +14 -13
  51. data/lib/head_music/rudiment/key_signature.rb +0 -26
  52. data/lib/head_music/rudiment/rhythmic_unit/parser.rb +2 -2
  53. data/lib/head_music/rudiment/rhythmic_value/parser.rb +1 -1
  54. data/lib/head_music/rudiment/rhythmic_value.rb +1 -1
  55. data/lib/head_music/rudiment/spelling.rb +3 -0
  56. data/lib/head_music/rudiment/tempo.rb +1 -1
  57. data/lib/head_music/rudiment/tuning/just_intonation.rb +0 -39
  58. data/lib/head_music/rudiment/tuning/meantone.rb +0 -39
  59. data/lib/head_music/rudiment/tuning/pythagorean.rb +0 -39
  60. data/lib/head_music/rudiment/tuning.rb +20 -0
  61. data/lib/head_music/style/guidelines/consonant_climax.rb +2 -2
  62. data/lib/head_music/style/modern_tradition.rb +8 -11
  63. data/lib/head_music/style/tradition.rb +1 -1
  64. data/lib/head_music/time/clock_position.rb +84 -0
  65. data/lib/head_music/time/conductor.rb +264 -0
  66. data/lib/head_music/time/meter_event.rb +37 -0
  67. data/lib/head_music/time/meter_map.rb +173 -0
  68. data/lib/head_music/time/musical_position.rb +188 -0
  69. data/lib/head_music/time/smpte_timecode.rb +164 -0
  70. data/lib/head_music/time/tempo_event.rb +40 -0
  71. data/lib/head_music/time/tempo_map.rb +187 -0
  72. data/lib/head_music/time.rb +32 -0
  73. data/lib/head_music/utilities/case.rb +27 -0
  74. data/lib/head_music/utilities/hash_key.rb +1 -1
  75. data/lib/head_music/version.rb +1 -1
  76. data/lib/head_music.rb +42 -13
  77. data/user_stories/backlog/notation-style.md +183 -0
  78. data/user_stories/{todo → backlog}/organizing-content.md +9 -1
  79. data/user_stories/done/consonance-dissonance-classification.md +117 -0
  80. data/user_stories/{todo → done}/dyad-analysis.md +4 -6
  81. data/user_stories/done/expand-playing-techniques.md +38 -0
  82. data/user_stories/{active → done}/handle-time.rb +5 -19
  83. data/user_stories/done/instrument-architecture.md +238 -0
  84. data/user_stories/done/move-musical-symbol-to-notation.md +161 -0
  85. data/user_stories/done/move-staff-mapping-to-notation.md +158 -0
  86. data/user_stories/done/move-staff-position-to-notation.md +141 -0
  87. data/user_stories/done/notation-module-foundation.md +102 -0
  88. data/user_stories/done/percussion_set.md +260 -0
  89. data/user_stories/{todo → done}/pitch-class-set-analysis.md +0 -40
  90. data/user_stories/done/sonority-identification.md +37 -0
  91. data/user_stories/done/string-pitches.md +41 -0
  92. data/user_stories/epics/notation-module.md +135 -0
  93. data/user_stories/{todo → visioning}/agentic-daw.md +0 -1
  94. metadata +56 -20
  95. data/check_instrument_consistency.rb +0 -0
  96. data/lib/head_music/instruments/instrument_type.rb +0 -188
  97. data/test_translations.rb +0 -15
  98. data/user_stories/todo/consonance-dissonance-classification.md +0 -57
  99. data/user_stories/todo/material-and-scores.md +0 -10
  100. data/user_stories/todo/percussion_set.md +0 -1
  101. data/user_stories/todo/pitch-set-classification.md +0 -72
  102. data/user_stories/todo/sonority-identification.md +0 -67
  103. /data/user_stories/{active → done}/handle-time.md +0 -0
@@ -162,6 +162,8 @@ fr:
162
162
  descant: dessus
163
163
  didgeridoo: didgeridoo
164
164
  double_bass: contrebasse
165
+ drum_kit: batterie
166
+ floor_tom: tom de sol
165
167
  double_contrabass_recorder: flûte à bec double-contrebasse
166
168
  english_horn: cor anglais
167
169
  euphonium: euphonium
@@ -183,6 +185,7 @@ fr:
183
185
  harpsichord: clavecin
184
186
  hautbois_d_amour: hautbois d'amour
185
187
  hi-hat: charleston
188
+ high_tom: tom aigu
186
189
  horn: cor
187
190
  kettledrum: timbale
188
191
  kick_drum: grosse caisse
@@ -194,6 +197,7 @@ fr:
194
197
  mezzo: mezzo
195
198
  mezzo_soprano: mezzo-soprano
196
199
  mezzo_soprano_voice: voix de mezzo-soprano
200
+ mid_tom: tom grave
197
201
  military_drum: tambour militaire
198
202
  mouth_organ: harmonica
199
203
  natural_horn: cor naturel
@@ -281,6 +285,25 @@ fr:
281
285
  woodblock: bloc de bois
282
286
  xylophone: xylophone
283
287
  zither: cithare
288
+ instrument_families:
289
+ drum_kit: batterie
290
+ hi_hat: charleston
291
+ tom_tom: tom-tom
292
+ playing_techniques:
293
+ bell: cloche
294
+ bow: archet
295
+ brush: brosse
296
+ choked: étouffé
297
+ closed: fermé
298
+ cross_stick: baguette croisée
299
+ damped: amorti
300
+ hand: main
301
+ let_ring: laisser résonner
302
+ mallet: maillet
303
+ open: ouvert
304
+ pedal: pédale
305
+ rim_shot: coup de bord
306
+ stick: baguette
284
307
  locales:
285
308
  de: allemand
286
309
  en: anglais
@@ -157,6 +157,8 @@ it:
157
157
  descant: soprano
158
158
  didgeridoo: didgeridoo
159
159
  double_bass: contrabbasso
160
+ drum_kit: batteria
161
+ floor_tom: tom a terra
160
162
  double_contrabass_recorder: doppio flauto dolce contrabbasso
161
163
  english_horn: corno inglese
162
164
  euphonium: eufonio
@@ -178,6 +180,7 @@ it:
178
180
  harpsichord: clavicembalo
179
181
  hautbois_d_amour: oboe d'amore
180
182
  hi-hat: hi-hat
183
+ high_tom: tom acuto
181
184
  horn: corno
182
185
  kettledrum: timpano
183
186
  kick_drum: grancassa
@@ -189,6 +192,7 @@ it:
189
192
  metal_block: blocco metallico
190
193
  mezzo: mezzosoprano
191
194
  mezzo_soprano_voice: mezzosoprano
195
+ mid_tom: tom grave
192
196
  military_drum: tamburo militare
193
197
  mouth_organ: armonica a bocca
194
198
  natural_horn: corno naturale
@@ -274,6 +278,25 @@ it:
274
278
  woodblock: legnetto
275
279
  xylophone: xilofono
276
280
  zither: cetra da tavolo
281
+ instrument_families:
282
+ drum_kit: batteria
283
+ hi_hat: hi-hat
284
+ tom_tom: tom-tom
285
+ playing_techniques:
286
+ bell: campana
287
+ bow: arco
288
+ brush: spazzola
289
+ choked: soffocato
290
+ closed: chiuso
291
+ cross_stick: bacchetta incrociata
292
+ damped: smorzato
293
+ hand: mano
294
+ let_ring: lasciar vibrare
295
+ mallet: bacchetta
296
+ open: aperto
297
+ pedal: pedale
298
+ rim_shot: colpo di rimshot
299
+ stick: bacchetta
277
300
  locales:
278
301
  de: tedesco
279
302
  en: inglese
@@ -103,6 +103,8 @@ ru:
103
103
  descant: дискант
104
104
  didgeridoo: диджериду
105
105
  double_bass: контрабас
106
+ drum_kit: барабанная установка
107
+ floor_tom: том для пола
106
108
  double_contrabass_recorder: двойная контрабасовая флейта
107
109
  english_horn: английский рожок
108
110
  euphonium: эуфониум
@@ -124,6 +126,7 @@ ru:
124
126
  harpsichord: клавесин
125
127
  hautbois_d_amour: гобой д'амур
126
128
  hi-hat: хай-хет
129
+ high_tom: том высокий
127
130
  horn: валторна
128
131
  kettledrum: тимпан
129
132
  kick_drum: бас-барабан
@@ -134,6 +137,7 @@ ru:
134
137
  mezzo: меццо
135
138
  mezzo_soprano: меццо-сопрано
136
139
  mezzo_soprano_voice: голос меццо-сопрано
140
+ mid_tom: том средний
137
141
  military_drum: военный барабан
138
142
  mouth_organ: губная гармоника
139
143
  natural_horn: натуральный рог
@@ -221,6 +225,25 @@ ru:
221
225
  woodblock: деревянный блок
222
226
  xylophone: ксилофон
223
227
  zither: цитра
228
+ instrument_families:
229
+ drum_kit: барабанная установка
230
+ hi_hat: хай-хет
231
+ tom_tom: том-том
232
+ playing_techniques:
233
+ bell: колокол
234
+ bow: смычок
235
+ brush: щётка
236
+ choked: задушенный
237
+ closed: закрытый
238
+ cross_stick: крест-стик
239
+ damped: приглушённый
240
+ hand: рука
241
+ let_ring: дать звучать
242
+ mallet: молоточек
243
+ open: открытый
244
+ pedal: педаль
245
+ rim_shot: удар по ободу
246
+ stick: палочка
224
247
  locales:
225
248
  de: Немецкий
226
249
  en: Английский
@@ -1,8 +1,8 @@
1
- # A module for music rudiments
2
- module HeadMusic::Rudiment; end
1
+ # A module for visual music notation
2
+ module HeadMusic::Notation; end
3
3
 
4
4
  # A symbol is a mark or sign that signifies a particular rudiment of music
5
- class HeadMusic::Rudiment::MusicalSymbol < HeadMusic::Rudiment::Base
5
+ class HeadMusic::Notation::MusicalSymbol
6
6
  attr_reader :ascii, :unicode, :html_entity
7
7
 
8
8
  def initialize(ascii: nil, unicode: nil, html_entity: nil)
@@ -0,0 +1,70 @@
1
+ # A module for visual music notation
2
+ module HeadMusic::Notation; end
3
+
4
+ # Represents the mapping of an instrument (and optional playing technique) to a staff position
5
+ #
6
+ # @example Basic mapping
7
+ # mapping = StaffMapping.new({
8
+ # "staff_position" => 4,
9
+ # "instrument" => "snare_drum"
10
+ # })
11
+ # mapping.instrument.name #=> "snare drum"
12
+ # mapping.position_index #=> 4
13
+ #
14
+ # @example Mapping with playing technique
15
+ # mapping = StaffMapping.new({
16
+ # "staff_position" => -1,
17
+ # "instrument" => "hi_hat",
18
+ # "playing_technique" => "pedal"
19
+ # })
20
+ # mapping.playing_technique.name #=> "pedal"
21
+ class HeadMusic::Notation::StaffMapping
22
+ attr_reader :staff_position, :instrument_key, :playing_technique_key
23
+
24
+ # Initialize a new StaffMapping
25
+ #
26
+ # @param attributes [Hash] the mapping attributes
27
+ # @option attributes [Integer, String] "staff_position" the staff position index
28
+ # @option attributes [String] "instrument" the instrument key
29
+ # @option attributes [String] "playing_technique" optional playing technique key
30
+ def initialize(attributes)
31
+ @staff_position = HeadMusic::Notation::StaffPosition.new(attributes["staff_position"].to_i)
32
+ @instrument_key = attributes["instrument"]
33
+ @playing_technique_key = attributes["playing_technique"]
34
+ end
35
+
36
+ # Get the Instrument object
37
+ #
38
+ # @return [Instrument, nil] the instrument or nil if not found
39
+ def instrument
40
+ HeadMusic::Instruments::Instrument.get(instrument_key) if instrument_key
41
+ end
42
+
43
+ # Get the PlayingTechnique object
44
+ #
45
+ # @return [PlayingTechnique, nil] the playing technique or nil if not specified
46
+ def playing_technique
47
+ HeadMusic::Instruments::PlayingTechnique.get(playing_technique_key) if playing_technique_key
48
+ end
49
+
50
+ # Get the staff position index
51
+ #
52
+ # @return [Integer] the position index
53
+ def position_index
54
+ staff_position.index
55
+ end
56
+
57
+ # String representation of this mapping
58
+ #
59
+ # @return [String] human-readable description
60
+ # @example
61
+ # mapping.to_s #=> "snare drum at line 3"
62
+ # mapping.to_s #=> "hi hat (pedal) at Space 0"
63
+ def to_s
64
+ parts = []
65
+ parts << (instrument&.name || instrument_key) if instrument_key
66
+ parts << "(#{playing_technique})" if playing_technique_key
67
+ parts << "at #{staff_position}"
68
+ parts.compact.join(" ")
69
+ end
70
+ end
@@ -0,0 +1,62 @@
1
+ # A module for visual music notation
2
+ module HeadMusic::Notation; end
3
+
4
+ class HeadMusic::Notation::StaffPosition
5
+ attr_reader :index # Integer, even = line, odd = space; bottom line = 0
6
+
7
+ NAMES = {
8
+ -2 => ["ledger line below staff"],
9
+ -1 => ["space below staff"],
10
+ 0 => ["bottom line", "line 1"],
11
+ 1 => ["bottom space", "space 1"],
12
+ 2 => ["line 2"],
13
+ 3 => ["space 2"],
14
+ 4 => ["middle line", "line 3"],
15
+ 5 => ["space 3"],
16
+ 6 => ["line 4"],
17
+ 7 => ["space 4"],
18
+ 8 => ["top line", "line 5"],
19
+ 9 => ["space above staff"],
20
+ 10 => ["ledger line above staff"]
21
+ }.freeze
22
+
23
+ # Accepts a name (string or symbol) and returns the corresponding StaffPosition index (integer), or nil if not found
24
+ def self.name_to_index(name)
25
+ NAMES.each do |index, names|
26
+ if names.map { |n|
27
+ HeadMusic::Utilities::Case.to_snake_case(n)
28
+ }.include?(HeadMusic::Utilities::Case.to_snake_case(name))
29
+ return index
30
+ end
31
+ end
32
+ nil
33
+ end
34
+
35
+ def initialize(index)
36
+ @index = index
37
+ end
38
+
39
+ def line?
40
+ index.even?
41
+ end
42
+
43
+ def space?
44
+ index.odd?
45
+ end
46
+
47
+ def line_number
48
+ return nil unless line?
49
+ (index / 2) + 1
50
+ end
51
+
52
+ def space_number
53
+ return nil unless space?
54
+ ((index - 1) / 2) + 1
55
+ end
56
+
57
+ def to_s
58
+ return NAMES[index].first if NAMES.key?(index)
59
+
60
+ line? ? "line #{line_number}" : "space #{space_number}"
61
+ end
62
+ end
@@ -0,0 +1,7 @@
1
+ # A module for visual music notation
2
+ module HeadMusic::Notation; end
3
+
4
+ # Load notation classes
5
+ require "head_music/notation/musical_symbol"
6
+ require "head_music/notation/staff_position"
7
+ require "head_music/notation/staff_mapping"
@@ -1,5 +1,3 @@
1
- require "head_music/rudiment/musical_symbol"
2
-
3
1
  # A module for music rudiments
4
2
  module HeadMusic::Rudiment; end
5
3
 
@@ -9,40 +7,20 @@ class HeadMusic::Rudiment::Alteration < HeadMusic::Rudiment::Base
9
7
  include Comparable
10
8
  include HeadMusic::Named
11
9
 
12
- attr_reader :identifier, :cents, :musical_symbols
10
+ attr_reader :identifier, :semitones, :musical_symbols
13
11
 
14
12
  delegate :ascii, :unicode, :html_entity, to: :musical_symbol
15
13
 
16
- ALTERATION_RECORDS = [
17
- {
18
- identifier: :sharp, cents: 100,
19
- symbols: [{ascii: "#", unicode: "♯", html_entity: "&#9839;"}]
20
- },
21
- {
22
- identifier: :flat, cents: -100,
23
- symbols: [{ascii: "b", unicode: "♭", html_entity: "&#9837;"}]
24
- },
25
- {
26
- identifier: :natural, cents: 0,
27
- symbols: [{ascii: "", unicode: "♮", html_entity: "&#9838;"}]
28
- },
29
- {
30
- identifier: :double_sharp, cents: 200,
31
- symbols: [{ascii: "x", unicode: "𝄪", html_entity: "&#119082;"}]
32
- },
33
- {
34
- identifier: :double_flat, cents: -200,
35
- symbols: [{ascii: "bb", unicode: "𝄫", html_entity: "&#119083;"}]
36
- }
37
- ].freeze
38
-
39
- ALTERATION_IDENTIFIERS = ALTERATION_RECORDS.map { |attributes| attributes[:identifier] }.freeze
40
- SYMBOLS = ALTERATION_RECORDS.map { |attributes| attributes[:symbols].map { |symbol| [symbol[:unicode], symbol[:ascii]] } }.flatten.freeze
41
- PATTERN = Regexp.union(SYMBOLS.reject { |s| s.nil? || s.empty? })
14
+ ALTERATION_RECORDS =
15
+ YAML.load_file(File.expand_path("alterations.yml", __dir__), symbolize_names: true)[:alterations].freeze
16
+
17
+ ALTERATION_IDENTIFIERS = ALTERATION_RECORDS.keys.freeze
18
+ SYMBOLS = ALTERATION_RECORDS.map { |key, attributes| attributes[:symbols].map { |symbol| [symbol[:unicode], symbol[:ascii]] } }.flatten.reject { |s| s.nil? || s.empty? }.freeze
19
+ PATTERN = Regexp.union(SYMBOLS)
42
20
  MATCHER = PATTERN
43
21
 
44
22
  def self.all
45
- ALTERATION_RECORDS.map { |attributes| new(attributes) }
23
+ @all ||= ALTERATION_RECORDS.map { |key, attributes| new(key, attributes) }
46
24
  end
47
25
 
48
26
  def self.symbols
@@ -57,13 +35,13 @@ class HeadMusic::Rudiment::Alteration < HeadMusic::Rudiment::Base
57
35
  return identifier if identifier.is_a?(HeadMusic::Rudiment::Alteration)
58
36
 
59
37
  all.detect do |alteration|
60
- alteration.representions.include?(identifier)
38
+ alteration.representations.include?(identifier)
61
39
  end
62
40
  end
63
41
 
64
42
  def self.by(key, value)
65
43
  all.detect do |alteration|
66
- alteration.send(key) == value if %i[cents semitones].include?(key.to_sym)
44
+ alteration.send(key) == value if %i[semitones].include?(key.to_sym)
67
45
  end
68
46
  end
69
47
 
@@ -71,24 +49,16 @@ class HeadMusic::Rudiment::Alteration < HeadMusic::Rudiment::Base
71
49
  all.detect { |alteration| alteration.name == name.to_s }
72
50
  end
73
51
 
74
- def self.from_pitched_item(input)
75
- nil
76
- end
77
-
78
52
  def name(locale_code: I18n.locale)
79
53
  super || identifier.to_s.tr("_", " ")
80
54
  end
81
55
 
82
- def representions
56
+ def representations
83
57
  [identifier, identifier.to_s, name, ascii, unicode, html_entity]
84
58
  .reject { |representation| representation.to_s.strip == "" }
85
59
  end
86
60
 
87
- def semitones
88
- cents / 100.0
89
- end
90
-
91
- ALTERATION_IDENTIFIERS.each do |key|
61
+ ALTERATION_RECORDS.keys.each do |key|
92
62
  define_method(:"#{key}?") { identifier == key }
93
63
  end
94
64
 
@@ -98,7 +68,7 @@ class HeadMusic::Rudiment::Alteration < HeadMusic::Rudiment::Base
98
68
 
99
69
  def <=>(other)
100
70
  other = HeadMusic::Rudiment::Alteration.get(other)
101
- cents <=> other.cents
71
+ semitones <=> other.semitones
102
72
  end
103
73
 
104
74
  def musical_symbol
@@ -107,9 +77,9 @@ class HeadMusic::Rudiment::Alteration < HeadMusic::Rudiment::Base
107
77
 
108
78
  private
109
79
 
110
- def initialize(attributes)
111
- @identifier = attributes[:identifier]
112
- @cents = attributes[:cents]
80
+ def initialize(key, attributes)
81
+ @identifier = key
82
+ @semitones = attributes[:semitones]
113
83
  initialize_musical_symbols(attributes[:symbols])
114
84
  initialize_localized_names
115
85
  end
@@ -122,7 +92,7 @@ class HeadMusic::Rudiment::Alteration < HeadMusic::Rudiment::Base
122
92
 
123
93
  def initialize_musical_symbols(list)
124
94
  @musical_symbols = (list || []).map do |record|
125
- HeadMusic::Rudiment::MusicalSymbol.new(
95
+ HeadMusic::Notation::MusicalSymbol.new(
126
96
  unicode: record[:unicode],
127
97
  ascii: record[:ascii],
128
98
  html_entity: record[:html_entity]
@@ -0,0 +1,32 @@
1
+ alterations:
2
+ double_flat:
3
+ semitones: -2
4
+ symbols:
5
+ - ascii: "bb"
6
+ unicode: "𝄫"
7
+ html_entity: "&#119083;"
8
+ flat:
9
+ semitones: -1
10
+ symbols:
11
+ - ascii: "b"
12
+ unicode: "♭"
13
+ html_entity: "&#9837;"
14
+ natural:
15
+ cents: 0
16
+ semitones: 0
17
+ symbols:
18
+ - ascii: ""
19
+ unicode: "♮"
20
+ html_entity: "&#9838;"
21
+ sharp:
22
+ semitones: 1
23
+ symbols:
24
+ - ascii: "#"
25
+ unicode: "♯"
26
+ html_entity: "&#9839;"
27
+ double_sharp:
28
+ semitones: 2
29
+ symbols:
30
+ - ascii: "x"
31
+ unicode: "𝄪"
32
+ html_entity: "&#119082;"
@@ -17,7 +17,7 @@ class HeadMusic::Rudiment::ChromaticInterval < HeadMusic::Rudiment::Base
17
17
 
18
18
  def self.get(identifier)
19
19
  @intervals ||= {}
20
- candidate = identifier.to_s.downcase.gsub(/\W+/, "_")
20
+ candidate = HeadMusic::Utilities::Case.to_snake_case(identifier)
21
21
  semitones = NAMES.index(candidate) || identifier.to_i
22
22
  @intervals[semitones] ||= new(semitones.to_i)
23
23
  end
@@ -98,7 +98,7 @@ class HeadMusic::Rudiment::Clef < HeadMusic::Rudiment::Base
98
98
 
99
99
  def initialize_musical_symbols(list)
100
100
  @musical_symbols = (list || []).map do |symbol_data|
101
- HeadMusic::Rudiment::MusicalSymbol.new(**symbol_data.slice(:ascii, :html_entity, :unicode))
101
+ HeadMusic::Notation::MusicalSymbol.new(**symbol_data.slice(:ascii, :html_entity, :unicode))
102
102
  end
103
103
  end
104
104
  end
@@ -3,17 +3,6 @@ module HeadMusic::Rudiment; end
3
3
 
4
4
  # Consonance describes a category or degree of harmonic pleasantness
5
5
  class HeadMusic::Rudiment::Consonance < HeadMusic::Rudiment::Base
6
- # Detailed categories aligned with music theory
7
- LEVELS = %w[
8
- perfect_consonance
9
- imperfect_consonance
10
- contextual
11
- mild_dissonance
12
- harsh_dissonance
13
- dissonance
14
- ].freeze
15
-
16
- # Constants for each consonance level
17
6
  PERFECT_CONSONANCE = :perfect_consonance
18
7
  IMPERFECT_CONSONANCE = :imperfect_consonance
19
8
  CONTEXTUAL = :contextual
@@ -21,10 +10,22 @@ class HeadMusic::Rudiment::Consonance < HeadMusic::Rudiment::Base
21
10
  HARSH_DISSONANCE = :harsh_dissonance
22
11
  DISSONANCE = :dissonance
23
12
 
13
+ LEVELS = [
14
+ PERFECT_CONSONANCE,
15
+ IMPERFECT_CONSONANCE,
16
+ CONTEXTUAL,
17
+ MILD_DISSONANCE,
18
+ HARSH_DISSONANCE,
19
+ DISSONANCE
20
+ ].freeze
21
+
24
22
  def self.get(name)
23
+ return name if name.is_a?(self)
24
+ return nil if name.nil?
25
+
25
26
  @consonances ||= {}
26
27
  name_sym = name.to_sym
27
- @consonances[name_sym] ||= new(name) if LEVELS.include?(name.to_s)
28
+ @consonances[name_sym] ||= new(name) if LEVELS.include?(name_sym)
28
29
  end
29
30
 
30
31
  attr_reader :name
@@ -56,6 +57,6 @@ class HeadMusic::Rudiment::Consonance < HeadMusic::Rudiment::Base
56
57
 
57
58
  # Predicate methods for each level
58
59
  LEVELS.each do |method_name|
59
- define_method(:"#{method_name}?") { to_s == method_name }
60
+ define_method(:"#{method_name}?") { to_s == method_name.to_s }
60
61
  end
61
62
  end
@@ -4,8 +4,6 @@ module HeadMusic::Rudiment; end
4
4
  # Represents a key signature (traditionally associated with a key)
5
5
  # This class maintains backward compatibility while delegating to Key/Mode internally
6
6
  class HeadMusic::Rudiment::KeySignature < HeadMusic::Rudiment::Base
7
- attr_reader :tonal_context
8
-
9
7
  ORDERED_LETTER_NAMES_OF_SHARPS = %w[F C G D A E B].freeze
10
8
  ORDERED_LETTER_NAMES_OF_FLATS = ORDERED_LETTER_NAMES_OF_SHARPS.reverse.freeze
11
9
 
@@ -27,10 +25,6 @@ class HeadMusic::Rudiment::KeySignature < HeadMusic::Rudiment::Base
27
25
  end
28
26
  end
29
27
 
30
- def self.from_tonal_context(tonal_context)
31
- new_from_context(tonal_context)
32
- end
33
-
34
28
  def self.from_scale(scale)
35
29
  # Find a key or mode that uses this scale
36
30
  tonic = scale.root_pitch.spelling
@@ -50,26 +44,6 @@ class HeadMusic::Rudiment::KeySignature < HeadMusic::Rudiment::Base
50
44
  @scale_type ||= HeadMusic::Rudiment::ScaleType.default
51
45
  @scale_type = @scale_type.parent || @scale_type
52
46
  @scale = HeadMusic::Rudiment::Scale.get(@tonic_spelling, @scale_type)
53
-
54
- # Create appropriate tonal context
55
- scale_type_str = scale_type.to_s.downcase if scale_type
56
-
57
- @tonal_context = if %w[major minor].include?(scale_type_str)
58
- HeadMusic::Rudiment::Key.get("#{tonic_spelling} #{scale_type}")
59
- elsif scale_type
60
- HeadMusic::Rudiment::Mode.get("#{tonic_spelling} #{scale_type}")
61
- else
62
- HeadMusic::Rudiment::Key.get("#{tonic_spelling} major")
63
- end
64
- rescue ArgumentError
65
- # Fall back to creating a major key if mode is not recognized
66
- @tonal_context = HeadMusic::Rudiment::Key.get("#{tonic_spelling} major")
67
- end
68
-
69
- def self.new_from_context(context)
70
- instance = allocate
71
- instance.instance_variable_set(:@tonal_context, context)
72
- instance
73
47
  end
74
48
 
75
49
  def spellings
@@ -38,7 +38,7 @@ class HeadMusic::Rudiment::RhythmicUnit::Parser
38
38
  end
39
39
 
40
40
  def normalized_identifier
41
- @normalized_identifier ||= identifier.downcase.strip.gsub(/[^a-z0-9]/, "_").gsub(/_+/, "_").gsub(/^_|_$/, "")
41
+ @normalized_identifier ||= HeadMusic::Utilities::Case.to_snake_case(identifier)
42
42
  end
43
43
 
44
44
  def from_american_name
@@ -81,6 +81,6 @@ class HeadMusic::Rudiment::RhythmicUnit::Parser
81
81
 
82
82
  def normalize_name(name)
83
83
  return nil if name.nil?
84
- name.to_s.downcase.strip.gsub(/[^a-z0-9]/, "_").gsub(/_+/, "_").gsub(/^_|_$/, "")
84
+ HeadMusic::Utilities::Case.to_snake_case(name)
85
85
  end
86
86
  end
@@ -45,7 +45,7 @@ class HeadMusic::Rudiment::RhythmicValue::Parser
45
45
  # But skip this if identifier looks like a decimal number
46
46
  unless identifier.match?(/^\d+\.\d+$/)
47
47
  dots = identifier.scan(".").count
48
- base_identifier = identifier.gsub(".", "").strip
48
+ base_identifier = identifier.delete(".").strip
49
49
 
50
50
  # Try RhythmicUnit::Parser on the base identifier
51
51
  parser = RhythmicUnit::Parser.new(base_identifier)
@@ -25,7 +25,7 @@ class HeadMusic::Rudiment::RhythmicValue
25
25
  return parsed if parsed
26
26
 
27
27
  # Then try the word-based approach as fallback
28
- identifier = original_identifier.downcase.gsub(/\W+/, "_")
28
+ identifier = HeadMusic::Utilities::Case.to_snake_case(original_identifier)
29
29
  begin
30
30
  from_words(identifier)
31
31
  rescue
@@ -10,6 +10,9 @@ class HeadMusic::Rudiment::Spelling < HeadMusic::Rudiment::Base
10
10
 
11
11
  MATCHER = /^\s*(#{LetterName::PATTERN})(#{Alteration::PATTERN})?(-?\d+)?\s*$/i
12
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
15
+
13
16
  attr_reader :pitch_class, :letter_name, :alteration
14
17
 
15
18
  delegate :number, to: :pitch_class, prefix: true
@@ -35,7 +35,7 @@ class HeadMusic::Rudiment::Tempo
35
35
  allegrissimo: ["quarter", 172], # 172–176 bpm
36
36
  presto: ["quarter", 180], # 168–200 bpm
37
37
  prestissimo: ["quarter", 200] # 200 bpm and over
38
- }
38
+ }.freeze
39
39
 
40
40
  def self.get(identifier)
41
41
  @tempos ||= {}
@@ -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