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.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +9 -3
- data/CHANGELOG.md +18 -0
- data/CLAUDE.md +35 -15
- data/Gemfile +7 -1
- data/Gemfile.lock +91 -3
- data/README.md +18 -0
- data/Rakefile +7 -2
- data/head_music.gemspec +1 -1
- data/lib/head_music/analysis/dyad.rb +229 -0
- data/lib/head_music/analysis/melodic_interval.rb +1 -1
- data/lib/head_music/analysis/pitch_class_set.rb +111 -14
- data/lib/head_music/analysis/{pitch_set.rb → pitch_collection.rb} +11 -5
- data/lib/head_music/analysis/sonority.rb +50 -12
- data/lib/head_music/content/cantus_firmus_examples.rb +58 -0
- data/lib/head_music/content/staff.rb +1 -1
- data/lib/head_music/content/voice.rb +1 -1
- data/lib/head_music/instruments/alternate_tuning.rb +102 -0
- data/lib/head_music/instruments/alternate_tunings.yml +78 -0
- data/lib/head_music/instruments/instrument.rb +251 -82
- data/lib/head_music/instruments/instrument_configuration.rb +66 -0
- data/lib/head_music/instruments/instrument_configuration_option.rb +38 -0
- data/lib/head_music/instruments/instrument_configurations.yml +288 -0
- data/lib/head_music/instruments/instrument_families.yml +77 -0
- data/lib/head_music/instruments/instrument_family.rb +3 -4
- data/lib/head_music/instruments/instruments.yml +795 -965
- data/lib/head_music/instruments/playing_technique.rb +75 -0
- data/lib/head_music/instruments/playing_techniques.yml +826 -0
- data/lib/head_music/instruments/score_order.rb +2 -5
- data/lib/head_music/instruments/staff.rb +61 -1
- data/lib/head_music/instruments/staff_scheme.rb +6 -4
- data/lib/head_music/instruments/stringing.rb +115 -0
- data/lib/head_music/instruments/stringing_course.rb +58 -0
- data/lib/head_music/instruments/stringings.yml +168 -0
- data/lib/head_music/instruments/variant.rb +0 -1
- data/lib/head_music/locales/de.yml +23 -0
- data/lib/head_music/locales/en.yml +100 -0
- data/lib/head_music/locales/es.yml +23 -0
- data/lib/head_music/locales/fr.yml +23 -0
- data/lib/head_music/locales/it.yml +23 -0
- data/lib/head_music/locales/ru.yml +23 -0
- data/lib/head_music/{rudiment → notation}/musical_symbol.rb +3 -3
- data/lib/head_music/notation/staff_mapping.rb +70 -0
- data/lib/head_music/notation/staff_position.rb +62 -0
- data/lib/head_music/notation.rb +7 -0
- data/lib/head_music/rudiment/alteration.rb +17 -47
- data/lib/head_music/rudiment/alterations.yml +32 -0
- data/lib/head_music/rudiment/chromatic_interval.rb +1 -1
- data/lib/head_music/rudiment/clef.rb +1 -1
- data/lib/head_music/rudiment/consonance.rb +14 -13
- data/lib/head_music/rudiment/key_signature.rb +0 -26
- data/lib/head_music/rudiment/rhythmic_unit/parser.rb +2 -2
- data/lib/head_music/rudiment/rhythmic_value/parser.rb +1 -1
- data/lib/head_music/rudiment/rhythmic_value.rb +1 -1
- data/lib/head_music/rudiment/spelling.rb +3 -0
- data/lib/head_music/rudiment/tempo.rb +1 -1
- data/lib/head_music/rudiment/tuning/just_intonation.rb +0 -39
- data/lib/head_music/rudiment/tuning/meantone.rb +0 -39
- data/lib/head_music/rudiment/tuning/pythagorean.rb +0 -39
- data/lib/head_music/rudiment/tuning.rb +20 -0
- data/lib/head_music/style/guidelines/consonant_climax.rb +2 -2
- data/lib/head_music/style/modern_tradition.rb +8 -11
- data/lib/head_music/style/tradition.rb +1 -1
- data/lib/head_music/time/clock_position.rb +84 -0
- data/lib/head_music/time/conductor.rb +264 -0
- data/lib/head_music/time/meter_event.rb +37 -0
- data/lib/head_music/time/meter_map.rb +173 -0
- data/lib/head_music/time/musical_position.rb +188 -0
- data/lib/head_music/time/smpte_timecode.rb +164 -0
- data/lib/head_music/time/tempo_event.rb +40 -0
- data/lib/head_music/time/tempo_map.rb +187 -0
- data/lib/head_music/time.rb +32 -0
- data/lib/head_music/utilities/case.rb +27 -0
- data/lib/head_music/utilities/hash_key.rb +1 -1
- data/lib/head_music/version.rb +1 -1
- data/lib/head_music.rb +42 -13
- data/user_stories/backlog/notation-style.md +183 -0
- data/user_stories/{todo → backlog}/organizing-content.md +9 -1
- data/user_stories/done/consonance-dissonance-classification.md +117 -0
- data/user_stories/{todo → done}/dyad-analysis.md +4 -6
- data/user_stories/done/expand-playing-techniques.md +38 -0
- data/user_stories/{active → done}/handle-time.rb +5 -19
- data/user_stories/done/instrument-architecture.md +238 -0
- data/user_stories/done/move-musical-symbol-to-notation.md +161 -0
- data/user_stories/done/move-staff-mapping-to-notation.md +158 -0
- data/user_stories/done/move-staff-position-to-notation.md +141 -0
- data/user_stories/done/notation-module-foundation.md +102 -0
- data/user_stories/done/percussion_set.md +260 -0
- data/user_stories/{todo → done}/pitch-class-set-analysis.md +0 -40
- data/user_stories/done/sonority-identification.md +37 -0
- data/user_stories/done/string-pitches.md +41 -0
- data/user_stories/epics/notation-module.md +135 -0
- data/user_stories/{todo → visioning}/agentic-daw.md +0 -1
- metadata +56 -20
- data/check_instrument_consistency.rb +0 -0
- data/lib/head_music/instruments/instrument_type.rb +0 -188
- data/test_translations.rb +0 -15
- data/user_stories/todo/consonance-dissonance-classification.md +0 -57
- data/user_stories/todo/material-and-scores.md +0 -10
- data/user_stories/todo/percussion_set.md +0 -1
- data/user_stories/todo/pitch-set-classification.md +0 -72
- data/user_stories/todo/sonority-identification.md +0 -67
- /data/user_stories/{active → done}/handle-time.md +0 -0
|
@@ -1,53 +1,136 @@
|
|
|
1
|
-
# Namespace for instrument definitions, categorization, and configuration
|
|
2
1
|
module HeadMusic::Instruments; end
|
|
3
2
|
|
|
4
|
-
# A
|
|
5
|
-
#
|
|
3
|
+
# A musical instrument with parent-based inheritance.
|
|
4
|
+
#
|
|
5
|
+
# Instruments can inherit from parent instruments, allowing for a clean
|
|
6
|
+
# hierarchy where child instruments override specific attributes while
|
|
7
|
+
# inheriting others from their parents.
|
|
6
8
|
#
|
|
7
9
|
# Examples:
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
10
|
+
# trumpet = HeadMusic::Instruments::Instrument.get("trumpet")
|
|
11
|
+
# clarinet_in_a = HeadMusic::Instruments::Instrument.get("clarinet_in_a")
|
|
12
|
+
# clarinet_in_a.parent # => clarinet
|
|
13
|
+
# clarinet_in_a.pitch_key # => "a" (own attribute)
|
|
14
|
+
# clarinet_in_a.family_key # => "clarinet" (inherited from parent)
|
|
11
15
|
#
|
|
12
|
-
# Attributes
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
16
|
+
# Attributes:
|
|
17
|
+
# name_key: the primary identifier for the instrument
|
|
18
|
+
# parent_key: optional key referencing the parent instrument
|
|
19
|
+
# family_key: the instrument family (e.g., "clarinet", "trumpet")
|
|
20
|
+
# pitch_key: the pitch designation (e.g., "b_flat", "a", "c")
|
|
21
|
+
# alias_name_keys: alternative names for the instrument
|
|
22
|
+
# range_categories: size/range classifications
|
|
23
|
+
# staff_schemes: notation schemes (to be moved to NotationStyle later)
|
|
17
24
|
class HeadMusic::Instruments::Instrument
|
|
18
25
|
include HeadMusic::Named
|
|
19
26
|
|
|
20
|
-
|
|
27
|
+
INSTRUMENTS = YAML.load_file(File.expand_path("instruments.yml", __dir__)).freeze
|
|
28
|
+
|
|
29
|
+
attr_reader :name_key, :parent_key, :alias_name_keys, :range_categories, :staff_schemes_data
|
|
30
|
+
|
|
31
|
+
class << self
|
|
32
|
+
# Factory method to get an Instrument instance
|
|
33
|
+
# @param name [String, Symbol] instrument name (e.g., "clarinet", "clarinet_in_a")
|
|
34
|
+
# @param variant_key [String, Symbol, nil] DEPRECATED: variant key (for backward compatibility)
|
|
35
|
+
# @return [Instrument, nil] instrument instance or nil if not found
|
|
36
|
+
def get(name, variant_key = nil)
|
|
37
|
+
return name if name.is_a?(self)
|
|
38
|
+
|
|
39
|
+
# Handle two-argument form for backward compatibility
|
|
40
|
+
if variant_key
|
|
41
|
+
combined_name = "#{name}_#{variant_key}"
|
|
42
|
+
result = find_valid_instrument(combined_name) || find_valid_instrument(name.to_s)
|
|
43
|
+
else
|
|
44
|
+
result = find_valid_instrument(name.to_s) || find_valid_instrument(normalize_variant_name(name))
|
|
45
|
+
end
|
|
21
46
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
47
|
+
result
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def find_valid_instrument(name)
|
|
51
|
+
instrument = get_by_name(name)
|
|
52
|
+
instrument&.name_key ? instrument : nil
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def all
|
|
56
|
+
HeadMusic::Instruments::InstrumentFamily.all # Ensure families are loaded first
|
|
57
|
+
INSTRUMENTS.map { |key, _data| get(key) }
|
|
58
|
+
@all ||= @instances.values.compact.sort_by { |instrument| instrument.name.downcase }
|
|
59
|
+
end
|
|
28
60
|
|
|
29
|
-
|
|
30
|
-
variant_key ||= parsed_variant_key
|
|
61
|
+
private
|
|
31
62
|
|
|
32
|
-
|
|
33
|
-
|
|
63
|
+
# Convert shorthand variant names to full form
|
|
64
|
+
# e.g., "trumpet_in_eb" -> "trumpet_in_e_flat"
|
|
65
|
+
# e.g., "clarinet_in_bb" -> "clarinet_in_b_flat"
|
|
66
|
+
def normalize_variant_name(name)
|
|
67
|
+
name_str = name.to_s
|
|
34
68
|
|
|
35
|
-
|
|
36
|
-
|
|
69
|
+
# Match patterns like "_in_eb" or "_in_bb" at the end (flat)
|
|
70
|
+
flat_pattern = /^(.+)_in_([a-g])b$/i
|
|
71
|
+
sharp_pattern = %r{^(.+)_in_([a-g])\#$}i
|
|
72
|
+
|
|
73
|
+
if name_str =~ flat_pattern
|
|
74
|
+
instrument = Regexp.last_match(1)
|
|
75
|
+
note = Regexp.last_match(2).downcase
|
|
76
|
+
"#{instrument}_in_#{note}_flat"
|
|
77
|
+
elsif name_str =~ sharp_pattern
|
|
78
|
+
instrument = Regexp.last_match(1)
|
|
79
|
+
note = Regexp.last_match(2).downcase
|
|
80
|
+
"#{instrument}_in_#{note}_sharp"
|
|
81
|
+
else
|
|
82
|
+
name_str
|
|
83
|
+
end
|
|
84
|
+
end
|
|
37
85
|
end
|
|
38
86
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
87
|
+
# Parent instrument (for inheritance)
|
|
88
|
+
def parent
|
|
89
|
+
return nil unless parent_key
|
|
90
|
+
|
|
91
|
+
@parent ||= self.class.get(parent_key)
|
|
43
92
|
end
|
|
44
93
|
|
|
45
|
-
#
|
|
46
|
-
|
|
47
|
-
|
|
94
|
+
# Attributes with parent chain resolution
|
|
95
|
+
|
|
96
|
+
def family_key
|
|
97
|
+
@family_key || parent&.family_key
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def pitch_key
|
|
101
|
+
@pitch_key || parent&.pitch_key
|
|
102
|
+
end
|
|
48
103
|
|
|
49
|
-
|
|
50
|
-
|
|
104
|
+
def family
|
|
105
|
+
return unless family_key
|
|
106
|
+
|
|
107
|
+
HeadMusic::Instruments::InstrumentFamily.get(family_key)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def orchestra_section_key
|
|
111
|
+
family&.orchestra_section_key
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def classification_keys
|
|
115
|
+
family&.classification_keys || []
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Pitch designation as a Spelling object (for backward compatibility)
|
|
119
|
+
def pitch_designation
|
|
120
|
+
return nil unless pitch_key
|
|
121
|
+
|
|
122
|
+
@pitch_designation ||= HeadMusic::Rudiment::Spelling.get(pitch_key_to_designation)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Staff schemes (notation concern - kept for backward compatibility)
|
|
126
|
+
def staff_schemes
|
|
127
|
+
@staff_schemes ||= build_staff_schemes
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def default_staff_scheme
|
|
131
|
+
@default_staff_scheme ||=
|
|
132
|
+
staff_schemes.find(&:default?) || staff_schemes.first
|
|
133
|
+
end
|
|
51
134
|
|
|
52
135
|
def default_staves
|
|
53
136
|
default_staff_scheme&.staves || []
|
|
@@ -85,77 +168,163 @@ class HeadMusic::Instruments::Instrument
|
|
|
85
168
|
default_clefs.any?
|
|
86
169
|
end
|
|
87
170
|
|
|
171
|
+
def translation(locale = :en)
|
|
172
|
+
return name unless name_key
|
|
173
|
+
|
|
174
|
+
I18n.translate(name_key, scope: %i[head_music instruments], locale: locale, default: name)
|
|
175
|
+
end
|
|
176
|
+
|
|
88
177
|
def ==(other)
|
|
89
178
|
return false unless other.is_a?(self.class)
|
|
90
179
|
|
|
91
|
-
|
|
180
|
+
name_key == other.name_key
|
|
92
181
|
end
|
|
93
182
|
|
|
94
183
|
def to_s
|
|
95
184
|
name
|
|
96
185
|
end
|
|
97
186
|
|
|
187
|
+
# For backward compatibility with code that expects variants
|
|
188
|
+
def variants
|
|
189
|
+
[]
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def default_variant
|
|
193
|
+
nil
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# Collect all instrument_configurations from self and ancestors
|
|
197
|
+
def instrument_configurations
|
|
198
|
+
own_configs = HeadMusic::Instruments::InstrumentConfiguration.for_instrument(name_key)
|
|
199
|
+
parent_configs = parent&.instrument_configurations || []
|
|
200
|
+
own_configs + parent_configs
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def stringing
|
|
204
|
+
@stringing ||= HeadMusic::Instruments::Stringing.for_instrument(self) || parent&.stringing
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def alternate_tunings
|
|
208
|
+
own_tunings = HeadMusic::Instruments::AlternateTuning.for_instrument(name_key)
|
|
209
|
+
return own_tunings if own_tunings.any?
|
|
210
|
+
|
|
211
|
+
parent&.alternate_tunings || []
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
private_class_method :new
|
|
215
|
+
|
|
98
216
|
private
|
|
99
217
|
|
|
100
|
-
def
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
pitch_name = format_pitch_name(pitch_designation)
|
|
105
|
-
self.name = "#{instrument_type.name} in #{pitch_name}"
|
|
218
|
+
def initialize(name)
|
|
219
|
+
record = record_for_name(name)
|
|
220
|
+
if record
|
|
221
|
+
initialize_data_from_record(record)
|
|
106
222
|
else
|
|
107
|
-
|
|
108
|
-
|
|
223
|
+
# Mark as invalid - will be filtered out by get_by_name
|
|
224
|
+
@name_key = nil
|
|
225
|
+
self.name = name.to_s
|
|
109
226
|
end
|
|
110
227
|
end
|
|
111
228
|
|
|
112
|
-
def
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
end
|
|
117
|
-
|
|
118
|
-
def
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
variant_key = :"in_#{variant_note}"
|
|
229
|
+
def record_for_name(name)
|
|
230
|
+
record_for_key(HeadMusic::Utilities::HashKey.for(name)) ||
|
|
231
|
+
record_for_key(key_for_name(name)) ||
|
|
232
|
+
record_for_alias(name)
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def key_for_name(name)
|
|
236
|
+
INSTRUMENTS.each do |key, _data|
|
|
237
|
+
I18n.config.available_locales.each do |locale|
|
|
238
|
+
translation = I18n.t("head_music.instruments.#{key}", locale: locale)
|
|
239
|
+
return key if translation.downcase == name.downcase
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
nil
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def record_for_key(key)
|
|
246
|
+
INSTRUMENTS.each do |name_key, data|
|
|
247
|
+
return data.merge("name_key" => name_key) if name_key.to_s == key.to_s
|
|
248
|
+
end
|
|
249
|
+
nil
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def record_for_alias(name)
|
|
253
|
+
normalized_name = HeadMusic::Utilities::HashKey.for(name).to_s
|
|
254
|
+
INSTRUMENTS.each do |name_key, data|
|
|
255
|
+
data["alias_name_keys"]&.each do |alias_key|
|
|
256
|
+
return data.merge("name_key" => name_key) if HeadMusic::Utilities::HashKey.for(alias_key).to_s == normalized_name
|
|
141
257
|
end
|
|
142
|
-
|
|
258
|
+
end
|
|
259
|
+
nil
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
def initialize_data_from_record(record)
|
|
263
|
+
@name_key = record["name_key"].to_sym
|
|
264
|
+
@parent_key = record["parent_key"]&.to_sym
|
|
265
|
+
@family_key = record["family_key"]
|
|
266
|
+
@pitch_key = record["pitch_key"]
|
|
267
|
+
@alias_name_keys = record["alias_name_keys"] || []
|
|
268
|
+
@range_categories = record["range_categories"] || []
|
|
269
|
+
@staff_schemes_data = record["staff_schemes"] || {}
|
|
270
|
+
|
|
271
|
+
initialize_name
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
def initialize_name
|
|
275
|
+
# Try to get a translation first
|
|
276
|
+
base_name = I18n.translate(name_key, scope: "head_music.instruments", locale: "en", default: nil)
|
|
277
|
+
|
|
278
|
+
if base_name
|
|
279
|
+
# Use the translation as-is
|
|
280
|
+
self.name = base_name
|
|
281
|
+
elsif parent_key && pitch_key
|
|
282
|
+
# Build name from parent + pitch for child instruments
|
|
283
|
+
pitch_name = format_pitch_name(pitch_key_to_designation)
|
|
284
|
+
self.name = "#{parent_translation} in #{pitch_name}"
|
|
143
285
|
else
|
|
144
|
-
|
|
286
|
+
# Fall back to inferred name
|
|
287
|
+
self.name = inferred_name
|
|
145
288
|
end
|
|
146
289
|
end
|
|
147
290
|
|
|
148
|
-
def
|
|
149
|
-
return
|
|
291
|
+
def parent_translation
|
|
292
|
+
return nil unless parent_key
|
|
293
|
+
|
|
294
|
+
I18n.translate(parent_key, scope: "head_music.instruments", locale: "en", default: parent_key.to_s.tr("_", " "))
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
def inferred_name
|
|
298
|
+
name_key.to_s.tr("_", " ")
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
def format_pitch_name(pitch_designation)
|
|
302
|
+
pitch_designation.to_s.tr("b", "♭").tr("#", "♯")
|
|
303
|
+
end
|
|
150
304
|
|
|
151
|
-
|
|
152
|
-
|
|
305
|
+
# Convert pitch_key (e.g., "b_flat") to designation format (e.g., "Bb")
|
|
306
|
+
def pitch_key_to_designation
|
|
307
|
+
return nil unless pitch_key
|
|
153
308
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
309
|
+
key = pitch_key.to_s
|
|
310
|
+
if key.end_with?("_flat")
|
|
311
|
+
"#{key[0].upcase}b"
|
|
312
|
+
elsif key.end_with?("_sharp")
|
|
313
|
+
"#{key[0].upcase}#"
|
|
314
|
+
else
|
|
315
|
+
key.upcase
|
|
316
|
+
end
|
|
158
317
|
end
|
|
159
318
|
|
|
160
|
-
|
|
319
|
+
def build_staff_schemes
|
|
320
|
+
return parent&.staff_schemes || [] if staff_schemes_data.empty?
|
|
321
|
+
|
|
322
|
+
staff_schemes_data.map do |key, list|
|
|
323
|
+
HeadMusic::Instruments::StaffScheme.new(
|
|
324
|
+
key: key,
|
|
325
|
+
instrument: self,
|
|
326
|
+
list: list
|
|
327
|
+
)
|
|
328
|
+
end
|
|
329
|
+
end
|
|
161
330
|
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
module HeadMusic::Instruments; end
|
|
2
|
+
|
|
3
|
+
# A configurable aspect of an instrument, such as a leadpipe, mute, or attachment.
|
|
4
|
+
#
|
|
5
|
+
# Examples:
|
|
6
|
+
# - Piccolo trumpet "leadpipe" configuration with options: b_flat (default), a
|
|
7
|
+
# - Trumpet "mute" configuration with options: open (default), straight, cup, harmon
|
|
8
|
+
# - Bass trombone "f_attachment" with options: disengaged (default), engaged
|
|
9
|
+
class HeadMusic::Instruments::InstrumentConfiguration
|
|
10
|
+
CONFIGURATIONS = YAML.load_file(File.expand_path("instrument_configurations.yml", __dir__)).freeze
|
|
11
|
+
|
|
12
|
+
attr_reader :name_key, :instrument_key, :options
|
|
13
|
+
|
|
14
|
+
class << self
|
|
15
|
+
def for_instrument(instrument_key)
|
|
16
|
+
instrument_key = instrument_key.to_s
|
|
17
|
+
return [] unless CONFIGURATIONS.key?(instrument_key)
|
|
18
|
+
|
|
19
|
+
CONFIGURATIONS[instrument_key].map do |config_name, config_data|
|
|
20
|
+
new(
|
|
21
|
+
name_key: config_name,
|
|
22
|
+
instrument_key: instrument_key,
|
|
23
|
+
options_data: config_data["options"] || {}
|
|
24
|
+
)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def initialize(name_key:, instrument_key:, options_data: {})
|
|
30
|
+
@name_key = name_key.to_sym
|
|
31
|
+
@instrument_key = instrument_key.to_sym
|
|
32
|
+
@options = build_options(options_data)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def default_option
|
|
36
|
+
@default_option ||= options.find(&:default?) || options.first
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def option(option_key)
|
|
40
|
+
options.find { |opt| opt.name_key == option_key.to_sym }
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def ==(other)
|
|
44
|
+
return false unless other.is_a?(self.class)
|
|
45
|
+
|
|
46
|
+
name_key == other.name_key && instrument_key == other.instrument_key
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def to_s
|
|
50
|
+
name_key.to_s
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
def build_options(options_data)
|
|
56
|
+
options_data.map do |option_name, option_attrs|
|
|
57
|
+
attrs = option_attrs || {}
|
|
58
|
+
HeadMusic::Instruments::InstrumentConfigurationOption.new(
|
|
59
|
+
name_key: option_name,
|
|
60
|
+
default: attrs["default"],
|
|
61
|
+
transposition_semitones: attrs["transposition_semitones"],
|
|
62
|
+
lowest_pitch_semitones: attrs["lowest_pitch_semitones"]
|
|
63
|
+
)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
module HeadMusic::Instruments; end
|
|
2
|
+
|
|
3
|
+
# An option for an instrument configuration.
|
|
4
|
+
#
|
|
5
|
+
# Examples:
|
|
6
|
+
# - Piccolo trumpet leadpipe: "a" option with transposition_semitones: -1
|
|
7
|
+
# - Bass trombone F attachment: "engaged" option with lowest_pitch_semitones: -6
|
|
8
|
+
# - Trumpet mute: "straight", "cup", "harmon" options (no pitch effects)
|
|
9
|
+
class HeadMusic::Instruments::InstrumentConfigurationOption
|
|
10
|
+
attr_reader :name_key, :default, :transposition_semitones, :lowest_pitch_semitones
|
|
11
|
+
|
|
12
|
+
def initialize(name_key:, default: false, transposition_semitones: nil, lowest_pitch_semitones: nil)
|
|
13
|
+
@name_key = name_key.to_sym
|
|
14
|
+
@default = default
|
|
15
|
+
@transposition_semitones = transposition_semitones
|
|
16
|
+
@lowest_pitch_semitones = lowest_pitch_semitones
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def default?
|
|
20
|
+
@default == true
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def affects_transposition?
|
|
24
|
+
!transposition_semitones.nil?
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def affects_range?
|
|
28
|
+
!lowest_pitch_semitones.nil?
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def ==(other)
|
|
32
|
+
to_s == other.to_s
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def to_s
|
|
36
|
+
name_key.to_s
|
|
37
|
+
end
|
|
38
|
+
end
|