Class: HeadMusic::Style::Guidelines::Contoured

Inherits:
Annotation
  • Object
show all
Defined in:
lib/head_music/style/guidelines/contoured.rb

Overview

Flags a melody without the configured contour Configure the contour with the factory, e.g. Contoured.with(:arch).

Constant Summary collapse

CONTOURS =
%i[ascending descending arch valley wave static].freeze
TREND_REVERSAL_SEMITONES =

a trend reversal must exceed a whole step

3

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

This class inherits a constructor from HeadMusic::Style::Annotation

Class Method Details

.with(contour_key) ⇒ Object



11
12
13
14
15
16
17
18
# File 'lib/head_music/style/guidelines/contoured.rb', line 11

def self.with(contour_key)
  contour = contour_key.to_s.downcase.to_sym
  unless CONTOURS.include?(contour)
    raise ArgumentError, "Contour must be one of: #{CONTOURS.join(", ")} (got #{contour_key.inspect})"
  end

  super(contour: contour)
end

Instance Method Details

#arch?Boolean (private)

The climax is by definition the maximum, so “net rise before, net fall after” is equivalent to both endpoints sitting below the climax pitch. Climax uniqueness and consonance remain ConsonantClimax’s job.

Returns:

  • (Boolean)


60
61
62
# File 'lib/head_music/style/guidelines/contoured.rb', line 60

def arch?
  notes.length >= 3 && first_note.pitch < highest_pitch && last_note.pitch < highest_pitch
end

#ascending?Boolean (private)

Returns:

  • (Boolean)


49
50
51
# File 'lib/head_music/style/guidelines/contoured.rb', line 49

def ascending?
  first_note.pitch == lowest_pitch && last_note.pitch == highest_pitch
end

#contourObject (private)

Validated again here because Contoured.new(voice, contour: :bogus) bypasses .with.



33
34
35
36
37
38
39
40
41
42
43
# File 'lib/head_music/style/guidelines/contoured.rb', line 33

def contour
  @contour ||= begin
    key = options.fetch(:contour)
    contour = key.to_s.downcase.to_sym
    unless CONTOURS.include?(contour)
      raise ArgumentError, "Contour must be one of: #{CONTOURS.join(", ")} (got #{key.inspect})"
    end

    contour
  end
end

#descending?Boolean (private)

Returns:

  • (Boolean)


53
54
55
# File 'lib/head_music/style/guidelines/contoured.rb', line 53

def descending?
  first_note.pitch == highest_pitch && last_note.pitch == lowest_pitch
end

#directional_endpoints?Boolean (private)

The highest_pitch > lowest_pitch guard is load-bearing: without it, an all-same-pitch melody (first == lowest and last == highest simultaneously) would absurdly fail static.

Returns:

  • (Boolean)


79
80
81
82
83
# File 'lib/head_music/style/guidelines/contoured.rb', line 79

def directional_endpoints?
  highest_pitch > lowest_pitch &&
    ((first_note.pitch == lowest_pitch && last_note.pitch == highest_pitch) ||
      (first_note.pitch == highest_pitch && last_note.pitch == lowest_pitch))
end

#marksObject



20
21
22
23
24
# File 'lib/head_music/style/guidelines/contoured.rb', line 20

def marks
  return if notes.empty? || matches_contour?

  HeadMusic::Style::Mark.for_all(notes)
end

#matches_contour?Boolean (private)

Returns:

  • (Boolean)


45
46
47
# File 'lib/head_music/style/guidelines/contoured.rb', line 45

def matches_contour?
  send("#{contour}?")
end

#messageObject



26
27
28
# File 'lib/head_music/style/guidelines/contoured.rb', line 26

def message
  "Write a melody with the #{contour} contour."
end

#pitch_numbersObject (private)



85
86
87
# File 'lib/head_music/style/guidelines/contoured.rb', line 85

def pitch_numbers
  @pitch_numbers ||= notes.map { |note| note.pitch.midi_note_number }
end

#static?Boolean (private)

Returns:

  • (Boolean)


72
73
74
# File 'lib/head_music/style/guidelines/contoured.rb', line 72

def static?
  range <= HeadMusic::Analysis::DiatonicInterval.get(:major_third) && !directional_endpoints?
end

#trend_directionsObject (private)

Zigzag walk: a trend reversal is confirmed only when the melody retraces at least TREND_REVERSAL_SEMITONES from the running extreme of the current trend, so stepwise neighbor-note undulation never registers as a trend change.



92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
# File 'lib/head_music/style/guidelines/contoured.rb', line 92

def trend_directions
  @trend_directions ||= begin
    directions = []
    direction = nil
    high = low = pitch_numbers.first
    extreme = nil
    pitch_numbers.drop(1).each do |number|
      case direction
      when nil # no trend confirmed yet
        if number - low >= TREND_REVERSAL_SEMITONES
          direction = :ascending
          extreme = number
          directions << direction
        elsif high - number >= TREND_REVERSAL_SEMITONES
          direction = :descending
          extreme = number
          directions << direction
        else
          high = [high, number].max
          low = [low, number].min
        end
      when :ascending
        if number > extreme
          extreme = number
        elsif extreme - number >= TREND_REVERSAL_SEMITONES
          direction = :descending
          extreme = number
          directions << direction
        end
      when :descending
        if number < extreme
          extreme = number
        elsif number - extreme >= TREND_REVERSAL_SEMITONES
          direction = :ascending
          extreme = number
          directions << direction
        end
      end
    end
    directions
  end
end

#valley?Boolean (private)

Returns:

  • (Boolean)


64
65
66
# File 'lib/head_music/style/guidelines/contoured.rb', line 64

def valley?
  notes.length >= 3 && first_note.pitch > lowest_pitch && last_note.pitch > lowest_pitch
end

#wave?Boolean (private)

Returns:

  • (Boolean)


68
69
70
# File 'lib/head_music/style/guidelines/contoured.rb', line 68

def wave?
  trend_directions.length >= 3
end