Class: HeadMusic::Instruments::Instrument

Inherits:
Object
  • Object
show all
Includes:
Named
Defined in:
lib/head_music/instruments/instrument.rb

Overview

A musical instrument with parent-based inheritance.

Instruments can inherit from parent instruments, allowing for a clean hierarchy where child instruments override specific attributes while inheriting others from their parents.

Examples: trumpet = HeadMusic::Instruments::Instrument.get("trumpet") clarinet_in_a = HeadMusic::Instruments::Instrument.get("clarinet_in_a") clarinet_in_a.parent # => clarinet clarinet_in_a.pitch_key # => "a" (own attribute) clarinet_in_a.family_key # => "clarinet" (inherited from parent)

Attributes: name_key: the primary identifier for the instrument parent_key: optional key referencing the parent instrument family_key: the instrument family (e.g., "clarinet", "trumpet") pitch_key: the pitch designation (e.g., "b_flat", "a", "c") alias_name_keys: alternative names for the instrument range_categories: size/range classifications staff_schemes: notation schemes (to be moved to NotationStyle later)

Constant Summary collapse

INSTRUMENTS =
YAML.load_file(File.expand_path("instruments.yml", __dir__)).freeze
VARIANT_PATTERN =

Convert shorthand variant names to full form e.g., "trumpet_in_eb" -> "trumpet_in_e_flat" e.g., "clarinet_in_bb" -> "clarinet_in_b_flat"

/^(.+)_in_([a-g])([b#])$/i

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(name) ⇒ Instrument (private)

Returns a new instance of Instrument.



206
207
208
209
210
211
212
213
214
215
# File 'lib/head_music/instruments/instrument.rb', line 206

def initialize(name)
  record = record_for_name(name)
  if record
    initialize_data_from_record(record)
  else
    # Mark as invalid - will be filtered out by get_by_name
    @name_key = nil
    self.name = name.to_s
  end
end

Instance Attribute Details

#alias_name_keysObject (readonly) Originally defined in module Named

Returns the value of attribute alias_name_keys.

#alias_name_keysObject (readonly)

Returns the value of attribute alias_name_keys.



30
31
32
# File 'lib/head_music/instruments/instrument.rb', line 30

def alias_name_keys
  @alias_name_keys
end

#name_keyObject (readonly)

Returns the value of attribute name_key.



30
31
32
# File 'lib/head_music/instruments/instrument.rb', line 30

def name_key
  @name_key
end

#name_keyObject (readonly) Originally defined in module Named

Returns the value of attribute name_key.

#parent_keyObject (readonly)

Returns the value of attribute parent_key.



30
31
32
# File 'lib/head_music/instruments/instrument.rb', line 30

def parent_key
  @parent_key
end

#range_categoriesObject (readonly)

Returns the value of attribute range_categories.



30
31
32
# File 'lib/head_music/instruments/instrument.rb', line 30

def range_categories
  @range_categories
end

#staff_schemes_dataObject (readonly)

Returns the value of attribute staff_schemes_data.



30
31
32
# File 'lib/head_music/instruments/instrument.rb', line 30

def staff_schemes_data
  @staff_schemes_data
end

Class Method Details

.allObject



53
54
55
56
57
# File 'lib/head_music/instruments/instrument.rb', line 53

def all
  HeadMusic::Instruments::InstrumentFamily.all # Ensure families are loaded first
  INSTRUMENTS.map { |key, _data| get(key) }
  @all ||= @instances.values.compact.sort_by { |instrument| instrument.name.downcase }
end

.find_valid_instrument(name) ⇒ Object



48
49
50
51
# File 'lib/head_music/instruments/instrument.rb', line 48

def find_valid_instrument(name)
  instrument = get_by_name(name)
  instrument&.name_key ? instrument : nil
end

.get(name, variant_key = nil) ⇒ Instrument?

Factory method to get an Instrument instance

Parameters:

  • name (String, Symbol)

    instrument name (e.g., "clarinet", "clarinet_in_a")

  • variant_key (String, Symbol, nil) (defaults to: nil)

    DEPRECATED: variant key (for backward compatibility)

Returns:

  • (Instrument, nil)

    instrument instance or nil if not found



37
38
39
40
41
42
43
44
45
46
# File 'lib/head_music/instruments/instrument.rb', line 37

def get(name, variant_key = nil)
  return name if name.is_a?(self)

  name_str = name.to_s
  if variant_key
    find_valid_instrument("#{name_str}_#{variant_key}") || find_valid_instrument(name_str)
  else
    find_valid_instrument(name_str) || find_valid_instrument(normalize_variant_name(name_str))
  end
end

.normalize_variant_name(name_str) ⇒ Object (private)



66
67
68
69
70
71
72
# File 'lib/head_music/instruments/instrument.rb', line 66

def normalize_variant_name(name_str)
  match = VARIANT_PATTERN.match(name_str.to_s)
  return name_str.to_s unless match

  suffix = (match[3] == "b") ? "flat" : "sharp"
  "#{match[1]}_in_#{match[2].downcase}_#{suffix}"
end

Instance Method Details

#==(other) ⇒ Object



165
166
167
168
169
# File 'lib/head_music/instruments/instrument.rb', line 165

def ==(other)
  return false unless other.is_a?(self.class)

  name_key == other.name_key
end

#alternate_tuningsObject



195
196
197
198
199
200
# File 'lib/head_music/instruments/instrument.rb', line 195

def alternate_tunings
  own_tunings = HeadMusic::Instruments::AlternateTuning.for_instrument(name_key)
  return own_tunings if own_tunings.any?

  parent&.alternate_tunings || []
end

#build_staff_schemesObject (private)



308
309
310
311
312
313
314
315
316
317
318
# File 'lib/head_music/instruments/instrument.rb', line 308

def build_staff_schemes
  return parent&.staff_schemes || [] if staff_schemes_data.empty?

  staff_schemes_data.map do |key, list|
    HeadMusic::Instruments::StaffScheme.new(
      key: key,
      instrument: self,
      list: list
    )
  end
end

#classification_keysObject



102
103
104
# File 'lib/head_music/instruments/instrument.rb', line 102

def classification_keys
  family&.classification_keys || []
end

#default_clefsObject



127
128
129
# File 'lib/head_music/instruments/instrument.rb', line 127

def default_clefs
  default_staves&.map(&:clef) || []
end

#default_staff_schemeObject



118
119
120
121
# File 'lib/head_music/instruments/instrument.rb', line 118

def default_staff_scheme
  @default_staff_scheme ||=
    staff_schemes.find(&:default?) || staff_schemes.first
end

#default_stavesObject



123
124
125
# File 'lib/head_music/instruments/instrument.rb', line 123

def default_staves
  default_staff_scheme&.staves || []
end

#default_variantObject



180
181
182
# File 'lib/head_music/instruments/instrument.rb', line 180

def default_variant
  nil
end

#ensure_localized_name(name:, locale_code: Locale::DEFAULT_CODE, abbreviation: nil) ⇒ Object Originally defined in module Named

#familyObject



92
93
94
95
96
# File 'lib/head_music/instruments/instrument.rb', line 92

def family
  return unless family_key

  HeadMusic::Instruments::InstrumentFamily.get(family_key)
end

#family_keyObject

Attributes with parent chain resolution



84
85
86
# File 'lib/head_music/instruments/instrument.rb', line 84

def family_key
  @family_key || parent&.family_key
end

#format_pitch_name(pitch_designation) ⇒ Object (private)



289
290
291
# File 'lib/head_music/instruments/instrument.rb', line 289

def format_pitch_name(pitch_designation)
  pitch_designation.to_s.tr("b", "♭").tr("#", "♯")
end

#inferred_nameObject (private)



285
286
287
# File 'lib/head_music/instruments/instrument.rb', line 285

def inferred_name
  name_key.to_s.tr("_", " ")
end

#initialize_data_from_record(record) ⇒ Object (private)



250
251
252
253
254
255
256
257
258
259
260
# File 'lib/head_music/instruments/instrument.rb', line 250

def initialize_data_from_record(record)
  @name_key = record["name_key"].to_sym
  @parent_key = record["parent_key"]&.to_sym
  @family_key = record["family_key"]
  @pitch_key = record["pitch_key"]
  @alias_name_keys = record["alias_name_keys"] || []
  @range_categories = record["range_categories"] || []
  @staff_schemes_data = record["staff_schemes"] || {}

  initialize_name
end

#initialize_nameObject (private)



262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
# File 'lib/head_music/instruments/instrument.rb', line 262

def initialize_name
  # Try to get a translation first
  base_name = I18n.translate(name_key, scope: "head_music.instruments", locale: "en", default: nil)

  if base_name
    # Use the translation as-is
    self.name = base_name
  elsif parent_key && pitch_key
    # Build name from parent + pitch for child instruments
    pitch_name = format_pitch_name(pitch_key_to_designation)
    self.name = "#{parent_translation} in #{pitch_name}"
  else
    # Fall back to inferred name
    self.name = inferred_name
  end
end

#instrument_configurationsObject

Collect all instrument_configurations from self and ancestors



185
186
187
188
189
# File 'lib/head_music/instruments/instrument.rb', line 185

def instrument_configurations
  own_configs = HeadMusic::Instruments::InstrumentConfiguration.for_instrument(name_key)
  parent_configs = parent&.instrument_configurations || []
  own_configs + parent_configs
end

#key_for_name(name) ⇒ Object (private)



223
224
225
226
227
228
229
230
231
# File 'lib/head_music/instruments/instrument.rb', line 223

def key_for_name(name)
  INSTRUMENTS.each do |key, _data|
    I18n.config.available_locales.each do |locale|
      translation = I18n.t("head_music.instruments.#{key}", locale: locale)
      return key if translation.downcase == name.downcase
    end
  end
  nil
end

#localized_name(locale_code: Locale::DEFAULT_CODE) ⇒ Object Originally defined in module Named

#localized_name_in_default_localeObject (private) Originally defined in module Named

#localized_name_in_locale_matching_language(locale) ⇒ Object (private) Originally defined in module Named

#localized_name_in_matching_locale(locale) ⇒ Object (private) Originally defined in module Named

#localized_namesObject Originally defined in module Named

Returns an array of LocalizedName instances that are synonymous with the name.

#multiple_staves?Boolean

Returns:

  • (Boolean)


149
150
151
# File 'lib/head_music/instruments/instrument.rb', line 149

def multiple_staves?
  default_staves.length > 1
end

#name(locale_code: Locale::DEFAULT_CODE) ⇒ Object Originally defined in module Named

#name=(name) ⇒ Object Originally defined in module Named

#orchestra_section_keyObject



98
99
100
# File 'lib/head_music/instruments/instrument.rb', line 98

def orchestra_section_key
  family&.orchestra_section_key
end

#parentObject

Parent instrument (for inheritance)



76
77
78
79
80
# File 'lib/head_music/instruments/instrument.rb', line 76

def parent
  return nil unless parent_key

  @parent ||= self.class.get(parent_key)
end

#parent_translationObject (private)



279
280
281
282
283
# File 'lib/head_music/instruments/instrument.rb', line 279

def parent_translation
  return nil unless parent_key

  I18n.translate(parent_key, scope: "head_music.instruments", locale: "en", default: parent_key.to_s.tr("_", " "))
end

#pitch_designationObject

Pitch designation as a Spelling object (for backward compatibility)



107
108
109
110
111
# File 'lib/head_music/instruments/instrument.rb', line 107

def pitch_designation
  return nil unless pitch_key

  @pitch_designation ||= HeadMusic::Rudiment::Spelling.get(pitch_key_to_designation)
end

#pitch_keyObject



88
89
90
# File 'lib/head_music/instruments/instrument.rb', line 88

def pitch_key
  @pitch_key || parent&.pitch_key
end

#pitch_key_to_designationObject (private)

Convert pitch_key (e.g., "b_flat") to designation format (e.g., "Bb")



294
295
296
297
298
299
300
301
302
303
304
305
306
# File 'lib/head_music/instruments/instrument.rb', line 294

def pitch_key_to_designation
  return nil unless pitch_key

  pitch_key_str = pitch_key.to_s
  first_letter = pitch_key_str[0].upcase
  if pitch_key_str.end_with?("_flat")
    "#{first_letter}b"
  elsif pitch_key_str.end_with?("_sharp")
    "#{first_letter}#"
  else
    pitch_key_str.upcase
  end
end

#pitched?Boolean

Returns:

  • (Boolean)


153
154
155
156
157
# File 'lib/head_music/instruments/instrument.rb', line 153

def pitched?
  return false if default_clefs.compact.uniq == [HeadMusic::Rudiment::Clef.get("neutral_clef")]

  default_clefs.any?
end

#record_for_alias(name) ⇒ Object (private)



240
241
242
243
244
245
246
247
248
# File 'lib/head_music/instruments/instrument.rb', line 240

def record_for_alias(name)
  normalized_name = HeadMusic::Utilities::HashKey.for(name).to_s
  INSTRUMENTS.each do |name_key, data|
    data["alias_name_keys"]&.each do |alias_key|
      return data.merge("name_key" => name_key) if HeadMusic::Utilities::HashKey.for(alias_key).to_s == normalized_name
    end
  end
  nil
end

#record_for_key(key) ⇒ Object (private)



233
234
235
236
237
238
# File 'lib/head_music/instruments/instrument.rb', line 233

def record_for_key(key)
  INSTRUMENTS.each do |name_key, data|
    return data.merge("name_key" => name_key) if name_key.to_s == key.to_s
  end
  nil
end

#record_for_name(name) ⇒ Object (private)



217
218
219
220
221
# File 'lib/head_music/instruments/instrument.rb', line 217

def record_for_name(name)
  record_for_key(HeadMusic::Utilities::HashKey.for(name)) ||
    record_for_key(key_for_name(name)) ||
    record_for_alias(name)
end

#single_staff?Boolean

Returns:

  • (Boolean)


145
146
147
# File 'lib/head_music/instruments/instrument.rb', line 145

def single_staff?
  default_staves.length == 1
end

#sounding_transpositionObject Also known as: default_sounding_transposition



131
132
133
# File 'lib/head_music/instruments/instrument.rb', line 131

def sounding_transposition
  default_staves&.first&.sounding_transposition || 0
end

#staff_schemesObject

Staff schemes (notation concern - kept for backward compatibility)



114
115
116
# File 'lib/head_music/instruments/instrument.rb', line 114

def staff_schemes
  @staff_schemes ||= build_staff_schemes
end

#stringingObject



191
192
193
# File 'lib/head_music/instruments/instrument.rb', line 191

def stringing
  @stringing ||= HeadMusic::Instruments::Stringing.for_instrument(self) || parent&.stringing
end

#to_sObject



171
172
173
# File 'lib/head_music/instruments/instrument.rb', line 171

def to_s
  name
end

#translation(locale = :en) ⇒ Object



159
160
161
162
163
# File 'lib/head_music/instruments/instrument.rb', line 159

def translation(locale = :en)
  return name unless name_key

  I18n.translate(name_key, scope: i[head_music instruments], locale: locale, default: name)
end

#transposing?Boolean

Returns:

  • (Boolean)


137
138
139
# File 'lib/head_music/instruments/instrument.rb', line 137

def transposing?
  sounding_transposition != 0
end

#transposing_at_the_octave?Boolean

Returns:

  • (Boolean)


141
142
143
# File 'lib/head_music/instruments/instrument.rb', line 141

def transposing_at_the_octave?
  transposing? && sounding_transposition % 12 == 0
end

#variantsObject

For backward compatibility with code that expects variants



176
177
178
# File 'lib/head_music/instruments/instrument.rb', line 176

def variants
  []
end