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

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(name) ⇒ Instrument (private)

Returns a new instance of Instrument.



218
219
220
221
222
223
224
225
226
227
# File 'lib/head_music/instruments/instrument.rb', line 218

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.



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

def alias_name_keys
  @alias_name_keys
end

#name_keyObject (readonly)

Returns the value of attribute name_key.



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

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.



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

def parent_key
  @parent_key
end

#range_categoriesObject (readonly)

Returns the value of attribute range_categories.



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

def range_categories
  @range_categories
end

#staff_schemes_dataObject (readonly)

Returns the value of attribute staff_schemes_data.



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

def staff_schemes_data
  @staff_schemes_data
end

Class Method Details

.allObject



55
56
57
58
59
# File 'lib/head_music/instruments/instrument.rb', line 55

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



50
51
52
53
# File 'lib/head_music/instruments/instrument.rb', line 50

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



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

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

  # Handle two-argument form for backward compatibility
  if variant_key
    combined_name = "#{name}_#{variant_key}"
    result = find_valid_instrument(combined_name) || find_valid_instrument(name.to_s)
  else
    result = find_valid_instrument(name.to_s) || find_valid_instrument(normalize_variant_name(name))
  end

  result
end

.normalize_variant_name(name) ⇒ Object (private)

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"



66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
# File 'lib/head_music/instruments/instrument.rb', line 66

def normalize_variant_name(name)
  name_str = name.to_s

  # Match patterns like "_in_eb" or "_in_bb" at the end (flat)
  flat_pattern = /^(.+)_in_([a-g])b$/i
  sharp_pattern = %r{^(.+)_in_([a-g])\#$}i

  if name_str =~ flat_pattern
    instrument = Regexp.last_match(1)
    note = Regexp.last_match(2).downcase
    "#{instrument}_in_#{note}_flat"
  elsif name_str =~ sharp_pattern
    instrument = Regexp.last_match(1)
    note = Regexp.last_match(2).downcase
    "#{instrument}_in_#{note}_sharp"
  else
    name_str
  end
end

Instance Method Details

#==(other) ⇒ Object



177
178
179
180
181
# File 'lib/head_music/instruments/instrument.rb', line 177

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

  name_key == other.name_key
end

#alternate_tuningsObject



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

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)



319
320
321
322
323
324
325
326
327
328
329
# File 'lib/head_music/instruments/instrument.rb', line 319

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



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

def classification_keys
  family&.classification_keys || []
end

#default_clefsObject



139
140
141
# File 'lib/head_music/instruments/instrument.rb', line 139

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

#default_staff_schemeObject



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

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

#default_stavesObject



135
136
137
# File 'lib/head_music/instruments/instrument.rb', line 135

def default_staves
  default_staff_scheme&.staves || []
end

#default_variantObject



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

def default_variant
  nil
end

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

#familyObject



104
105
106
107
108
# File 'lib/head_music/instruments/instrument.rb', line 104

def family
  return unless family_key

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

#family_keyObject

Attributes with parent chain resolution



96
97
98
# File 'lib/head_music/instruments/instrument.rb', line 96

def family_key
  @family_key || parent&.family_key
end

#format_pitch_name(pitch_designation) ⇒ Object (private)



301
302
303
# File 'lib/head_music/instruments/instrument.rb', line 301

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

#inferred_nameObject (private)



297
298
299
# File 'lib/head_music/instruments/instrument.rb', line 297

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

#initialize_data_from_record(record) ⇒ Object (private)



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

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)



274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
# File 'lib/head_music/instruments/instrument.rb', line 274

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



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

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)



235
236
237
238
239
240
241
242
243
# File 'lib/head_music/instruments/instrument.rb', line 235

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)


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

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



110
111
112
# File 'lib/head_music/instruments/instrument.rb', line 110

def orchestra_section_key
  family&.orchestra_section_key
end

#parentObject

Parent instrument (for inheritance)



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

def parent
  return nil unless parent_key

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

#parent_translationObject (private)



291
292
293
294
295
# File 'lib/head_music/instruments/instrument.rb', line 291

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)



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

def pitch_designation
  return nil unless pitch_key

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

#pitch_keyObject



100
101
102
# File 'lib/head_music/instruments/instrument.rb', line 100

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")



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

def pitch_key_to_designation
  return nil unless pitch_key

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

#pitched?Boolean

Returns:

  • (Boolean)


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

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)



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

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)



245
246
247
248
249
250
# File 'lib/head_music/instruments/instrument.rb', line 245

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)



229
230
231
232
233
# File 'lib/head_music/instruments/instrument.rb', line 229

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)


157
158
159
# File 'lib/head_music/instruments/instrument.rb', line 157

def single_staff?
  default_staves.length == 1
end

#sounding_transpositionObject Also known as: default_sounding_transposition



143
144
145
# File 'lib/head_music/instruments/instrument.rb', line 143

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

#staff_schemesObject

Staff schemes (notation concern - kept for backward compatibility)



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

def staff_schemes
  @staff_schemes ||= build_staff_schemes
end

#stringingObject



203
204
205
# File 'lib/head_music/instruments/instrument.rb', line 203

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

#to_sObject



183
184
185
# File 'lib/head_music/instruments/instrument.rb', line 183

def to_s
  name
end

#translation(locale = :en) ⇒ Object



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

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)


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

def transposing?
  sounding_transposition != 0
end

#transposing_at_the_octave?Boolean

Returns:

  • (Boolean)


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

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

#variantsObject

For backward compatibility with code that expects variants



188
189
190
# File 'lib/head_music/instruments/instrument.rb', line 188

def variants
  []
end