Skip to content

Commit 287dd0b

Browse files
committed
factored out envelope profile from segment
1 parent 374e437 commit 287dd0b

File tree

2 files changed

+162
-40
lines changed

2 files changed

+162
-40
lines changed

birdfish/envelope.py

+114-34
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,13 @@
3636
a pause segment could be represented as a segment with negative duration
3737
3838
"""
39+
# TODO
40+
# Need to introduce an EnvelopeProfile for the reusable math bits
41+
# all cumulative update tracking should happen in the end segment
42+
# and a containing envelope needs to "reset" the segment as needed
3943

40-
class EnvelopeSegment(object):
44+
45+
class EnvelopeProfile(object):
4146
"""
4247
contains one phase or segment of a multi-segment envelope
4348
attributes:
@@ -48,22 +53,55 @@ class EnvelopeSegment(object):
4853
end value represented as delta/change
4954
"""
5055

51-
def __init__(self, tween=tween.LINEAR, start=0, change=1.0, duration=1.0):
56+
def __init__(self, tween=tween.LINEAR, start=0, change=1.0, duration=1.0,
57+
label="profile"):
5258
self.tween = tween
5359
self.start = float(start)
5460
self.change = float(change)
5561
self.duration = float(duration)
62+
self.label = label
63+
5664
assert self.duration > 0 # only a duration > 0 makes sense
5765

58-
def update(self, delta):
66+
def get_value(self, delta):
5967
if delta > self.duration:
6068
delta = self.duration
69+
# TODO save a self.value here for more consistency with envelopes?
70+
print "updating %s %s at %s" % (self.label, self, delta)
6171
return self.tween(delta, self.start, self.change, self.duration)
6272

6373
def get_jump_time(self, value):
6474
return tween.jump_time(self.tween, value, self.start, self.change, self.duration)
6575

6676

77+
class EnvelopeSegment(object):
78+
"""
79+
An ultimate end point in a nested envelope
80+
contains reference to an envelope profile"""
81+
def __init__(self, tween=tween.LINEAR, start=0, change=1.0, duration=1.0,
82+
profile=None, label="segment"):
83+
if profile:
84+
self.profile = profile
85+
else:
86+
self.profile = EnvelopeProfile(tween=tween, start=start,
87+
change=change, duration=duration, label="%s-profile" % label)
88+
self.duration = float(duration)
89+
self.label = label
90+
self.elapsed = 0
91+
self.value = 0
92+
93+
assert self.duration > 0 # only a duration > 0 makes sense
94+
95+
def reset(self):
96+
self.elapsed = 0
97+
self.value = 0
98+
99+
def update(self, delta):
100+
print "updating %s %s at delta %s" % (self.label, self, delta)
101+
self.elapsed += delta
102+
self.value = self.profile.get_value(self.elapsed)
103+
return self.value
104+
67105
class StaticEnvelopeSegment(EnvelopeSegment):
68106
# just returns the start value unchanged - always
69107
# will never advance on its own without a trigger interving
@@ -73,12 +111,13 @@ def __init__(self, *args, **kwargs):
73111
self.duration = 0
74112

75113
def update(self, delta):
76-
return self.start
114+
return self.profile.start
77115

78116
class Envelope(EnvelopeSegment):
79117

80-
def __init__(self, loop=0):
118+
def __init__(self, loop=0, label="envelope"):
81119
self.segments = []
120+
self.label = label
82121

83122
# Not sure I need this state
84123
self.running = False
@@ -105,39 +144,60 @@ def reset(self):
105144
self.index = 0
106145
self.value = 0
107146
self.current_segment_time_delta = 0
147+
for segment in self.segments:
148+
segment.reset()
108149

109150
def advance(self):
110-
self.index += 1
111-
self.current_segment_time_delta = 0
112-
if self.index + 1 > len(self.segments) and self.loop and self.loop_counter:
113-
self.index = 0
114-
if self.loop > 0: # this is a finite loop
115-
self.loop_counter -= 1
151+
# return True if advanced
152+
print "advancing"
153+
print self.index, len(self.segments)
154+
if self.index + 1 == len(self.segments): # last segment
155+
if self.loop and self.loop_counter:
156+
print 'looping'
157+
self.segments[self.index].reset()
158+
self.index = 0
159+
if self.loop > 0: # this is a finite loop
160+
self.loop_counter -= 1
161+
print 'loop counter now: ', self.loop_counter
162+
return True
163+
print 'infinite loop counter now: ', self.loop_counter
164+
else:
165+
print "not looping on advance - staying at last segment"
166+
# non-looping, or done with final loop
167+
pass
168+
else:
169+
# proceed through sequence of segments
170+
self.segments[self.index].reset()
171+
self.index += 1
172+
self.current_segment_time_delta = 0
173+
return True
174+
return False
116175

117176

118177
def update(self, delta):
178+
print '---------------------'
179+
print "updating %s %s at delta %s" % (self.label, self, delta)
180+
119181
# delta is time passed since last update
120182
if self.index + 1 > len(self.segments):
121183
# non looping or end of finite loop
122184
# just reurn last value until something resets index
123185
return self.value
124186
segment = self.segments[self.index]
125187
self.current_segment_time_delta += delta
126-
# self.timedelta += delta
188+
print "self current elapsed %s" % self.current_segment_time_delta
127189
if (segment.duration and
128190
(self.current_segment_time_delta > segment.duration)):
129191
overage = self.current_segment_time_delta - segment.duration
130-
self.advance()
131192
# TODO don't handle case where overage > new segment
132193
# duration - could need recursion
133-
self.current_segment_time_delta = overage
134-
if self.index + 1 > len(self.segments):
135-
self.value = segment.update(segment.duration)
136-
else:
194+
195+
if self.advance():
196+
print 'advanced, new delta: ', overage
197+
delta = self.current_segment_time_delta = overage
137198
segment = self.segments[self.index]
138-
self.value = segment.update(self.current_segment_time_delta)
139-
else:
140-
self.value = segment.update(self.current_segment_time_delta)
199+
200+
self.value = segment.update(delta)
141201
return self.value
142202

143203
@property
@@ -155,6 +215,7 @@ class TriggeredEnvelope(Envelope):
155215
def __init__(self, *args, **kwargs):
156216
super(TriggeredEnvelope, self).__init__(*args, **kwargs)
157217
self.state = 0 # on:1 off:0
218+
self.label = kwargs.get('label', 'triggered-envelope')
158219
# two states - each has a segment - which may be a envelope with sub-segments
159220

160221
def trigger(self, state=1, value=1.0):
@@ -177,8 +238,9 @@ def trigger(self, state=1, value=1.0):
177238
# off trigger
178239
print 'advance'
179240
self.advance()
180-
if self.value != self.segments[1].start:
181-
self.current_segment_time_delta += self.segments[1].get_jump_time(self.value)
241+
if self.value != self.segments[1].profile.start:
242+
print "shortcutting time"
243+
self.current_segment_time_delta += self.segments[1].profile.get_jump_time(self.value)
182244
self.state = state
183245

184246
def update(self, delta):
@@ -203,28 +265,46 @@ def __init__(self, peak_value=1.0, sustain_value=0.8,
203265
attack_shape=tween.LINEAR, attack_duration=0.5,
204266
decay_shape=tween.LINEAR, decay_duration=.2,
205267
release_shape=tween.LINEAR, release_duration=.5,
206-
bell_mode=False):
268+
bell_mode=False, label='ADSR-envelope'):
269+
270+
super(ADSREnvelope, self).__init__(loop=0, label=label)
207271

208-
self.attack_envelope = EnvelopeSegment(tween=attack_shape,
209-
change=peak_value, duration=attack_duration)
272+
self.attack_envelope = EnvelopeSegment(
273+
tween=attack_shape,
274+
change=peak_value,
275+
duration=attack_duration,
276+
label="attack",
277+
)
210278

211-
self.on_envelope = Envelope()
279+
self.on_envelope = Envelope(label="on-envelope")
212280
self.on_envelope.segments = [self.attack_envelope]
213281

214282
decay_change = -(peak_value - sustain_value)
283+
215284
if decay_change:
216-
self.decay_envelope = EnvelopeSegment(tween=decay_shape,
217-
start=peak_value, change=decay_change, duration=decay_duration)
218-
self.on_evenlope.segments.append(self.decay_envelope)
285+
self.decay_envelope = EnvelopeSegment(
286+
tween=decay_shape,
287+
start=peak_value,
288+
change=decay_change,
289+
duration=decay_duration,
290+
label="decay",
291+
)
292+
self.on_envelope.segments.append(self.decay_envelope)
219293
else:
220294
self.decay_envelope = None
221295

222-
self.sustain_envelope = StaticEnvelopeSegment(start=sustain_value)
296+
self.sustain_envelope = StaticEnvelopeSegment(start=sustain_value,
297+
label="sustain")
223298
self.on_envelope.segments.append(self.sustain_envelope)
224-
self.release_envelope = EnvelopeSegment(tween=release_shape,
225-
start=sustain_value, change=0-sustain_value, duration=release_duration)
226-
self.off_envelope = Envelope()
227-
if self.release_envelope.change and self.release_envelope.duration:
299+
self.release_envelope = EnvelopeSegment(
300+
tween=release_shape,
301+
start=sustain_value,
302+
change=0-sustain_value,
303+
duration=release_duration,
304+
label="release",
305+
)
306+
self.off_envelope = Envelope(label="off-envelope")
307+
if self.release_envelope.profile.change and self.release_envelope.duration:
228308
self.off_envelope.segments.append(self.release_envelope)
229309
self.segments = [self.on_envelope, self.off_envelope]
230310

tests/test_envelope.py

+48-6
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,10 @@ def test_simple_evelope():
3838
assert val == .8
3939

4040
def test_looping_envelope():
41-
s1 = envelope.EnvelopeSegment(start=0, change=1.0, duration=1.0)
42-
s2 = envelope.EnvelopeSegment(start=1, change=-1.0, duration=1.0)
43-
e = envelope.Envelope(loop=2)
44-
e.segments = [s1, s2]
41+
s1 = envelope.EnvelopeSegment(start=0, change=1.0, duration=1.0, label='a')
42+
s2 = envelope.EnvelopeSegment(start=1, change=-1.0, duration=1.0, label='b')
43+
e = envelope.Envelope(loop=2, label='looping-envelope')
44+
e.segments = [s1, s2] # 2 seconds for full envelope - 2 loops, 4 secs total
4545
# start off with 2 loops remaining
4646
assert e.loop_counter == 2
4747
for i in range(6):
@@ -55,11 +55,27 @@ def test_looping_envelope():
5555
assert val == 0
5656
e.loop = -1 # infinite loop
5757
e.reset()
58-
for i in range(51):
58+
for i in range(81):
5959
val = e.update(.5)
6060
assert e.loop_counter == -1
6161
assert val == .5
6262

63+
def test_nested_envelope():
64+
s1 = envelope.EnvelopeSegment(start=0, change=1.0, duration=1.0, label='seg1')
65+
s2 = envelope.EnvelopeSegment(start=1, change=-1.0, duration=1.0, label='seg2')
66+
s3 = envelope.EnvelopeSegment(start=0, change=1.0, duration=1.0, label='seg3')
67+
s4 = envelope.EnvelopeSegment(start=1, change=-1.0, duration=1.0, label='seg4')
68+
e1 = envelope.Envelope(label='e1')
69+
e2 = envelope.Envelope(label='e2')
70+
e3 = envelope.Envelope(label='e3')
71+
e1.segments = [s1, s2]
72+
e2.segments = [s3, s4]
73+
e3.segments = [e1, e2]
74+
e3.update(.5)
75+
assert e3.value == .5
76+
e3.update(.4)
77+
assert e3.value == .9
78+
6379
def test_trigger_envelope():
6480
s1 = envelope.StaticEnvelopeSegment(start=.3)
6581
s2 = envelope.EnvelopeSegment(start=.3, change=-.3, duration=1.0)
@@ -85,7 +101,7 @@ def test_trigger_envelope():
85101
for i in range(10):
86102
val = e.update(.5)
87103
assert val == 0
88-
assert e.index == 2
104+
assert e.index == 1
89105
e.trigger(state=1)
90106
assert e.index == 0
91107
# force state of partial value
@@ -95,4 +111,30 @@ def test_trigger_envelope():
95111
# value
96112
assert round(e.current_segment_time_delta, 2) == .33
97113

114+
def test_adsr_envelope():
115+
# envelope using defaults
116+
e = envelope.ADSREnvelope()
117+
e.trigger(state=1)
118+
e.update(.25)
119+
print e.index
120+
print e.segments
121+
# The attack segment of on segment
122+
d1_index = e.segments[e.index].index
123+
assert d1_index == 0
124+
d1_seg = e.segments[e.index].segments[d1_index]
125+
print d1_seg
126+
print d1_seg.__dict__
127+
assert e.value == .5
128+
e.update(.25)
129+
print e.current_segment_time_delta
130+
d1_index = e.segments[e.index].index
131+
assert d1_index == 0
132+
assert e.value == 1.0
133+
e.update(.1)
134+
# should have advanced to decay
135+
d1_index = e.segments[e.index].index
136+
assert d1_index == 1
137+
assert e.value == .9
138+
139+
98140

0 commit comments

Comments
 (0)