Class: HeadMusic::Style::Guidelines::Contoured
- Inherits:
-
Annotation
- Object
- Annotation
- HeadMusic::Style::Guidelines::Contoured
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).
Defined Under Namespace
Classes: TrendWalk
Constant Summary
collapse
- CONTOURS =
%i[ascending descending arch valley wave static].freeze
- TREND_REVERSAL_SEMITONES =
a trend reversal must exceed a whole step
3
- DEFAULT_WEIGHT =
HeadMusic::GOLDEN_RATIO_INVERSE
Class Method Summary
collapse
Instance Method Summary
collapse
-
#advance_trend(walk, number) ⇒ Object
private
-
#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.
-
#ascending? ⇒ Boolean
private
-
#continue_trend(walk, number) ⇒ Object
private
Within a trend: extend the extreme while the melody keeps going, or confirm a reversal once it retraces from that extreme by at least the threshold.
-
#contour ⇒ Object
private
Validated again here because Contoured.new(voice, contour: :bogus) bypasses .with.
-
#descending? ⇒ Boolean
private
-
#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.
-
#endpoints_interior_to?(extreme_pitch) ⇒ Boolean
private
Because the given pitch is a running extreme (the highest or lowest), no endpoint can pass it, so “interior” simply means neither endpoint touches it.
-
#marks ⇒ Object
-
#matches_contour? ⇒ Boolean
private
-
#message ⇒ Object
-
#opposite_direction(direction) ⇒ Object
private
-
#pitch_numbers ⇒ Object
private
-
#seek_trend(walk, number) ⇒ Object
private
No trend confirmed yet: widen the running range until the melody breaks out of it by at least the reversal threshold, which sets the first direction.
-
#start_trend(walk, direction, number) ⇒ Object
private
-
#static? ⇒ Boolean
private
-
#trend_directions ⇒ Object
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.
-
#valley? ⇒ Boolean
private
-
#wave? ⇒ Boolean
private
Class Method Details
.default_weight ⇒ Object
29
30
31
|
# File 'lib/head_music/style/guidelines/contoured.rb', line 29
def self.default_weight
DEFAULT_WEIGHT
end
|
.normalized_contour(contour_key) ⇒ Object
20
21
22
23
24
25
26
27
|
# File 'lib/head_music/style/guidelines/contoured.rb', line 20
def self.normalized_contour(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
contour
end
|
.with(contour_key, **options) ⇒ Object
16
17
18
|
# File 'lib/head_music/style/guidelines/contoured.rb', line 16
def self.with(contour_key, **options)
super(contour: normalized_contour(contour_key), **options)
end
|
Instance Method Details
#advance_trend(walk, number) ⇒ Object
110
111
112
113
114
115
116
|
# File 'lib/head_music/style/guidelines/contoured.rb', line 110
def advance_trend(walk, number)
if walk.direction.nil?
seek_trend(walk, number)
else
continue_trend(walk, number)
end
end
|
#arch? ⇒ Boolean
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.
65
66
67
|
# File 'lib/head_music/style/guidelines/contoured.rb', line 65
def arch?
endpoints_interior_to?(highest_pitch)
end
|
#ascending? ⇒ Boolean
54
55
56
|
# File 'lib/head_music/style/guidelines/contoured.rb', line 54
def ascending?
first_note.pitch == lowest_pitch && last_note.pitch == highest_pitch
end
|
#continue_trend(walk, number) ⇒ Object
Within a trend: extend the extreme while the melody keeps going, or confirm a
reversal once it retraces from that extreme by at least the threshold.
133
134
135
136
137
138
139
140
141
|
# File 'lib/head_music/style/guidelines/contoured.rb', line 133
def continue_trend(walk, number)
sign = (walk.direction == :ascending) ? 1 : -1
delta = number - walk.extreme
if sign * delta > 0
walk.extreme = number
elsif -sign * delta >= TREND_REVERSAL_SEMITONES
start_trend(walk, opposite_direction(walk.direction), number)
end
end
|
#contour ⇒ Object
Validated again here because Contoured.new(voice, contour: :bogus) bypasses .with.
46
47
48
|
# File 'lib/head_music/style/guidelines/contoured.rb', line 46
def contour
@contour ||= self.class.normalized_contour(options.fetch(:contour))
end
|
#descending? ⇒ Boolean
58
59
60
|
# File 'lib/head_music/style/guidelines/contoured.rb', line 58
def descending?
first_note.pitch == highest_pitch && last_note.pitch == lowest_pitch
end
|
#directional_endpoints? ⇒ Boolean
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.
90
91
92
|
# File 'lib/head_music/style/guidelines/contoured.rb', line 90
def directional_endpoints?
highest_pitch > lowest_pitch && (ascending? || descending?)
end
|
#endpoints_interior_to?(extreme_pitch) ⇒ Boolean
Because the given pitch is a running extreme (the highest or lowest), no
endpoint can pass it, so “interior” simply means neither endpoint touches it.
75
76
77
|
# File 'lib/head_music/style/guidelines/contoured.rb', line 75
def endpoints_interior_to?(extreme_pitch)
notes.length >= 3 && first_note.pitch != extreme_pitch && last_note.pitch != extreme_pitch
end
|
#matches_contour? ⇒ Boolean
50
51
52
|
# File 'lib/head_music/style/guidelines/contoured.rb', line 50
def matches_contour?
send("#{contour}?")
end
|
#message ⇒ Object
39
40
41
|
# File 'lib/head_music/style/guidelines/contoured.rb', line 39
def message
"Write a melody with the #{contour} contour."
end
|
#opposite_direction(direction) ⇒ Object
149
150
151
|
# File 'lib/head_music/style/guidelines/contoured.rb', line 149
def opposite_direction(direction)
(direction == :ascending) ? :descending : :ascending
end
|
#pitch_numbers ⇒ Object
94
95
96
|
# File 'lib/head_music/style/guidelines/contoured.rb', line 94
def pitch_numbers
@pitch_numbers ||= notes.map { |note| note.pitch.midi_note_number }
end
|
#seek_trend(walk, number) ⇒ Object
No trend confirmed yet: widen the running range until the melody breaks out
of it by at least the reversal threshold, which sets the first direction.
120
121
122
123
124
125
126
127
128
129
|
# File 'lib/head_music/style/guidelines/contoured.rb', line 120
def seek_trend(walk, number)
if number - walk.low >= TREND_REVERSAL_SEMITONES
start_trend(walk, :ascending, number)
elsif walk.high - number >= TREND_REVERSAL_SEMITONES
start_trend(walk, :descending, number)
else
walk.high = [walk.high, number].max
walk.low = [walk.low, number].min
end
end
|
#start_trend(walk, direction, number) ⇒ Object
143
144
145
146
147
|
# File 'lib/head_music/style/guidelines/contoured.rb', line 143
def start_trend(walk, direction, number)
walk.direction = direction
walk.extreme = number
walk.directions << direction
end
|
#trend_directions ⇒ Object
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.
101
102
103
104
105
106
107
108
|
# File 'lib/head_music/style/guidelines/contoured.rb', line 101
def trend_directions
@trend_directions ||= begin
first = pitch_numbers.first
walk = TrendWalk.new([], nil, first, first, nil)
pitch_numbers.drop(1).each { |number| advance_trend(walk, number) }
walk.directions
end
end
|
#valley? ⇒ Boolean
69
70
71
|
# File 'lib/head_music/style/guidelines/contoured.rb', line 69
def valley?
endpoints_interior_to?(lowest_pitch)
end
|
#wave? ⇒ Boolean
79
80
81
|
# File 'lib/head_music/style/guidelines/contoured.rb', line 79
def wave?
trend_directions.length >= 3
end
|