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,139 @@
1
+ module HeadMusic::Instruments; end
2
+
3
+ class HeadMusic::Instruments::ScoreOrder
4
+ include HeadMusic::Named
5
+
6
+ SCORE_ORDERS = YAML.load_file(File.expand_path("score_orders.yml", __dir__)).freeze
7
+
8
+ DEFAULT_ENSEMBLE_TYPE_KEY = :orchestral
9
+
10
+ attr_reader :ensemble_type_key, :sections
11
+
12
+ # Factory method to get a ScoreOrder instance for a specific ensemble type
13
+ def self.get(ensemble_type)
14
+ @instances ||= {}
15
+ key = HeadMusic::Utilities::HashKey.for(ensemble_type)
16
+ return unless SCORE_ORDERS.key?(key.to_s)
17
+
18
+ @instances[key] ||= new(key)
19
+ end
20
+
21
+ # Convenience method to order instruments in orchestral order
22
+ def self.in_orchestral_order(instruments)
23
+ get(:orchestral).order(instruments)
24
+ end
25
+
26
+ # Convenience method to order instruments in concert band order
27
+ def self.in_band_order(instruments)
28
+ get(:band).order(instruments)
29
+ end
30
+
31
+ # Accepts a list of instruments and orders them according to this ensemble type's conventions
32
+ def order(instruments)
33
+ valid_inputs = instruments.compact.reject { |i| i.respond_to?(:empty?) && i.empty? }
34
+ instrument_objects = valid_inputs.map { |i| normalize_to_instrument(i) }.compact
35
+
36
+ # Build ordering index
37
+ ordering_index = build_ordering_index
38
+
39
+ # Separate known and unknown instruments
40
+ known_instruments = []
41
+ unknown_instruments = []
42
+
43
+ instrument_objects.each do |instrument|
44
+ position_info = find_position_with_transposition(instrument, ordering_index)
45
+ if position_info
46
+ known_instruments << [instrument, position_info]
47
+ else
48
+ unknown_instruments << instrument
49
+ end
50
+ end
51
+
52
+ # Sort known instruments by position (primary) and transposition (secondary)
53
+ sorted_known = known_instruments.sort_by { |_, pos_info|
54
+ [pos_info[:position], -pos_info[:transposition]]
55
+ }.map(&:first)
56
+ sorted_known + unknown_instruments.sort_by(&:to_s)
57
+ end
58
+
59
+ private_class_method :new
60
+
61
+ private
62
+
63
+ def initialize(ensemble_type_key = DEFAULT_ENSEMBLE_TYPE_KEY)
64
+ @ensemble_type_key = ensemble_type_key.to_sym
65
+ data = SCORE_ORDERS[ensemble_type_key.to_s]
66
+
67
+ @sections = data["sections"] || []
68
+ self.name = data["name"] || ensemble_type_key.to_s.tr("_", " ").capitalize
69
+ end
70
+
71
+ def normalize_to_instrument(input)
72
+ # Return if already an Instrument instance
73
+ return input if input.is_a?(HeadMusic::Instruments::Instrument)
74
+
75
+ # Return InstrumentType instances as-is for backward compatibility (duck typing)
76
+ return input.default_instrument if input.is_a?(HeadMusic::Instruments::InstrumentType)
77
+
78
+ # Return other objects that respond to required methods (mock objects, etc.)
79
+ return input if input.respond_to?(:name_key) && input.respond_to?(:family_key)
80
+
81
+ # Create an Instrument instance for string inputs
82
+ HeadMusic::Instruments::Instrument.get(input) || HeadMusic::Instruments::InstrumentType.get(input)
83
+ end
84
+
85
+ # Builds an index mapping instrument names to their position in the order
86
+ def build_ordering_index
87
+ index = {}
88
+ position = 0
89
+
90
+ sections.each do |section|
91
+ instruments = section["instruments"] || []
92
+ instruments.each do |instrument_key|
93
+ # Store position for this instrument key
94
+ index[instrument_key.to_s] = position
95
+ position += 1
96
+ end
97
+ end
98
+
99
+ index
100
+ end
101
+
102
+ # Finds the position of an instrument in the ordering
103
+ def find_position(instrument, ordering_index)
104
+ # Try exact match with name_key
105
+ return ordering_index[instrument.name_key.to_s] if instrument.name_key && ordering_index.key?(instrument.name_key.to_s)
106
+
107
+ # Try matching by family + range category (e.g., alto_saxophone -> saxophone family)
108
+ if instrument.family_key
109
+ family_base = instrument.family_key.to_s
110
+ instrument_key = instrument.name_key.to_s
111
+
112
+ # Check if this is a variant of a family (e.g., alto_saxophone)
113
+ if instrument_key.include?(family_base)
114
+ # Look for the specific variant first
115
+ return ordering_index[instrument_key] if ordering_index.key?(instrument_key)
116
+
117
+ # Fall back to generic family instrument if listed
118
+ return ordering_index[family_base] if ordering_index.key?(family_base)
119
+ end
120
+ end
121
+
122
+ # Try normalized name (lowercase, underscored)
123
+ normalized = instrument.name.downcase.tr(" ", "_").tr("-", "_")
124
+ return ordering_index[normalized] if ordering_index.key?(normalized)
125
+
126
+ nil
127
+ end
128
+
129
+ # Finds the position and transposition information for an instrument
130
+ def find_position_with_transposition(instrument, ordering_index)
131
+ position = find_position(instrument, ordering_index)
132
+ return nil unless position
133
+
134
+ # Get the sounding transposition for secondary sorting
135
+ transposition = instrument.default_sounding_transposition || 0
136
+
137
+ {position: position, transposition: transposition}
138
+ end
139
+ end
@@ -0,0 +1,130 @@
1
+ ---
2
+ orchestral:
3
+ name: "Orchestral"
4
+ sections:
5
+ - section_key: woodwind
6
+ instruments:
7
+ - piccolo_flute
8
+ - flute
9
+ - alto_flute
10
+ - alto_recorder
11
+ - soprano_recorder
12
+ - tenor_recorder
13
+ - oboe
14
+ - english_horn
15
+ - clarinet
16
+ - alto_clarinet
17
+ - bass_clarinet
18
+ - soprano_saxophone
19
+ - alto_saxophone
20
+ - tenor_saxophone
21
+ - baritone_saxophone
22
+ - bassoon
23
+ - contrabassoon
24
+ - section_key: brass
25
+ instruments:
26
+ - french_horn
27
+ - trumpet
28
+ - cornet
29
+ - trombone
30
+ - bass_trombone
31
+ - tuba
32
+ - section_key: percussion
33
+ instruments:
34
+ - timpani
35
+ - snare_drum
36
+ - bass_drum
37
+ - cymbal
38
+ - gong
39
+ - xylophone
40
+ - glockenspiel
41
+ - marimba
42
+ - vibraphone
43
+ - percussion
44
+ - section_key: keyboard
45
+ instruments:
46
+ - harp
47
+ - piano
48
+ - celesta
49
+ - harpsichord
50
+ - organ
51
+ - section_key: voice
52
+ instruments:
53
+ - soprano_voice
54
+ - alto_voice
55
+ - tenor_voice
56
+ - bass_voice
57
+ - section_key: string
58
+ instruments:
59
+ - violin
60
+ - viola
61
+ - cello
62
+ - double_bass
63
+
64
+ band:
65
+ name: "Concert Band"
66
+ sections:
67
+ - section_key: woodwind
68
+ instruments:
69
+ - piccolo
70
+ - flute
71
+ - oboe
72
+ - english_horn
73
+ - bassoon
74
+ - contrabassoon
75
+ - clarinet
76
+ - alto_clarinet
77
+ - bass_clarinet
78
+ - soprano_saxophone
79
+ - alto_saxophone
80
+ - tenor_saxophone
81
+ - baritone_saxophone
82
+ - section_key: brass
83
+ instruments:
84
+ - cornet
85
+ - trumpet
86
+ - french_horn
87
+ - trombone
88
+ - bass_trombone
89
+ - euphonium
90
+ - baritone_horn
91
+ - tuba
92
+ - section_key: percussion
93
+ instruments:
94
+ - timpani
95
+ - snare_drum
96
+ - bass_drum
97
+ - cymbal
98
+ - percussion
99
+
100
+ brass_quintet:
101
+ name: "Brass Quintet"
102
+ sections:
103
+ - section_key: brass
104
+ instruments:
105
+ - trumpet # First trumpet
106
+ - trumpet # Second trumpet
107
+ - french_horn
108
+ - trombone
109
+ - tuba
110
+
111
+ woodwind_quintet:
112
+ name: "Woodwind Quintet"
113
+ sections:
114
+ - section_key: mixed
115
+ instruments:
116
+ - flute
117
+ - oboe
118
+ - clarinet
119
+ - french_horn # Horn is traditional in woodwind quintets
120
+ - bassoon
121
+
122
+ string_quartet:
123
+ name: "String Quartet"
124
+ sections:
125
+ - section_key: string
126
+ instruments:
127
+ - violin # First violin
128
+ - violin # Second violin
129
+ - viola
130
+ - cello
@@ -35,4 +35,10 @@ class HeadMusic::Instruments::Variant
35
35
  @default_staff_scheme ||=
36
36
  staff_schemes.find(&:default?) || staff_schemes.first
37
37
  end
38
+
39
+ def ==(other)
40
+ return false unless other.is_a?(self.class)
41
+
42
+ key == other.key && attributes == other.attributes
43
+ end
38
44
  end
@@ -1,5 +1,11 @@
1
1
  de:
2
2
  head_music:
3
+ alterations:
4
+ sharp: Kreuz
5
+ flat: Be
6
+ natural: Auflösungszeichen
7
+ double_sharp: Doppelkreuz
8
+ double_flat: Doppel-Be
3
9
  chromatic_intervals:
4
10
  perfect_unison: reine Prime
5
11
  minor_second: kleine Sekunde
@@ -1,5 +1,11 @@
1
1
  en:
2
2
  head_music:
3
+ alterations:
4
+ sharp: sharp
5
+ flat: flat
6
+ natural: natural
7
+ double_sharp: double sharp
8
+ double_flat: double flat
3
9
  chromatic_intervals:
4
10
  perfect_unison: perfect unison
5
11
  minor_second: minor second
@@ -14,6 +20,98 @@ en:
14
20
  minor_seventh: minor seventh
15
21
  major_seventh: major seventh
16
22
  perfect_octave: perfect octave
23
+ diatonic_intervals:
24
+ perfect_unison: perfect unison
25
+ minor_second: minor second
26
+ major_second: major second
27
+ diminished_second: diminished second
28
+ augmented_second: augmented second
29
+ doubly_diminished_second: doubly diminished second
30
+ doubly_augmented_second: doubly augmented second
31
+ minor_third: minor third
32
+ major_third: major third
33
+ diminished_third: diminished third
34
+ augmented_third: augmented third
35
+ doubly_diminished_third: doubly diminished third
36
+ doubly_augmented_third: doubly augmented third
37
+ perfect_fourth: perfect fourth
38
+ diminished_fourth: diminished fourth
39
+ augmented_fourth: augmented fourth
40
+ doubly_diminished_fourth: doubly diminished fourth
41
+ doubly_augmented_fourth: doubly augmented fourth
42
+ perfect_fifth: perfect fifth
43
+ diminished_fifth: diminished fifth
44
+ augmented_fifth: augmented fifth
45
+ doubly_diminished_fifth: doubly diminished fifth
46
+ doubly_augmented_fifth: doubly augmented fifth
47
+ minor_sixth: minor sixth
48
+ major_sixth: major sixth
49
+ diminished_sixth: diminished sixth
50
+ augmented_sixth: augmented sixth
51
+ doubly_diminished_sixth: doubly diminished sixth
52
+ doubly_augmented_sixth: doubly augmented sixth
53
+ minor_seventh: minor seventh
54
+ major_seventh: major seventh
55
+ diminished_seventh: diminished seventh
56
+ augmented_seventh: augmented seventh
57
+ doubly_diminished_seventh: doubly diminished seventh
58
+ doubly_augmented_seventh: doubly augmented seventh
59
+ perfect_octave: perfect octave
60
+ diminished_octave: diminished octave
61
+ augmented_octave: augmented octave
62
+ doubly_diminished_octave: doubly diminished octave
63
+ doubly_augmented_octave: doubly augmented octave
64
+ minor_ninth: minor ninth
65
+ major_ninth: major ninth
66
+ diminished_ninth: diminished ninth
67
+ augmented_ninth: augmented ninth
68
+ doubly_diminished_ninth: doubly diminished ninth
69
+ doubly_augmented_ninth: doubly augmented ninth
70
+ minor_tenth: minor tenth
71
+ major_tenth: major tenth
72
+ diminished_tenth: diminished tenth
73
+ augmented_tenth: augmented tenth
74
+ doubly_diminished_tenth: doubly diminished tenth
75
+ doubly_augmented_tenth: doubly augmented tenth
76
+ perfect_eleventh: perfect eleventh
77
+ diminished_eleventh: diminished eleventh
78
+ augmented_eleventh: augmented eleventh
79
+ doubly_diminished_eleventh: doubly diminished eleventh
80
+ doubly_augmented_eleventh: doubly augmented eleventh
81
+ perfect_twelfth: perfect twelfth
82
+ diminished_twelfth: diminished twelfth
83
+ augmented_twelfth: augmented twelfth
84
+ doubly_diminished_twelfth: doubly diminished twelfth
85
+ doubly_augmented_twelfth: doubly augmented twelfth
86
+ minor_thirteenth: minor thirteenth
87
+ major_thirteenth: major thirteenth
88
+ diminished_thirteenth: diminished thirteenth
89
+ augmented_thirteenth: augmented thirteenth
90
+ doubly_diminished_thirteenth: doubly diminished thirteenth
91
+ doubly_augmented_thirteenth: doubly augmented thirteenth
92
+ minor_fourteenth: minor fourteenth
93
+ major_fourteenth: major fourteenth
94
+ diminished_fourteenth: diminished fourteenth
95
+ augmented_fourteenth: augmented fourteenth
96
+ doubly_diminished_fourteenth: doubly diminished fourteenth
97
+ doubly_augmented_fourteenth: doubly augmented fourteenth
98
+ perfect_fifteenth: perfect fifteenth
99
+ diminished_fifteenth: diminished fifteenth
100
+ augmented_fifteenth: augmented fifteenth
101
+ doubly_diminished_fifteenth: doubly diminished fifteenth
102
+ doubly_augmented_fifteenth: doubly augmented fifteenth
103
+ minor_sixteenth: minor sixteenth
104
+ major_sixteenth: major sixteenth
105
+ diminished_sixteenth: diminished sixteenth
106
+ augmented_sixteenth: augmented sixteenth
107
+ doubly_diminished_sixteenth: doubly diminished sixteenth
108
+ doubly_augmented_sixteenth: doubly augmented sixteenth
109
+ minor_seventeenth: minor seventeenth
110
+ major_seventeenth: major seventeenth
111
+ diminished_seventeenth: diminished seventeenth
112
+ augmented_seventeenth: augmented seventeenth
113
+ doubly_diminished_seventeenth: doubly diminished seventeenth
114
+ doubly_augmented_seventeenth: doubly augmented seventeenth
17
115
  clefs:
18
116
  alto_clef: alto clef
19
117
  baritone_c_clef: baritone C-clef
@@ -1,5 +1,11 @@
1
1
  es:
2
2
  head_music:
3
+ alterations:
4
+ sharp: sostenido
5
+ flat: bemol
6
+ natural: becuadro
7
+ double_sharp: doble sostenido
8
+ double_flat: doble bemol
3
9
  chromatic_intervals:
4
10
  perfect_unison: unísono perfecto
5
11
  minor_second: segunda menor
@@ -1,5 +1,11 @@
1
1
  fr:
2
2
  head_music:
3
+ alterations:
4
+ sharp: dièse
5
+ flat: bémol
6
+ natural: bécarre
7
+ double_sharp: double dièse
8
+ double_flat: double bémol
3
9
  chromatic_intervals:
4
10
  perfect_unison: unisson parfait
5
11
  minor_second: seconde mineure
@@ -1,5 +1,11 @@
1
1
  it:
2
2
  head_music:
3
+ alterations:
4
+ sharp: diesis
5
+ flat: bemolle
6
+ natural: bequadro
7
+ double_sharp: doppio diesis
8
+ double_flat: doppio bemolle
3
9
  chromatic_intervals:
4
10
  perfect_unison: unisono perfetto
5
11
  minor_second: seconda minore
@@ -1,5 +1,11 @@
1
1
  ru:
2
2
  head_music:
3
+ alterations:
4
+ sharp: диез
5
+ flat: бемоль
6
+ natural: бекар
7
+ double_sharp: дубль-диез
8
+ double_flat: дубль-бемоль
3
9
  chromatic_intervals:
4
10
  perfect_unison: чистый унисон
5
11
  minor_second: малая секунда
@@ -5,8 +5,9 @@ module HeadMusic::Rudiment; end
5
5
 
6
6
  # An Alteration is a symbol that modifies pitch, such as a sharp, flat, or natural.
7
7
  # In French, sharps and flats in the key signature are called "altérations".
8
- class HeadMusic::Rudiment::Alteration
8
+ class HeadMusic::Rudiment::Alteration < HeadMusic::Rudiment::Base
9
9
  include Comparable
10
+ include HeadMusic::Named
10
11
 
11
12
  attr_reader :identifier, :cents, :musical_symbols
12
13
 
@@ -36,6 +37,9 @@ class HeadMusic::Rudiment::Alteration
36
37
  ].freeze
37
38
 
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? })
42
+ MATCHER = PATTERN
39
43
 
40
44
  def self.all
41
45
  ALTERATION_RECORDS.map { |attributes| new(attributes) }
@@ -45,12 +49,8 @@ class HeadMusic::Rudiment::Alteration
45
49
  @symbols ||= all.map { |alteration| [alteration.ascii, alteration.unicode] }.flatten.reject { |s| s.nil? || s.empty? }
46
50
  end
47
51
 
48
- def self.matcher
49
- @matcher ||= Regexp.new symbols.join("|")
50
- end
51
-
52
52
  def self.symbol?(candidate)
53
- candidate =~ /^(#{matcher})$/
53
+ SYMBOLS.include?(candidate)
54
54
  end
55
55
 
56
56
  def self.get(identifier)
@@ -67,8 +67,16 @@ class HeadMusic::Rudiment::Alteration
67
67
  end
68
68
  end
69
69
 
70
- def name
71
- identifier.to_s.tr("_", " ")
70
+ def self.get_by_name(name)
71
+ all.detect { |alteration| alteration.name == name.to_s }
72
+ end
73
+
74
+ def self.from_pitched_item(input)
75
+ nil
76
+ end
77
+
78
+ def name(locale_code: I18n.locale)
79
+ super || identifier.to_s.tr("_", " ")
72
80
  end
73
81
 
74
82
  def representions
@@ -103,6 +111,13 @@ class HeadMusic::Rudiment::Alteration
103
111
  @identifier = attributes[:identifier]
104
112
  @cents = attributes[:cents]
105
113
  initialize_musical_symbols(attributes[:symbols])
114
+ initialize_localized_names
115
+ end
116
+
117
+ def initialize_localized_names
118
+ # Initialize default English names
119
+ ensure_localized_name(name: identifier.to_s.tr("_", " "), locale_code: :en)
120
+ # Additional localized names will be loaded from locale files
106
121
  end
107
122
 
108
123
  def initialize_musical_symbols(list)
@@ -0,0 +1,9 @@
1
+ module HeadMusic::Rudiment; end
2
+
3
+ class HeadMusic::Rudiment::Base
4
+ private
5
+
6
+ def initialize
7
+ raise NotImplementedError, "Cannot instantiate abstract rudiment base class"
8
+ end
9
+ end
@@ -2,7 +2,7 @@
2
2
  module HeadMusic::Rudiment; end
3
3
 
4
4
  # A chromatic interval is the distance between two pitches measured in half-steps.
5
- class HeadMusic::Rudiment::ChromaticInterval
5
+ class HeadMusic::Rudiment::ChromaticInterval < HeadMusic::Rudiment::Base
6
6
  include Comparable
7
7
  include HeadMusic::Named
8
8
 
@@ -23,11 +23,8 @@ class HeadMusic::Rudiment::ChromaticInterval
23
23
  end
24
24
 
25
25
  def initialize(identifier)
26
- if /^\D/i.match?(identifier.to_s.strip)
27
- candidate = identifier.to_s.downcase.gsub(/\W+/, "_")
28
- semitones = NAMES.index(candidate) || identifier.to_i
29
- end
30
- @semitones = semitones || identifier.to_i
26
+ candidate = HeadMusic::Utilities::HashKey.for(identifier).to_s
27
+ @semitones = NAMES.index(candidate) || identifier.to_i
31
28
  set_name
32
29
  end
33
30
 
@@ -4,7 +4,7 @@ require "yaml"
4
4
  module HeadMusic::Rudiment; end
5
5
 
6
6
  # A clef assigns pitches to the lines and spaces of a staff.
7
- class HeadMusic::Rudiment::Clef
7
+ class HeadMusic::Rudiment::Clef < HeadMusic::Rudiment::Base
8
8
  include HeadMusic::Named
9
9
 
10
10
  RECORDS = YAML.load_file(File.expand_path("clefs.yml", __dir__)).freeze
@@ -1,13 +1,30 @@
1
1
  # A module for music rudiments
2
2
  module HeadMusic::Rudiment; end
3
3
 
4
- # Consonance describes a category or degree of harmonic pleasantness: perfect, imperfect, or dissonant
5
- class HeadMusic::Rudiment::Consonance
6
- LEVELS = %w[perfect imperfect dissonant].freeze
4
+ # Consonance describes a category or degree of harmonic pleasantness
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
+ PERFECT_CONSONANCE = :perfect_consonance
18
+ IMPERFECT_CONSONANCE = :imperfect_consonance
19
+ CONTEXTUAL = :contextual
20
+ MILD_DISSONANCE = :mild_dissonance
21
+ HARSH_DISSONANCE = :harsh_dissonance
22
+ DISSONANCE = :dissonance
7
23
 
8
24
  def self.get(name)
9
25
  @consonances ||= {}
10
- @consonances[name.to_sym] ||= new(name) if LEVELS.include?(name.to_s)
26
+ name_sym = name.to_sym
27
+ @consonances[name_sym] ||= new(name) if LEVELS.include?(name.to_s)
11
28
  end
12
29
 
13
30
  attr_reader :name
@@ -22,6 +39,22 @@ class HeadMusic::Rudiment::Consonance
22
39
  to_s == other.to_s
23
40
  end
24
41
 
42
+ # Check if this represents a consonance (perfect or imperfect)
43
+ def consonant?
44
+ [PERFECT_CONSONANCE, IMPERFECT_CONSONANCE].include?(name)
45
+ end
46
+
47
+ # Check if this represents any form of dissonance
48
+ def dissonant?
49
+ [MILD_DISSONANCE, HARSH_DISSONANCE, DISSONANCE].include?(name)
50
+ end
51
+
52
+ # Contextual is special - neither strictly consonant nor dissonant
53
+ def contextual?
54
+ name == CONTEXTUAL
55
+ end
56
+
57
+ # Predicate methods for each level
25
58
  LEVELS.each do |method_name|
26
59
  define_method(:"#{method_name}?") { to_s == method_name }
27
60
  end
@@ -0,0 +1,25 @@
1
+ # A module for music rudiments
2
+ module HeadMusic::Rudiment; end
3
+
4
+ # Abstract class representing a diatonic tonal context (7-note scale system)
5
+ class HeadMusic::Rudiment::DiatonicContext < HeadMusic::Rudiment::TonalContext
6
+ def scale_type
7
+ raise NotImplementedError, "Subclasses must implement #scale_type"
8
+ end
9
+
10
+ def scale
11
+ @scale ||= HeadMusic::Rudiment::Scale.get(tonic_spelling, scale_type)
12
+ end
13
+
14
+ def key_signature
15
+ @key_signature ||= HeadMusic::Rudiment::KeySignature.from_scale(scale)
16
+ end
17
+
18
+ def relative
19
+ raise NotImplementedError, "Subclasses must implement #relative"
20
+ end
21
+
22
+ def parallel
23
+ raise NotImplementedError, "Subclasses must implement #parallel"
24
+ end
25
+ end