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,188 @@
1
+ # Namespace for instrument definitions, categorization, and configuration
2
+ module HeadMusic::Instruments; end
3
+
4
+ # A musical instrument type representing a catalog entry.
5
+ # An instrument type defines the base characteristics and available variants for an instrument.
6
+ # Attributes:
7
+ # name_key: the name of the instrument type
8
+ # alias_name_keys: an array of alternative names for the instrument type
9
+ # orchestra_section_key: the section of the orchestra (e.g. "strings")
10
+ # family_key: the key for the family of the instrument (e.g. "saxophone")
11
+ # classification_keys: an array of classification_keys
12
+ # default_clefs: the default clef or system of clefs for the instrument type
13
+ # - [treble] for instruments that use the treble clef
14
+ # - [treble, bass] for instruments that use the grand staff
15
+ # variants:
16
+ # a hash of default and alternative pitch designations
17
+ # Associations:
18
+ # family: the family of the instrument (e.g. "saxophone")
19
+ # orchestra_section: the section of the orchestra (e.g. "strings")
20
+ class HeadMusic::Instruments::InstrumentType
21
+ include HeadMusic::Named
22
+
23
+ INSTRUMENTS = YAML.load_file(File.expand_path("instruments.yml", __dir__)).freeze
24
+
25
+ def self.get(name)
26
+ get_by_name(name)
27
+ end
28
+
29
+ def self.all
30
+ HeadMusic::Instruments::InstrumentFamily.all
31
+ @all ||=
32
+ INSTRUMENTS.map { |key, _data| get(key) }.sort_by { |instrument| instrument.name.downcase }
33
+ end
34
+
35
+ attr_reader(
36
+ :name_key, :alias_name_keys,
37
+ :family_key, :orchestra_section_key,
38
+ :variants, :classification_keys
39
+ )
40
+
41
+ def ==(other)
42
+ to_s == other.to_s
43
+ end
44
+
45
+ def translation(locale = :en)
46
+ return name unless name_key
47
+
48
+ I18n.translate(name_key, scope: %i[head_music instruments], locale: locale, default: name)
49
+ end
50
+
51
+ def family
52
+ return unless family_key
53
+
54
+ HeadMusic::Instruments::InstrumentFamily.get(family_key)
55
+ end
56
+
57
+ # Returns true if the instrument sounds at a different pitch than written.
58
+ def transposing?
59
+ default_sounding_transposition != 0
60
+ end
61
+
62
+ # Returns true if the instrument sounds at a different register than written.
63
+ def transposing_at_the_octave?
64
+ transposing? && default_sounding_transposition % 12 == 0
65
+ end
66
+
67
+ def single_staff?
68
+ default_staves.length == 1
69
+ end
70
+
71
+ def multiple_staves?
72
+ default_staves.length > 1
73
+ end
74
+
75
+ def pitched?
76
+ return false if default_clefs.compact.uniq == [HeadMusic::Rudiment::Clef.get("neutral_clef")]
77
+
78
+ default_clefs.any?
79
+ end
80
+
81
+ def default_variant
82
+ variants&.find(&:default?) || variants&.first
83
+ end
84
+
85
+ def default_instrument
86
+ @default_instrument ||= HeadMusic::Instruments::Instrument.new(self, default_variant)
87
+ end
88
+
89
+ def default_staff_scheme
90
+ default_variant&.default_staff_scheme
91
+ end
92
+
93
+ def default_staves
94
+ default_staff_scheme&.staves || []
95
+ end
96
+
97
+ def default_clefs
98
+ default_staves&.map(&:clef) || []
99
+ end
100
+
101
+ def default_sounding_transposition
102
+ default_staves&.first&.sounding_transposition || 0
103
+ end
104
+
105
+ private_class_method :new
106
+
107
+ private
108
+
109
+ def initialize(name)
110
+ record = record_for_name(name)
111
+ if record
112
+ initialize_data_from_record(record)
113
+ else
114
+ self.name = name.to_s
115
+ end
116
+ end
117
+
118
+ def record_for_name(name)
119
+ record_for_key(HeadMusic::Utilities::HashKey.for(name)) ||
120
+ record_for_key(key_for_name(name)) ||
121
+ record_for_alias(name)
122
+ end
123
+
124
+ def key_for_name(name)
125
+ INSTRUMENTS.each do |key, _data|
126
+ I18n.config.available_locales.each do |locale|
127
+ translation = I18n.t("head_music.instruments.#{key}", locale: locale)
128
+ return key if translation.downcase == name.downcase
129
+ end
130
+ end
131
+ nil
132
+ end
133
+
134
+ def record_for_key(key)
135
+ INSTRUMENTS.each do |name_key, data|
136
+ return data.merge!("name_key" => name_key) if name_key.to_s == key.to_s
137
+ end
138
+ nil
139
+ end
140
+
141
+ def record_for_alias(name)
142
+ normalized_name = HeadMusic::Utilities::HashKey.for(name).to_s
143
+ INSTRUMENTS.each do |name_key, data|
144
+ data["alias_name_keys"]&.each do |alias_key|
145
+ return data.merge!("name_key" => name_key) if HeadMusic::Utilities::HashKey.for(alias_key).to_s == normalized_name
146
+ end
147
+ end
148
+ nil
149
+ end
150
+
151
+ def initialize_data_from_record(record)
152
+ initialize_family(record)
153
+ inherit_family_attributes(record)
154
+ initialize_names(record)
155
+ initialize_attributes(record)
156
+ end
157
+
158
+ def initialize_family(record)
159
+ @family_key = record["family_key"]
160
+ @family = HeadMusic::Instruments::InstrumentFamily.get(family_key)
161
+ end
162
+
163
+ def inherit_family_attributes(record)
164
+ return unless family
165
+
166
+ @orchestra_section_key = family.orchestra_section_key
167
+ @classification_keys = family.classification_keys || []
168
+ end
169
+
170
+ def initialize_names(record)
171
+ @name_key = record["name_key"].to_sym
172
+ self.name = I18n.translate(name_key, scope: "head_music.instruments", locale: "en", default: inferred_name)
173
+ @alias_name_keys = record["alias_name_keys"] || []
174
+ end
175
+
176
+ def initialize_attributes(record)
177
+ @orchestra_section_key ||= record["orchestra_section_key"]
178
+ @classification_keys = [@classification_keys, record["classification_keys"]].flatten.compact.uniq
179
+ @variants =
180
+ (record["variants"] || {}).map do |key, attributes|
181
+ HeadMusic::Instruments::Variant.new(key, attributes)
182
+ end
183
+ end
184
+
185
+ def inferred_name
186
+ name_key.to_s.tr("_", " ")
187
+ end
188
+ end