Class: HeadMusic::Style::Guidelines::Contoured
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
Class Method Summary
collapse
Instance Method Summary
collapse
-
#advance_trend(walk, number) ⇒ Object
private
-
#arch? ⇒ Boolean
private
The climax is the maximum by definition, so “rise then fall” reduces to both endpoints sitting below it.
-
#ascending? ⇒ Boolean
private
-
#continue_trend(walk, number) ⇒ Object
private
-
#contour ⇒ Object
private
Validated again here because Contoured.new(voice, contour: :bogus) bypasses .with.
-
#descending? ⇒ Boolean
private
-
#directional_endpoints? ⇒ Boolean
private
The range guard is load-bearing: an all-same-pitch melody is simultaneously ascending and descending, and would otherwise fail static.
-
#endpoints_interior_to?(extreme_pitch) ⇒ Boolean
private
The pitch is a running extreme, so no endpoint can pass it and “interior” means neither endpoint touches it.
-
#marks ⇒ Object
-
#matches_contour? ⇒ Boolean
private
-
#opposite_direction(direction) ⇒ Object
private
-
#pitch_numbers ⇒ Object
private
-
#seek_trend(walk, number) ⇒ Object
private
-
#start_trend(walk, direction, number) ⇒ Object
private
-
#static? ⇒ Boolean
private
-
#trend_directions ⇒ Object
private
A reversal counts only once the melody retraces TREND_REVERSAL_SEMITONES from the running extreme, so neighbor-note undulation is not a trend change.
-
#valley? ⇒ Boolean
private
-
#wave? ⇒ Boolean
private
Class Method Details
.normalized_contour(contour_key) ⇒ Object
18
19
20
21
22
23
24
25
|
# File 'lib/head_music/style/guidelines/contoured.rb', line 18
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
|
.template_values(config) ⇒ Object
33
34
35
|
# File 'lib/head_music/style/guidelines/contoured.rb', line 33
def self.template_values(config)
{contour: HeadMusic::Style::Template.render("contours.#{normalized_contour(config.fetch(:contour))}")}
end
|
.with(contour_key, **options) ⇒ Object
14
15
16
|
# File 'lib/head_music/style/guidelines/contoured.rb', line 14
def self.with(contour_key, **options)
super(contour: normalized_contour(contour_key), **options)
end
|
Instance Method Details
#advance_trend(walk, number) ⇒ Object
101
102
103
104
105
106
107
|
# File 'lib/head_music/style/guidelines/contoured.rb', line 101
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 the maximum by definition, so “rise then fall” reduces to both
endpoints sitting below it. Uniqueness and consonance are ConsonantClimax’s.
58
59
60
|
# File 'lib/head_music/style/guidelines/contoured.rb', line 58
def arch?
endpoints_interior_to?(highest_pitch)
end
|
#ascending? ⇒ Boolean
48
49
50
|
# File 'lib/head_music/style/guidelines/contoured.rb', line 48
def ascending?
first_note.pitch == lowest_pitch && last_note.pitch == highest_pitch
end
|
#continue_trend(walk, number) ⇒ Object
120
121
122
123
124
125
126
127
128
|
# File 'lib/head_music/style/guidelines/contoured.rb', line 120
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.
40
41
42
|
# File 'lib/head_music/style/guidelines/contoured.rb', line 40
def contour
@contour ||= self.class.normalized_contour(options.fetch(:contour))
end
|
#descending? ⇒ Boolean
52
53
54
|
# File 'lib/head_music/style/guidelines/contoured.rb', line 52
def descending?
first_note.pitch == highest_pitch && last_note.pitch == lowest_pitch
end
|
#directional_endpoints? ⇒ Boolean
The range guard is load-bearing: an all-same-pitch melody is simultaneously
ascending and descending, and would otherwise fail static.
82
83
84
|
# File 'lib/head_music/style/guidelines/contoured.rb', line 82
def directional_endpoints?
highest_pitch > lowest_pitch && (ascending? || descending?)
end
|
#endpoints_interior_to?(extreme_pitch) ⇒ Boolean
The pitch is a running extreme, so no endpoint can pass it and “interior”
means neither endpoint touches it.
68
69
70
|
# File 'lib/head_music/style/guidelines/contoured.rb', line 68
def endpoints_interior_to?(extreme_pitch)
notes.length >= 3 && first_note.pitch != extreme_pitch && last_note.pitch != extreme_pitch
end
|
#matches_contour? ⇒ Boolean
44
45
46
|
# File 'lib/head_music/style/guidelines/contoured.rb', line 44
def matches_contour?
send("#{contour}?")
end
|
#opposite_direction(direction) ⇒ Object
136
137
138
|
# File 'lib/head_music/style/guidelines/contoured.rb', line 136
def opposite_direction(direction)
(direction == :ascending) ? :descending : :ascending
end
|
#pitch_numbers ⇒ Object
86
87
88
|
# File 'lib/head_music/style/guidelines/contoured.rb', line 86
def pitch_numbers
@pitch_numbers ||= notes.map { |note| note.pitch.midi_note_number }
end
|
#seek_trend(walk, number) ⇒ Object
109
110
111
112
113
114
115
116
117
118
|
# File 'lib/head_music/style/guidelines/contoured.rb', line 109
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
130
131
132
133
134
|
# File 'lib/head_music/style/guidelines/contoured.rb', line 130
def start_trend(walk, direction, number)
walk.direction = direction
walk.extreme = number
walk.directions << direction
end
|
#trend_directions ⇒ Object
A reversal counts only once the melody retraces TREND_REVERSAL_SEMITONES
from the running extreme, so neighbor-note undulation is not a trend change.
92
93
94
95
96
97
98
99
|
# File 'lib/head_music/style/guidelines/contoured.rb', line 92
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
62
63
64
|
# File 'lib/head_music/style/guidelines/contoured.rb', line 62
def valley?
endpoints_interior_to?(lowest_pitch)
end
|
#wave? ⇒ Boolean
72
73
74
|
# File 'lib/head_music/style/guidelines/contoured.rb', line 72
def wave?
trend_directions.length >= 3
end
|