Class: Musa::Chords::ChordDefinition

Inherits:
Object
  • Object
show all
Defined in:
lib/musa-dsl/music/chord-definition.rb

Overview

Chord template defining structure and features.

ChordDefinition is a template that specifies the intervals and characteristics of a chord type. It's defined once and used to create many chord instances.

Components

  • Name: Unique identifier (:maj, :min, :dom7, etc.)
  • Offsets: Semitone intervals from root ({ root: 0, third: 4, fifth: 7 })
  • Features: Characteristics (quality: :major, size: :triad)

Registration

Chord definitions are registered globally:

ChordDefinition.register :maj,
  quality: :major,
  size: :triad,
  offsets: { root: 0, third: 4, fifth: 7 }

Finding Definitions

By name:

ChordDefinition[:maj]  # => <ChordDefinition :maj>

By features:

ChordDefinition.find_by_features(quality: :major, size: :triad)
# => [<ChordDefinition :maj>]

By pitches:

ChordDefinition.find_by_pitches([60, 64, 67])  # C E G
# => <ChordDefinition :maj>

Examples:

Defining a chord the library does not have

# A name already in use is REFUSED -- :maj, :min7 and the rest are
# registered at load time, and replacing one silently would change what
# every other piece in the process means by it.
ChordDefinition.register :sus4,
  quality: :suspended,
  size: :triad,
  offsets: { root: 0, fourth: 5, fifth: 7 }

ChordDefinition[:sus4].pitches(60)  # => [60, 65, 67]

ChordDefinition.register :maj, quality: :major, size: :triad,
                         offsets: { root: 0, third: 4, fifth: 7 }
# => ArgumentError

Defining a dominant seventh

ChordDefinition.register :dom7_wide,
  quality: :dominant,
  size: :seventh,
  offsets: { root: 0, third: 4, fifth: 7, seventh: 10 }

See Also:

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(name, offsets:, **features) ⇒ ChordDefinition

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Creates a chord definition.

Parameters:

  • name (Symbol)

    chord name

  • offsets (Hash{Symbol => Integer})

    semitone offsets

  • features (Hash)

    chord features



273
274
275
276
277
278
279
# File 'lib/musa-dsl/music/chord-definition.rb', line 273

def initialize(name, offsets:, **features)
  @name = name.freeze
  @features = features.transform_values(&:dup).transform_values(&:freeze).freeze
  @pitch_offsets = offsets.dup.freeze
  @pitch_names = offsets.collect { |k, v| [v, k] }.to_h.freeze
  freeze
end

Instance Attribute Details

#featuresHash{Symbol => Symbol} (readonly)

Chord features (quality, size, etc.).

Returns:

  • (Hash{Symbol => Symbol})


287
288
289
# File 'lib/musa-dsl/music/chord-definition.rb', line 287

def features
  @features
end

#nameSymbol (readonly)

Chord name.

Returns:

  • (Symbol)


283
284
285
# File 'lib/musa-dsl/music/chord-definition.rb', line 283

def name
  @name
end

#pitch_namesHash{Integer => Symbol} (readonly)

Position names by semitone offset.

Returns:

  • (Hash{Integer => Symbol})


295
296
297
# File 'lib/musa-dsl/music/chord-definition.rb', line 295

def pitch_names
  @pitch_names
end

#pitch_offsetsHash{Symbol => Integer} (readonly)

Semitone offsets by position name.

Returns:

  • (Hash{Symbol => Integer})


291
292
293
# File 'lib/musa-dsl/music/chord-definition.rb', line 291

def pitch_offsets
  @pitch_offsets
end

Class Method Details

.feature_key_of(feature_value) ⇒ Symbol

Returns feature key for a feature value.

Parameters:

  • feature_value (Symbol)

    feature value

Returns:

  • (Symbol)

    feature key



248
249
250
# File 'lib/musa-dsl/music/chord-definition.rb', line 248

def self.feature_key_of(feature_value)
  @features_by_value[feature_value]
end

.feature_keysSet<Symbol>

Returns all registered feature keys.

Returns:

  • (Set<Symbol>)

    feature keys



262
263
264
# File 'lib/musa-dsl/music/chord-definition.rb', line 262

def self.feature_keys
  @feature_keys
end

.feature_valuesArray<Symbol>

Returns all registered feature values.

Returns:

  • (Array<Symbol>)

    feature values



255
256
257
# File 'lib/musa-dsl/music/chord-definition.rb', line 255

def self.feature_values
  @features_by_value.keys
end

.features_from(values = nil, hash = nil) ⇒ Hash

Converts feature values to feature hash.

Examples:

ChordDefinition.features_from([:major, :triad])
# => { quality: :major, size: :triad }

Parameters:

  • values (Array<Symbol>) (defaults to: nil)

    feature values

  • hash (Hash) (defaults to: nil)

    feature key-value pairs

Returns:

  • (Hash)

    combined features



220
221
222
223
224
225
226
227
228
# File 'lib/musa-dsl/music/chord-definition.rb', line 220

def self.features_from(values = nil, hash = nil)
  values ||= []
  hash ||= {}

  features = hash.dup
  values.each { |v| features[@features_by_value[v]] = v }

  features
end

.find_by_features(*values, **hash) ⇒ Array<ChordDefinition>

Finds definitions matching specified features.

Examples:

ChordDefinition.find_by_features(quality: :major, size: :triad).collect(&:name)
# => [:maj]

Parameters:

  • values (Array<Symbol>)

    feature values

  • hash (Hash)

    feature key-value pairs

Returns:



239
240
241
242
# File 'lib/musa-dsl/music/chord-definition.rb', line 239

def self.find_by_features(*values, **hash)
  features = features_from(values, hash)
  @definitions.values.select { |d| features <= d.features }
end

.find_by_pitches(pitches) ⇒ ChordDefinition?

Finds chord definition matching a set of pitches.

Identifies chord by comparing pitch intervals, accounting for octave reduction.

Examples:

definition = ChordDefinition.find_by_pitches([60, 64, 67])
definition.name  # => :maj

No definition matches

ChordDefinition.find_by_pitches([60, 61, 62])  # => nil

Parameters:

  • pitches (Array<Integer>)

    MIDI pitch numbers

Returns:



207
208
209
# File 'lib/musa-dsl/music/chord-definition.rb', line 207

def self.find_by_pitches(pitches)
  @definitions.values.find { |d| d.matches(pitches) }
end

.get(name) ⇒ ChordDefinition? Also known as: []

Retrieves a registered chord definition by name.

Examples:

ChordDefinition[:maj].features    # => { quality: :major, size: :triad }
ChordDefinition[:min7].pitches(60)  # => [60, 63, 67, 70]

ChordDefinition[:no_such_chord]   # => nil

Parameters:

  • name (Symbol)

    chord definition name

Returns:



132
133
134
# File 'lib/musa-dsl/music/chord-definition.rb', line 132

def self.get(name)
  @definitions[name]
end

.register(name, offsets:, **features) ⇒ self

Registers a new chord definition.

Creates and registers a chord definition with specified intervals and features. The definition becomes available globally for chord creation.

Examples:

A name of your own

ChordDefinition.register :add9,
  quality: :major,
  size: :extended,
  offsets: { root: 0, third: 4, fifth: 7, ninth: 14 }

ChordDefinition[:add9].pitches(60)  # => [60, 64, 67, 74]

A name already taken

ChordDefinition.register :min7, quality: :minor, size: :seventh,
                         offsets: { root: 0, third: 3, fifth: 7, seventh: 10 }
# => ArgumentError

# The registry is global and it is loaded before anyone gets a chance
# to add to it. Silently replacing an entry meant the piece that asked
# first went on asking for its own chord and got somebody else's.

Parameters:

  • name (Symbol)

    unique chord identifier

  • offsets (Hash{Symbol => Integer})

    semitone intervals from root

  • features (Hash)

    chord characteristics (quality, size, etc.)

Returns:

  • (self)

Raises:

  • (ArgumentError)

    if the name is already registered



168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
# File 'lib/musa-dsl/music/chord-definition.rb', line 168

def self.register(name, offsets:, **features)
  definition = ChordDefinition.new(name, offsets: offsets, **features)

  @definitions ||= {}

  # Registering over an existing name used to replace it silently, and a
  # chord definition is global: the piece that registered first goes on
  # asking for its own chord and gets somebody else's. Refuse instead, and
  # say what is already there.
  if (existing = @definitions[definition.name])
    raise ArgumentError,
          "chord definition #{definition.name.inspect} is already registered " \
          "as #{existing.features.inspect}; unregister it first if you mean to replace it"
  end

  @definitions[definition.name] = definition

  @features_by_value ||= {}
  definition.features.each { |k, v| @features_by_value[v] = k }

  @feature_keys ||= Set[]
  features.each_key { |feature_name| @feature_keys << feature_name }

  self
end

Instance Method Details

#in_scale?(scale, chord_root_pitch:) ⇒ Boolean

Checks if chord fits within a scale.

Examples:

maj_def.in_scale?(c_major, chord_root_pitch: 60)  # => true

Parameters:

  • scale (Scales::Scale)

    scale to check against

  • chord_root_pitch (Integer)

    chord root pitch

Returns:

  • (Boolean)

    true if all chord notes are in scale



316
317
318
# File 'lib/musa-dsl/music/chord-definition.rb', line 316

def in_scale?(scale, chord_root_pitch:)
  !pitches(chord_root_pitch).find { |chord_pitch| scale.note_of_pitch(chord_pitch).nil? }
end

#inspectString Also known as: to_s

Returns string representation.

Returns:



370
371
372
# File 'lib/musa-dsl/music/chord-definition.rb', line 370

def inspect
  "<ChordDefinition: name = #{@name} features = #{@features} pitch_offsets = #{@pitch_offsets}>"
end

#matches(pitches) ⇒ Boolean

Checks if pitches match this chord definition.

Compares octave-reduced pitch sets to determine if they form this chord.

Examples:

maj_def.matches([60, 64, 67])  # => true (C major)
maj_def.matches([60, 63, 67])  # => false (C minor)

Parameters:

  • pitches (Array<Integer>)

    MIDI pitches to check

Returns:

  • (Boolean)

    true if pitches match this chord



359
360
361
362
363
364
365
# File 'lib/musa-dsl/music/chord-definition.rb', line 359

def matches(pitches)
  reduced_pitches = octave_reduce(pitches).uniq

  !!reduced_pitches.find do |candidate_root_pitch|
    reduced_pitches.sort == octave_reduce(pitches(candidate_root_pitch)).uniq.sort
  end
end

#named_pitches(elements_or_pitches) {|element| ... } ⇒ Hash{Symbol => Array}

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Maps elements to named chord positions.

Parameters:

  • elements_or_pitches (Array)

    elements to map

Yields:

  • (element)

    block to extract pitch from element

Returns:

  • (Hash{Symbol => Array})

    position names to elements



327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
# File 'lib/musa-dsl/music/chord-definition.rb', line 327

def named_pitches(elements_or_pitches, &block)
  pitches = elements_or_pitches.collect do |element_or_pitch|
    [if block_given?
       yield element_or_pitch
     else
       element_or_pitch
     end,
     element_or_pitch]
  end.to_h

  root_pitch = pitches.keys.find do |candidate_root_pitch|
    candidate_pitches = pitches.keys.collect { |p| p - candidate_root_pitch }
    octave_reduce(candidate_pitches).uniq == octave_reduce(@pitch_offsets.values).uniq
  end

  # TODO: OJO: problema con las notas duplicadas, con la identificación de inversiones y con las notas a distancias de más de una octava

  pitches.collect do |pitch, element|
    [@pitch_names[pitch - root_pitch], [element]]
  end.to_h
end

#pitches(root_pitch) ⇒ Array<Integer>

Calculates chord pitches from root pitch.

Examples:

chord_def.pitches(60)  # => [60, 64, 67]  (C major)

Parameters:

  • root_pitch (Integer)

    MIDI root pitch

Returns:

  • (Array<Integer>)

    chord pitches



304
305
306
# File 'lib/musa-dsl/music/chord-definition.rb', line 304

def pitches(root_pitch)
  @pitch_offsets.values.collect { |offset| root_pitch + offset }
end