Class: HeadMusic::Analysis::Dyad

Inherits:
Object
  • Object
show all
Defined in:
lib/head_music/analysis/dyad.rb

Overview

A Dyad is a two-pitch combination that can imply various chords. It analyzes the harmonic implications of two pitches sounding together.

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(pitch1, pitch2, key: nil) ⇒ Dyad

Returns a new instance of Dyad.



9
10
11
12
13
14
15
# File 'lib/head_music/analysis/dyad.rb', line 9

def initialize(pitch1, pitch2, key: nil)
  @pitch1, @pitch2 = [
    HeadMusic::Rudiment::Pitch.get(pitch1),
    HeadMusic::Rudiment::Pitch.get(pitch2)
  ].sort
  @key = key ? HeadMusic::Rudiment::Key.get(key) : nil
end

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(method_name, *args, &block) ⇒ Object



61
62
63
# File 'lib/head_music/analysis/dyad.rb', line 61

def method_missing(method_name, *args, &block)
  respond_to_missing?(method_name) ? interval.send(method_name, *args, &block) : super
end

Instance Attribute Details

#keyObject (readonly)

Returns the value of attribute key.



7
8
9
# File 'lib/head_music/analysis/dyad.rb', line 7

def key
  @key
end

#pitch1Object (readonly)

Returns the value of attribute pitch1.



7
8
9
# File 'lib/head_music/analysis/dyad.rb', line 7

def pitch1
  @pitch1
end

#pitch2Object (readonly)

Returns the value of attribute pitch2.



7
8
9
# File 'lib/head_music/analysis/dyad.rb', line 7

def pitch2
  @pitch2
end

Instance Method Details

#alteration_sign(semitones) ⇒ Object (private)



220
221
222
223
224
225
226
227
228
# File 'lib/head_music/analysis/dyad.rb', line 220

def alteration_sign(semitones)
  case semitones
  when -2 then "bb"
  when -1 then "b"
  when 0 then ""
  when 1 then "#"
  when 2 then "##"
  end
end

#enharmonic_respellingsObject



53
54
55
# File 'lib/head_music/analysis/dyad.rb', line 53

def enharmonic_respellings
  @enharmonic_respellings ||= generate_enharmonic_respellings
end

#filter_by_key(pitch_collections) ⇒ Object (private)



155
156
157
158
159
160
161
162
163
# File 'lib/head_music/analysis/dyad.rb', line 155

def filter_by_key(pitch_collections)
  return pitch_collections unless key

  diatonic_spellings = key.scale.spellings

  pitch_collections.select do |pitch_collection|
    pitch_collection.pitches.all? { |pitch| diatonic_spellings.include?(pitch.spelling) }
  end
end

#generate_enharmonic_respellingsObject (private)



177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
# File 'lib/head_music/analysis/dyad.rb', line 177

def generate_enharmonic_respellings
  respellings = []

  # Get enharmonic equivalents for each pitch
  pitch1_equivalents = get_enharmonic_equivalents(pitch1)
  pitch2_equivalents = get_enharmonic_equivalents(pitch2)

  # Generate all combinations
  pitch1_equivalents.each do |p1|
    pitch2_equivalents.each do |p2|
      # Skip the original combination
      next if p1.spelling == pitch1.spelling && p2.spelling == pitch2.spelling

      # Create new dyad with same key context
      respellings << self.class.new(p1, p2, key: key)
    end
  end

  respellings
end

#generate_possible_seventh_chordsObject (private)



111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
# File 'lib/head_music/analysis/dyad.rb', line 111

def generate_possible_seventh_chords
  seventh_chords = []
  pitch_classes = [lower_pitch.pitch_class, upper_pitch.pitch_class]

  HeadMusic::Rudiment::Spelling::CHROMATIC_SPELLINGS.each do |root_spelling|
    root_pitch = HeadMusic::Rudiment::Pitch.get("#{root_spelling}4")

    # Try all common seventh chord types from this root
    seventh_chord_intervals = [
      %w[M3 P5 M7],   # major seventh
      %w[M3 P5 m7],   # dominant seventh (major-minor)
      %w[m3 P5 m7],   # minor seventh
      %w[m3 P5 M7],   # minor-major seventh
      %w[m3 d5 m7],   # half-diminished seventh
      %w[m3 d5 d7],   # diminished seventh
      %w[M2 M3 P5 m7], # dominant ninth
      %w[m2 M3 P5 m7], # dominant minor ninth
      %w[M2 m3 P5 m7], # minor ninth
      %w[M2 M3 P5 M7]  # major ninth
    ]

    seventh_chord_intervals.each do |intervals|
      chord_pitches = [root_pitch]

      # Each interval is FROM THE ROOT, not consecutive
      intervals.each do |interval_name|
        interval = HeadMusic::Analysis::DiatonicInterval.get(interval_name)
        next_pitch = interval.above(root_pitch)
        chord_pitches << next_pitch
      end

      pitch_collection = HeadMusic::Analysis::PitchCollection.new(chord_pitches)
      chord_pitch_classes = pitch_collection.pitch_classes

      # Check if this chord contains both pitches from our dyad
      if pitch_classes.all? { |pc| chord_pitch_classes.include?(pc) }
        seventh_chords << pitch_collection
      end
    end
  end

  seventh_chords.uniq { |c| c.pitch_classes.sort.map(&:to_i) }
end

#generate_possible_trichordsObject (private)



71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
# File 'lib/head_music/analysis/dyad.rb', line 71

def generate_possible_trichords
  trichords = []
  pitch_classes = [lower_pitch.pitch_class, upper_pitch.pitch_class]

  HeadMusic::Rudiment::Spelling::CHROMATIC_SPELLINGS.each do |root_spelling|
    root_pitch = HeadMusic::Rudiment::Pitch.get("#{root_spelling}4")

    # Try all common trichord types from this root
    trichord_intervals = [
      %w[M3 P5],   # major triad
      %w[m3 P5],   # minor triad
      %w[m3 d5],   # diminished triad
      %w[M3 A5],   # augmented triad
      %w[P4 P5],   # sus4 (not a triad)
      %w[M2 P5]    # sus2 (not a triad)
    ]

    trichord_intervals.each do |intervals|
      trichord_pitches = [root_pitch]

      # Each interval is FROM THE ROOT, not consecutive
      intervals.each do |interval_name|
        interval = HeadMusic::Analysis::DiatonicInterval.get(interval_name)
        next_pitch = interval.above(root_pitch)
        trichord_pitches << next_pitch
      end

      pitch_collection = HeadMusic::Analysis::PitchCollection.new(trichord_pitches)
      trichord_pitch_classes = pitch_collection.pitch_classes

      # Check if this trichord contains both pitches from our dyad
      if pitch_classes.all? { |pc| trichord_pitch_classes.include?(pc) }
        trichords << pitch_collection
      end
    end
  end

  trichords.uniq { |t| t.pitch_classes.sort.map(&:to_i) }
end

#get_enharmonic_equivalents(pitch) ⇒ Object (private)



198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
# File 'lib/head_music/analysis/dyad.rb', line 198

def get_enharmonic_equivalents(pitch)
  equivalents = [pitch]

  # Get common enharmonic spellings
  pitch_class = pitch.pitch_class
  letter_names = HeadMusic::Rudiment::LetterName.all

  letter_names.each do |letter_name|
    [-2, -1, 0, 1, 2].each do |alteration_semitones|
      spelling = HeadMusic::Rudiment::Spelling.get("#{letter_name}#{alteration_sign(alteration_semitones)}")
      next unless spelling

      if spelling.pitch_class == pitch_class
        equivalent_pitch = HeadMusic::Rudiment::Pitch.fetch_or_create(spelling, pitch.register)
        equivalents << equivalent_pitch unless equivalents.any? { |p| p.spelling == spelling }
      end
    end
  end

  equivalents
end

#intervalObject



17
18
19
# File 'lib/head_music/analysis/dyad.rb', line 17

def interval
  @interval ||= HeadMusic::Analysis::DiatonicInterval.new(lower_pitch, upper_pitch)
end

#lower_pitchObject



25
26
27
# File 'lib/head_music/analysis/dyad.rb', line 25

def lower_pitch
  @lower_pitch ||= [pitch1, pitch2].min
end

#pitchesObject



21
22
23
# File 'lib/head_music/analysis/dyad.rb', line 21

def pitches
  [pitch1, pitch2]
end

#possible_seventh_chordsObject



45
46
47
48
49
50
51
# File 'lib/head_music/analysis/dyad.rb', line 45

def possible_seventh_chords
  @possible_seventh_chords ||= begin
    seventh_chords = generate_possible_seventh_chords
    seventh_chords = filter_by_key(seventh_chords) if key
    sort_by_diatonic_agreement(seventh_chords)
  end
end

#possible_triadsObject



41
42
43
# File 'lib/head_music/analysis/dyad.rb', line 41

def possible_triads
  @possible_triads ||= possible_trichords.select(&:triad?)
end

#possible_trichordsObject



33
34
35
36
37
38
39
# File 'lib/head_music/analysis/dyad.rb', line 33

def possible_trichords
  @possible_trichords ||= begin
    trichords = generate_possible_trichords
    trichords = filter_by_key(trichords) if key
    sort_by_diatonic_agreement(trichords)
  end
end

#respond_to_missing?(method_name, *_args) ⇒ Boolean

Returns:

  • (Boolean)


65
66
67
# File 'lib/head_music/analysis/dyad.rb', line 65

def respond_to_missing?(method_name, *_args)
  interval.respond_to?(method_name)
end

#sort_by_diatonic_agreement(pitch_collections) ⇒ Object (private)



165
166
167
168
169
170
171
172
173
174
175
# File 'lib/head_music/analysis/dyad.rb', line 165

def sort_by_diatonic_agreement(pitch_collections)
  return pitch_collections unless key

  diatonic_spellings = key.scale.spellings

  pitch_collections.sort_by do |pitch_collection|
    # Count how many pitches match diatonic spellings (lower is better for sort)
    diatonic_count = pitch_collection.pitches.count { |pitch| diatonic_spellings.include?(pitch.spelling) }
    -diatonic_count # Negative so higher counts come first
  end
end

#to_sObject



57
58
59
# File 'lib/head_music/analysis/dyad.rb', line 57

def to_s
  "#{pitch1} - #{pitch2}"
end

#upper_pitchObject



29
30
31
# File 'lib/head_music/analysis/dyad.rb', line 29

def upper_pitch
  @upper_pitch ||= [pitch1, pitch2].max
end