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

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.



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

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)

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

#alias_name_keysObject (readonly) Originally defined in module Named

Returns the value of attribute alias_name_keys.

#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

Class Method Details

.allObject



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

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



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

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
# File 'lib/head_music/instruments/instrument.rb', line 36

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)



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

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



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

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

  name_key == other.name_key
end

#alternate_tuningsObject



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

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

  parent&.alternate_tunings || []
end

#classification_keysObject



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

def classification_keys
  family&.classification_keys || []
end

#default_clefsObject



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

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

#default_notation_staves_dataObject (private)

The raw staff-attribute list for this instrument’s default notation, resolved from the default NotationStyle.



319
320
321
322
# File 'lib/head_music/instruments/instrument.rb', line 319

def default_notation_staves_data
  notation = HeadMusic::Notation::NotationStyle.default.notation_for(self)
  (notation&.staves || []).map(&:attributes)
end

#default_staff_schemeObject



125
126
127
128
129
130
131
# File 'lib/head_music/instruments/instrument.rb', line 125

def default_staff_scheme
  @default_staff_scheme ||= HeadMusic::Instruments::StaffScheme.new(
    key: "default",
    instrument: self,
    list: default_notation_staves_data
  )
end

#default_stavesObject



133
134
135
# File 'lib/head_music/instruments/instrument.rb', line 133

def default_staves
  default_staff_scheme.staves
end

#default_variantObject



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

def default_variant
  nil
end

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

#familyObject



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

def family
  return unless family_key

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

#family_keyObject

Attributes with parent chain resolution



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

def family_key
  @family_key || parent&.family_key
end

#format_pitch_name(pitch_designation) ⇒ Object (private)



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

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

#inferred_nameObject (private)



294
295
296
# File 'lib/head_music/instruments/instrument.rb', line 294

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

#initialize_data_from_record(record) ⇒ Object (private)



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

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"] || []

  initialize_name
end

#initialize_nameObject (private)



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

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



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

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)



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

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)


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

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

#notation(style: :default) ⇒ Object

Notation for this instrument in the given style (defaults to :default).



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

def notation(style: :default)
  HeadMusic::Notation::NotationStyle.get(style).notation_for(self)
end

#orchestra_section_keyObject



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

def orchestra_section_key
  family&.orchestra_section_key
end

#parentObject

Parent instrument (for inheritance)



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

def parent
  return nil unless parent_key

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

#parent_translationObject (private)



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

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)



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

def pitch_designation
  return nil unless pitch_key

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

#pitch_keyObject



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

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



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

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)


163
164
165
166
167
# File 'lib/head_music/instruments/instrument.rb', line 163

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)



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

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)



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

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)



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

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)


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

def single_staff?
  default_staves.length == 1
end

#sounding_transpositionObject Also known as: default_sounding_transposition



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

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

#staff_schemesObject

Staff schemes are a notation concern; they now live in NotationStyle. These methods remain for backward compatibility and delegate to the default style. Referenced only inside method bodies, because the Notation module loads after Instruments (see head_music.rb load order).



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

def staff_schemes
  [default_staff_scheme]
end

#stringingObject



201
202
203
# File 'lib/head_music/instruments/instrument.rb', line 201

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

#to_sObject



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

def to_s
  name
end

#translation(locale = :en) ⇒ Object



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

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)


147
148
149
# File 'lib/head_music/instruments/instrument.rb', line 147

def transposing?
  sounding_transposition != 0
end

#transposing_at_the_octave?Boolean

Returns:

  • (Boolean)


151
152
153
# File 'lib/head_music/instruments/instrument.rb', line 151

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

#variantsObject

For backward compatibility with code that expects variants



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

def variants
  []
end