@@ -46,7 +46,7 @@ def __init__(self, d, x0=None, order=4):
46
46
47
47
def pad_const (x , n , axis = 0 ):
48
48
"""Prefix and postfix the array `x` by `n` repetitions of the first and
49
- last vlaue along `axis`.
49
+ last value along `axis`.
50
50
"""
51
51
a = np .repeat (x .take ([0 ], axis ), n , axis )
52
52
b = np .repeat (x .take ([- 1 ], axis ), n , axis )
@@ -86,9 +86,9 @@ class CoefficientSource:
86
86
def crop_x (self , start , stop , num = 2 ):
87
87
"""Return an array of valid sample positions.
88
88
89
- This function needs to be implemented only if this
90
- `CoefficientSource` does not support sampling at arbitrary
91
- positions .
89
+ This method needs to be overloaded if this `CoefficientSource`
90
+ does not support sampling at arbitrary positions or at arbitrary
91
+ density .
92
92
93
93
:param start: First sample position.
94
94
:param stop: Last sample position.
@@ -99,15 +99,23 @@ def crop_x(self, start, stop, num=2):
99
99
return np .linspace (start , stop , num )
100
100
101
101
def scale_x (self , x , scale ):
102
+ # TODO: This could be moved to the the Driver/Mediator code as it is
103
+ # device-specific.
102
104
"""Scale and round sample positions.
103
105
106
+ The sample times may need to be changed and/or decimated if
107
+ incompatible with hardware requirements.
108
+
104
109
:param x: Input sample positions in data space.
105
110
:param scale: Data space position to cycles conversion scale,
106
111
in units of x-units per clock cycle.
107
112
:return: `x_sample`, the rounded sample positions and `durations`, the
108
113
integer durations of the individual samples in cycles.
109
114
"""
110
- raise NotImplementedError
115
+ t = np .rint (x / scale )
116
+ x_sample = t * scale
117
+ durations = np .diff (t ).astype (np .int )
118
+ return x_sample , durations
111
119
112
120
def __call__ (self , x , ** kwargs ):
113
121
"""Perform sampling and return coefficients.
@@ -118,25 +126,18 @@ def __call__(self, x, **kwargs):
118
126
raise NotImplementedError
119
127
120
128
def get_segment_data (self , start , stop , scale , cutoff = 1e-12 ,
121
- min_duration = 1 , min_length = 20 ,
122
129
target = "bias" , variable = "amplitude" ):
123
130
"""Build wavesynth segment data.
124
131
125
132
:param start: see `crop_x()`.
126
133
:param stop: see `crop_x()`.
127
134
:param scale: see `scale_x()`.
128
- :param num: see `crop_x()`.
129
135
:param cutoff: coefficient cutoff towards zero to compress data.
130
- :param min_duration: Minimum duration of a line.
131
- :param min_length: Minimum segment length to space triggers.
132
136
"""
133
137
x = self .crop_x (start , stop )
134
138
x_sample , durations = self .scale_x (x , scale )
135
139
coefficients = self (x_sample )
136
- np .clip (np .fabs (durations ), min_duration , None , out = durations )
137
- if len (durations ) == 1 :
138
- durations [0 ] = max (durations [0 ], min_length )
139
- if start == stop :
140
+ if len (x_sample ) == 1 and start == stop :
140
141
coefficients = coefficients [:1 ]
141
142
# rescale coefficients accordingly
142
143
coefficients *= (scale * np .sign (durations ))** np .arange (
@@ -146,12 +147,14 @@ def get_segment_data(self, start, stop, scale, cutoff=1e-12,
146
147
return build_segment (durations , coefficients , target = target ,
147
148
variable = variable )
148
149
149
- def extend_segment (self , segment , * args , ** kwargs ):
150
+ def extend_segment (self , segment , trigger = True , * args , ** kwargs ):
150
151
"""Extend a wavesynth segment.
151
152
152
153
See `get_segment()` for arguments.
153
154
"""
154
- for line in self .get_segment_data (* args , ** kwargs ):
155
+ for i , line in enumerate (self .get_segment_data (* args , ** kwargs )):
156
+ if i == 0 :
157
+ line ["trigger" ] = True
155
158
segment .add_line (** line )
156
159
157
160
@@ -183,36 +186,42 @@ def crop_x(self, start, stop):
183
186
x = self .x [ia :ib ]
184
187
return np .r_ [start , x , stop ]
185
188
186
- def scale_x (self , x , scale , nudge = 1e-9 ):
189
+ def scale_x (self , x , scale , min_duration = 1 , min_length = 20 ):
190
+ """
191
+ Due to minimum duration and/or minimum segment length constraints
192
+ this method may drop samples from `x_sample` or adjust `durations` to
193
+ comply. But `x_sample` and `durations` should be kept consistent.
194
+
195
+ :param min_duration: Minimum duration of a line.
196
+ :param min_length: Minimum segment length to space triggers.
197
+ """
187
198
# We want to only sample a spline at t_knot + epsilon
188
199
# where the highest order derivative has just jumped
189
200
# and is valid at least up to the next knot after t_knot.
190
201
#
191
- # To ensure that we are on the right side of a knot:
202
+ # To ensure that we are on the correct side of a knot:
192
203
# * only ever increase t when rounding (for increasing t)
193
204
# * or only ever decrease it (for decreasing t)
194
- #
195
- # The highest derivative is discontinuous at t
196
- # and the correct value for a segment is obtained
197
- # for t_int >= t_float == t_knot (and v.v. for t decreasing).
198
- x = x / scale
199
- inc = np .diff (x ) >= 0
205
+ t = x / scale
206
+ inc = np .diff (t ) >= 0
200
207
inc = np .r_ [inc , inc [- 1 ]]
201
- x = np .where (inc , np .ceil (x + nudge ), np .floor (x - nudge ))
202
- if len (x ) > 1 and x [0 ] == x [1 ]:
203
- x = x [1 :]
204
- if len (x ) > 1 and x [- 2 ] == x [- 1 ]:
205
- x = x [:- 1 ]
206
- x_sample = x [:- 1 ]* scale
207
- durations = np .diff (x .astype (np .int ))
208
- return x_sample , durations
208
+ t = np .where (inc , np .ceil (t ), np .floor (t ))
209
+ dt = np .diff (t .astype (np .int ))
210
+
211
+ valid = np .absolute (dt ) >= min_duration
212
+ dt = dt [valid ]
213
+ t = t [np .r_ [True , valid ]]
214
+ if dt .shape [0 ] == 1 :
215
+ dt [0 ] = max (dt [0 ], min_length )
216
+ x_sample = t [:- 1 ]* scale
217
+ return x_sample , dt
209
218
210
219
def __call__ (self , x ):
211
220
return self .spline (x )
212
221
213
222
214
223
class ComposingSplineSource (SplineSource ):
215
- # TODO
224
+ # TODO: verify, test, document
216
225
def __init__ (self , x , y , components , order = 4 , pad_dx = 1. ):
217
226
self .x = np .asanyarray (x )
218
227
assert self .x .ndim == 1
@@ -236,18 +245,18 @@ def __init__(self, x, y, components, order=4, pad_dx=1.):
236
245
components , self .x , order )
237
246
238
247
def __call__ (self , t , gain = {}, offset = {}):
239
- der = list ((set (self .components .n ) | set (offset )) & set (self .der ))
248
+ der = list ((set (self .components .n ) | set (offset ))
249
+ & set (range (len (self .splines ))))
240
250
u = np .zeros ((self .splines [0 ].order , len (self .splines [0 ].s ), len (t )))
241
251
# der, order, ele, t
242
252
p = np .array ([self .splines [i ](t ) for i in der ])
243
- # order, der, None, t
244
- s = self .components (t )
245
253
s_gain = np .array ([gain .get (_ , 1. ) for _ in self .components .n ])
246
- s = s [:, :, None , :]* s_gain [None , :, None , None ]
254
+ # order, der, None, t
255
+ s = self .components (t )[:, :, None , :]* s_gain [None , :, None , None ]
247
256
for k , v in offset .items ():
248
- if v and k in self . der :
249
- u += v * p [self . der . index ( k ) ]
250
- ps = p [[ self .der . index ( _ ) for _ in self . shims .der ] ]
257
+ if v :
258
+ u += v * p [k ]
259
+ ps = p [self .shims .n ]
251
260
for i in range (u .shape [1 ]):
252
261
for j in range (i + 1 ):
253
262
u [i ] += binom (i , j )* (s [j ]* ps [:, i - j ]).sum (0 )
0 commit comments