CreativePython 1.1.6__py3-none-any.whl → 1.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -997,8 +997,6 @@ class DisplayMirror:
997
997
  else:
998
998
  item.qObject.setZValue(qZValue)
999
999
  self._scene.addItem(item.qObject) # attach QGraphicsItem to scene
1000
- cacheMode = QtWidgets.QGraphicsItem.CacheMode.DeviceCoordinateCache
1001
- item.qObject.setCacheMode(cacheMode)
1002
1000
 
1003
1001
  posArgs = {}
1004
1002
  if x is not None:
@@ -2874,9 +2872,6 @@ class GroupMirror(_DrawableMirror):
2874
2872
  item.qObject.setZValue(qZValue)
2875
2873
  self.qObject.addToGroup(item.qObject)
2876
2874
 
2877
- cacheMode = QtWidgets.QGraphicsItem.CacheMode.DeviceCoordinateCache
2878
- item.qObject.setCacheMode(cacheMode)
2879
-
2880
2875
  def _removeChild(self, args, responseId):
2881
2876
  """Removes a child from the group."""
2882
2877
  itemId = args.get('itemId')
@@ -306,9 +306,9 @@ class _RealtimeAudioPlayer:
306
306
  self.loopRegionStartFrame = max(0.0, float(loopRegionStartFrame))
307
307
  self.loopRegionEndFrame = float(loopRegionEndFrame) if loopRegionEndFrame is not None else -1.0
308
308
 
309
- # adjust the loop region end frame so it does not exceed the last valid frame in the audio file
309
+ # adjust the loop region end frame so it does not exceed the end of the audio file
310
310
  if self.loopRegionEndFrame > 0:
311
- self.loopRegionEndFrame = min(self.loopRegionEndFrame, self.numFrames - 1 if self.numFrames > 0 else 0.0)
311
+ self.loopRegionEndFrame = min(self.loopRegionEndFrame, float(self.numFrames))
312
312
 
313
313
  # ensure the loop region is valid: if the start frame is after or equal to the end frame, reset to default (full file)
314
314
  if self.numFrames > 0 and self.loopRegionEndFrame > 0 and self.loopRegionStartFrame >= self.loopRegionEndFrame:
@@ -689,7 +689,8 @@ class _RealtimeAudioPlayer:
689
689
 
690
690
  # determine where playback should end for this segment
691
691
  # NOTE: supports three modes: natural EOF, loop region end, or play(size) duration
692
- effectiveSegmentEndFrame = numAudioFrames - 1
692
+ # effectiveSegmentEndFrame is the exclusive end (first frame NOT to play)
693
+ effectiveSegmentEndFrame = float(numAudioFrames)
693
694
  if looping and self.loopRegionEndFrame > 0:
694
695
  effectiveSegmentEndFrame = self.loopRegionEndFrame
695
696
  elif not looping and self.targetEndSourceFrame > 0:
@@ -699,14 +700,13 @@ class _RealtimeAudioPlayer:
699
700
  # NOTE: batch boundary detection avoids per-sample checks (major optimization)
700
701
  framesToProcess = frames
701
702
  willHitBoundary = False
702
- boundaryFrame = -1
703
703
 
704
704
  if effectiveSegmentEndFrame > 0:
705
705
  framesToEndpoint = (effectiveSegmentEndFrame - self.playbackPosition) / rateFactor
706
- if framesToEndpoint < frames and framesToEndpoint > 0:
706
+ if 0 <= framesToEndpoint < frames:
707
707
  willHitBoundary = True
708
- boundaryFrame = int(np.floor(framesToEndpoint))
709
- framesToProcess = min(frames, boundaryFrame + 1)
708
+ # ceil gives the number of frames that stay strictly within the endpoint
709
+ framesToProcess = min(frames, int(np.ceil(framesToEndpoint)))
710
710
 
711
711
  ###########################################################################
712
712
  # PHASE 2: PITCH SHIFTING VIA TIME-STRETCH INTERPOLATION
@@ -856,7 +856,8 @@ class _RealtimeAudioPlayer:
856
856
  outdata[framesToProcess:, :] = 0.0
857
857
 
858
858
  # safety - fill any unfilled portion with silence
859
- if framesToProcess < frames and self.isPlaying:
859
+ # skip when willHitBoundary is True: boundary handling already filled remainder (via recursion or explicit zero-fill)
860
+ if framesToProcess < frames and self.isPlaying and not willHitBoundary:
860
861
  outdata[framesToProcess:, :] = 0.0
861
862
 
862
863
  # update pan smoothing (prevents clicks from abrupt pan changes)
animation.py ADDED
@@ -0,0 +1,554 @@
1
+ #######################################################################################
2
+ # animation.py Version 1.0 18-Apr-2026 Bill Manaris and Taj Ballinger
3
+ #######################################################################################
4
+
5
+
6
+ # The animation library provides a simplified way to automate repeated calling of functions
7
+ # for animation or other automation effects. Examples may range from continuously animating
8
+ # GUI graphics (similar to MIT Processing's draw() loop), to sequencing precise, data-driven
9
+ # musical automation (such as volume envelopes or filter sweeps).
10
+ #
11
+ # It uses a single "master" timer, which makes sure that all visual, audio, and other effects
12
+ # are perfectly synchronized (unlike creating individual timers for each). This is very important.
13
+ #
14
+ # It provides an Animate static class that can register functions to call repeatedly to support various scenarios.
15
+ # Anything more involved can be created using regular timers (via the timer library).
16
+ #
17
+ # Additionally, it provides a few shorthand functions, like animate(), setAnimationRate(), etc.
18
+ #
19
+ # NOTE: Animation engine is initialized (i.e., master timer starts ticking) at module load time!!!
20
+ # This doesn't cost much, and simplifies life...
21
+
22
+ class Animate:
23
+ """
24
+ Animate provides methods to automate calling of user-provided functions using a common, lock-step
25
+ time interval (or framerate).
26
+ """
27
+ _ENGINE = None # master timer, created and started at module load time
28
+ _interval = 1000.0 / 60.0 # current timer interval (in ms)
29
+ _actionList = [] # list of registered callback functions to automate
30
+
31
+
32
+ @staticmethod
33
+ def _init():
34
+ """
35
+ Initialize and start master animation timer.
36
+ """
37
+
38
+ # initialize only if engine has not been created previously
39
+ if Animate._ENGINE is None: # no engine available?
40
+
41
+ # yes, so create a new engine and start it
42
+ from timer import Timer
43
+
44
+ # use a regular timer (i.e., our master timer) to call every interval
45
+ # our tick function (which in turns calls all registered functions appropriately)
46
+ # Timer(delay, callback, arguments, repeat)
47
+ Animate._ENGINE = Timer(Animate._interval, Animate._tick, [], True) # create it
48
+ Animate._ENGINE.start() # and start it!!!
49
+
50
+ # now, the master timer is running, ready to call any function that gets registered
51
+
52
+
53
+ @staticmethod
54
+ def _tick():
55
+ """
56
+ Call each registered callback function once.
57
+ """
58
+ # iterate through the list of registered actions
59
+ for action in Animate._actionList:
60
+
61
+ if callable(action): # is this a function?
62
+
63
+ # yes, so execute it!!
64
+ action()
65
+
66
+ # now, all registered functions have been called one time
67
+
68
+
69
+ @staticmethod
70
+ def resume():
71
+ """
72
+ Resume the animation.
73
+ """
74
+
75
+ if Animate._ENGINE is not None: # do we have an engine?
76
+
77
+ # yes, so resume it (starting it continues where we left off...)
78
+ Animate._ENGINE.start()
79
+
80
+
81
+ @staticmethod
82
+ def pause():
83
+ """
84
+ Pause the animation.
85
+ """
86
+
87
+ if Animate._ENGINE is not None: # do we have an engine?
88
+
89
+ # yes, so pause it (stopping simply stops ticking - everything else remains in place!!)
90
+ Animate._ENGINE.stop()
91
+
92
+
93
+ @staticmethod
94
+ def add(action):
95
+ """
96
+ Register action (a callback function) to automate calling it.
97
+ A function may be registered several times (hopefully with different parameters).
98
+ """
99
+
100
+ if callable(action): # is this a function?
101
+
102
+ # yes, so add it to list of functions to be called
103
+ Animate._actionList.append(action)
104
+
105
+ else:
106
+
107
+ # let them know that something is wrong...
108
+ print(f"Animate.add(): '{action}' is not a callable function.")
109
+
110
+
111
+ @staticmethod
112
+ def addWithValues(action, values, duration=None, repeat=1, whenDone=None):
113
+ """
114
+ Register 'action' (a callback function) to be called repeatedly, passing it a sequence
115
+ of 'values' over a specified 'duration'. This will be repeated 'repeat' times.
116
+ When this process is finished, function 'whenDone' is called (if provided).
117
+ """
118
+
119
+ if not callable(action): # is it NOT a function?
120
+
121
+ # let them know something is wrong...
122
+ print(f"Animate.addWithValues(): '{action}' is not a callable function.")
123
+
124
+ elif len(values) == 0: # is the list empty?
125
+
126
+ # let them know something is wrong...
127
+ print("Animate.addWithValues(): 'values' list is empty.")
128
+
129
+ else: # value list is not empty, so...
130
+
131
+ if duration is None: # default duration?
132
+
133
+ timeDelayBetweenValues = Animate._interval # use engine's default interval
134
+
135
+ else: # they have provided their own time-stretching, so...
136
+
137
+ # calculate time delay (in millisecs) between values
138
+ timeDelayBetweenValues = (duration * 1000.0) / len(values)
139
+
140
+ # now, timeDelayBetweenValues contains how long we should wait to process next value...
141
+
142
+ # check if this makes sense...
143
+ if timeDelayBetweenValues < Animate._interval: # is duration provided too small for our frame rate?
144
+
145
+ # calculate the minimum possible duration for the error message
146
+ minAllowableDuration = (Animate._interval * len(values)) / 1000.0
147
+
148
+ # let them know something is wrong...
149
+ print(f"Animate.addWithValues(): duration ({duration}s) is too short to process, given frame rate ({Animate.getRate()})")
150
+ print(f" Should be at least {minAllowableDuration:.2f} secs.")
151
+
152
+ else: # all good, so proceed...
153
+
154
+ import time
155
+
156
+ #####
157
+ # Now, we will build a closure (inner function) to handle processing each individual value,
158
+ # as well as anything else that may need to happen (i.e., start over for the next repetition,
159
+ # or call the 'whenDone' function if we are completely done - after the last value).
160
+
161
+ # A closure needs some non-local (i.e., external to it) variables to remember how far it has gone.
162
+
163
+ # NOTE: Every time addWithValues() above gets called, new variables are created here,
164
+ # and these are combined with the closure below, maintaining its state persistently
165
+ # without overwriting each other - it works well.
166
+
167
+ # external variables to be wrapped within the closure
168
+ valuesIndex = 0 # points to next value to be processed
169
+ repeatCounter = 0 # how many repetitions done so far
170
+
171
+ # timer delay (in millisecs) for each value
172
+ delay = timeDelayBetweenValues
173
+
174
+ # remembers when the previous execution happened
175
+ previousTime = time.time() * 1000.0 # initialize to "now"...
176
+
177
+ # define an inner function (closure) that contains the logic of what to do
178
+ # with each provided value...
179
+ def processEachValueClosure():
180
+
181
+ nonlocal previousTime # remembers when previous execution happened
182
+ nonlocal valuesIndex # remembers which value is to be processed this time
183
+ nonlocal repeatCounter # remembers how many times we have processed the complete value list
184
+
185
+ # get current time (in millisecs)
186
+ currentTime = time.time() * 1000.0
187
+
188
+ # has enough time passed to trigger the next value?
189
+ if (currentTime - previousTime) >= delay:
190
+
191
+ # yes, update time tracker
192
+ previousTime = previousTime + delay
193
+
194
+ # act only if there are remaining values to be processed
195
+ if valuesIndex < len(values): # any left?
196
+
197
+ # yes, so...
198
+
199
+ # get next value
200
+ value = values[valuesIndex]
201
+
202
+ # NOTE: this is the important step that advances the animation!!!
203
+ action(value) # execute the original callback with the current value
204
+
205
+ # point to next value to be processed (if any)
206
+ valuesIndex = valuesIndex + 1
207
+
208
+ # now, the current value has been processed...
209
+
210
+ # have we completed processing all the values?
211
+ if valuesIndex >= len(values): # no more values left?
212
+
213
+ # yes, we are done... so, advance to the next repetition (if any)
214
+ repeatCounter = repeatCounter + 1
215
+
216
+ # do we have more repetitions?
217
+ if repeat == -1 or repeatCounter < repeat:
218
+
219
+ # yes, so start processing values once more
220
+ valuesIndex = 0 # point to first value
221
+
222
+ else: # we are done repeating, so...
223
+
224
+ # remove this inner function from the engine...
225
+ Animate.remove( processEachValueClosure )
226
+
227
+ # and execute the callback (if any)
228
+ if callable(whenDone): # is there a completion callback?
229
+
230
+ # yes, so execute it!!
231
+ whenDone()
232
+
233
+ # now, we have finished processing current value, and have taken care
234
+ # of anything else that may have needed to happen (i.e., start over next repetition,
235
+ # or called 'whenDone' function, if we are completely done - after this value)
236
+
237
+ # NOTE: Very important - now that this inner function has been built,
238
+ # we need to register it with our engine, so it can do its job...
239
+
240
+ # register the closure to be called - it will take care of everything else...
241
+ Animate._actionList.append(processEachValueClosure)
242
+
243
+
244
+ @staticmethod
245
+ def addWithTimedValues(action, values, times, duration=None, repeat=1, whenDone=None):
246
+ """
247
+ Register 'action' (a callback function) to be called repeatedly.
248
+ 'values' is a list of sublists containing the parameters to be passed to the action.
249
+ 'times' is a parallel list containing the target timestamps (in millisecs) for each value.
250
+ This will be repeated 'repeat' times.
251
+ When this process is finished, function 'whenDone' is called (if provided).
252
+ """
253
+
254
+ if not callable(action): # is it NOT a function?
255
+
256
+ # let them know that something is wrong...
257
+ print(f"Animate.addWithTimedValues(): '{action}' is not a callable function.")
258
+
259
+ elif len(values) == 0: # is the list empty?
260
+
261
+ # let them know that something is wrong...
262
+ print("Animate.addWithTimedValues(): 'values' list is empty.")
263
+
264
+ elif len(values) != len(times): # are the lists not parallel?
265
+
266
+ # let them know that something is wrong...
267
+ print(f"Animate.addWithTimedValues(): length of 'values' ({len(values)}) and 'times' ({len(times)}) must be the same.")
268
+
269
+ else: # lists are non-empty and parallel, so...
270
+
271
+ # calculate all raw intervals between consecutive times
272
+ # (if list has only 1 item, intervals will be empty)
273
+ intervals = [times[i] - times[i-1] for i in range(1, len(times))]
274
+
275
+ # check for simultaneous or out-of-order timestamps
276
+ if len(intervals) > 0 and min(intervals) <= 0:
277
+
278
+ # let them know that something is wrong...
279
+ print(f"Animate.addWithTimedValues(): all timestamps should be larger than their previous one.")
280
+
281
+ else: # timestamps are valid and strictly increasing, so check engine limits...
282
+
283
+ # initialize default time scale (no time-stretching)
284
+ timeScale = 1.0
285
+
286
+ if duration is not None: # did they provide duration?
287
+
288
+ # find the original duration (very last element in the times sequence)
289
+ originalDuration = float(times[-1])
290
+
291
+ if originalDuration > 0:
292
+
293
+ # calculate scale factor to stretch/compress the overall timing
294
+ timeScale = (duration * 1000.0) / originalDuration
295
+
296
+ # adjust the smallest raw interval by our timeScale to see what the engine will actually process
297
+ isTooSmall = False
298
+
299
+ if len(intervals) > 0:
300
+
301
+ minInterval = min(intervals) * timeScale
302
+
303
+ if minInterval < Animate._interval: # is the smallest gap too tight?
304
+
305
+ isTooSmall = True # remember that
306
+
307
+ # now, either isTooSmall is True, meaning the tightest gap is invalid, or
308
+ # it is False, meaning all intervals are valid
309
+
310
+ if isTooSmall: # did we find an interval that is too small?
311
+
312
+ # calculate what the scale factor WOULD need to be to make the smallest gap fit the engine
313
+ # (we can safely divide by min(intervals) here because we proved > 0 in the previous block)
314
+ requiredScaleFactor = Animate._interval / float(min(intervals))
315
+
316
+ # calculate the minimum allowable duration (in seconds) based on that scale factor
317
+ originalDuration = float(times[-1])
318
+ minAllowableDuration = (requiredScaleFactor * originalDuration) / 1000.0
319
+
320
+ if duration is not None: # did they provide a duration?
321
+
322
+ # let them know their duration is too short
323
+ print(f"Animate.addWithTimedValues(): duration ({duration} secs) is too small to process, given frame rate ({Animate.getRate()}).")
324
+ print(f" Should be at least {minAllowableDuration:.2f} secs.")
325
+
326
+ else: # they didn't provide a duration, but the raw timestamps are too tight
327
+
328
+ # let them know they need to stretch the sequence
329
+ print(f"Animate.addWithTimedValues(): found time interval that is too small ({minInterval} secs), given frame rate ({Animate.getRate()}).")
330
+ print(f" Increase interval between times, or provide a duration of at least {minAllowableDuration:.2f} secs.")
331
+
332
+ else: # all good, so proceed...
333
+
334
+ import time
335
+
336
+ #####
337
+ # Now, we will build a closure (inner function) to handle processing each individual value,
338
+ # as well as anything else that may need to happen (i.e., start over for the next repetition,
339
+ # or call the 'whenDone' function if we are completely done - after the last value).
340
+
341
+ # A closure needs some non-local (i.e., external to it) variables to remember how far it has gone.
342
+
343
+ # NOTE: Every time addWithTimedValues() above gets called, new variables are created here,
344
+ # and these are combined with the closure below, maintaining its state persistently
345
+ # without overwriting each other - it works well.
346
+
347
+ # external variables to be wrapped within the closure
348
+ valuesIndex = 0 # points to next value to be processed
349
+ repeatCounter = 0 # how many repetitions done so far
350
+
351
+ # remembers when the current animation sequence started
352
+ sequenceStartTime = time.time() * 1000.0 # initialize to "now"...
353
+
354
+ # define an inner function (closure) that contains the logic of what to do
355
+ # with each provided value...
356
+ def processEachTimedValueClosure():
357
+
358
+ nonlocal sequenceStartTime # remembers when the current sequence repetition started
359
+ nonlocal valuesIndex # remembers which value is to be processed this time
360
+ nonlocal repeatCounter # remembers how many times we have processed the complete value list
361
+
362
+ # get current time (in millisecs)
363
+ currentTime = time.time() * 1000.0
364
+
365
+ # calculate how much time has elapsed since the sequence started
366
+ elapsedTime = currentTime - sequenceStartTime
367
+
368
+ # get the target timestamp directly from the times list (adjusted by our timeScale)
369
+ targetTimestamp = times[valuesIndex] * timeScale
370
+
371
+ # has enough time passed to trigger the next value based on its target timestamp?
372
+ if elapsedTime >= targetTimestamp:
373
+
374
+ # yes, so...
375
+
376
+ # get the arguments directly from the values list
377
+ actionArgs = values[valuesIndex]
378
+
379
+ # NOTE: this is the important step that advances the animation!!!
380
+ # unpack the arguments (*) and execute the original callback
381
+ action(*actionArgs)
382
+
383
+ # point to next value to be processed (if any)
384
+ valuesIndex = valuesIndex + 1
385
+
386
+ # now, the current value has been processed...
387
+
388
+ # have we completed processing all the values?
389
+ if valuesIndex >= len(values): # no more values left?
390
+
391
+ # yes, we are done... so, advance to the next repetition (if any)
392
+ repeatCounter = repeatCounter + 1
393
+
394
+ # do we have more repetitions?
395
+ if repeat == -1 or repeatCounter < repeat:
396
+
397
+ # yes, so start processing values once more
398
+ valuesIndex = 0 # point to first value
399
+
400
+ # reset the sequence start time for the next repetition cycle
401
+ sequenceStartTime = time.time() * 1000.0
402
+
403
+ else: # we are done repeating, so...
404
+
405
+ # remove this inner function from the engine...
406
+ Animate.remove( processEachTimedValueClosure )
407
+
408
+ # and execute the callback (if any)
409
+ if callable(whenDone): # is there a completion callback?
410
+
411
+ # yes, so execute it!!
412
+ whenDone()
413
+
414
+ # now, we have finished processing current value, and have taken care
415
+ # of anything else that may have needed to happen (i.e., start over next repetition,
416
+ # or called 'whenDone' function, if we are completely done - after this value)
417
+
418
+ # NOTE: Very important - now that this inner function has been built,
419
+ # we need to register it with our engine, so it can do its job...
420
+
421
+ # register the closure to be called - it will take care of everything else...
422
+ Animate._actionList.append(processEachTimedValueClosure)
423
+
424
+
425
+ @staticmethod
426
+ def remove(action):
427
+ """
428
+ Remove a callback function from the list of functions being called.
429
+ If a function is registered several times, it removes the earliest addition.
430
+ """
431
+
432
+ if action in Animate._actionList: # is this a previously registered function?
433
+
434
+ # yes, so remove first matching occurrence
435
+ Animate._actionList.remove(action)
436
+
437
+ else:
438
+
439
+ # let them know that something is wrong...
440
+ print(f"Animate.remove(): '{action}' does not appear in the list of registered functions.")
441
+
442
+
443
+ @staticmethod
444
+ def getRate():
445
+ """
446
+ Return current animation rate (in frames per second).
447
+ """
448
+
449
+ # convert milliseconds back to frames per second
450
+ return int(1000.0 / Animate._interval)
451
+
452
+
453
+ @staticmethod
454
+ def setRate(frameRate=60):
455
+ """
456
+ Set current animation rate (in frames per second).
457
+ """
458
+
459
+ # convert frames per second to milliseconds
460
+ Animate._interval = (1000.0 / frameRate)
461
+
462
+ if Animate._ENGINE is not None: # do we have an engine?
463
+
464
+ # yes, so update the engine
465
+ Animate._ENGINE.setDelay(Animate._interval)
466
+
467
+
468
+ #####
469
+ # NOTE: This is important - initialize engine and start master timer at module load time.
470
+ # Leave it here!!!
471
+ Animate._init()
472
+
473
+
474
+ ####
475
+ # function aliases for backwards compatibility / simplicity
476
+ def animate(action):
477
+ """
478
+ Add a function to be called repeatedly by animation engine.
479
+ """
480
+
481
+ Animate.add(action)
482
+
483
+
484
+ def setAnimationRate(frameRate=60):
485
+ """
486
+ Set animation frame rate (frames per second).
487
+ """
488
+
489
+ Animate.setRate(frameRate)
490
+
491
+
492
+ def getAnimationRate():
493
+ """
494
+ Return current animation frame rate (frames per second).
495
+ """
496
+
497
+ return Animate.getRate()
498
+
499
+
500
+ def pauseAnimation():
501
+ """
502
+ Pause animation (everything else remains in place).
503
+ """
504
+ Animate.pause()
505
+
506
+
507
+ def resumeAnimation():
508
+ """
509
+ Resume stopped animation (everything continues from where it had left off).
510
+ """
511
+
512
+ Animate.resume()
513
+
514
+
515
+ ####
516
+ # Helper functions
517
+
518
+ from music import mapValue # import mapValue from music library
519
+
520
+ def mapValueList(values, minValue, maxValue, minResult, maxResult):
521
+ """
522
+ Map a sequence of 'values' from their original range (defined by 'minValue'
523
+ and 'maxValue') to a new range defined by 'minResult' and 'maxResult'.
524
+ Returns a new list containing the mapped values.
525
+ """
526
+
527
+ newValues = [] # holds mapped values
528
+
529
+ # do some sanity checking...
530
+ if type(values) is not list: # are the provided values NOT in a list?
531
+
532
+ # let them know something is wrong...
533
+ print(f"mapValueList(): '{values}' is not a valid list.")
534
+
535
+ elif len(values) == 0: # is the list empty?
536
+
537
+ # let them know something is wrong...
538
+ print("mapValueList(): 'trajectoryValues' list is empty.")
539
+
540
+ else: # all good, so proceed...
541
+
542
+ # map each value in provided list
543
+ for value in values:
544
+
545
+ # map this value from source to target range
546
+ newValue = mapValue(value, minValue, maxValue, minResult, maxResult)
547
+
548
+ # and remember it...
549
+ newValues.append(newValue)
550
+
551
+ # now, newValues contains scaled values...
552
+
553
+ # and return it (empty, if validation checks fail)
554
+ return newValues
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: CreativePython
3
- Version: 1.1.6
3
+ Version: 1.2
4
4
  Summary: A Python-based software environment for developing algorithmic art projects.
5
5
  Author-email: "Dr. Bill Manaris" <manaris@cofc.edu>, Taj Ballinger <ballingertj@g.cofc.edu>, Trevor Ritchie <ritchiets@g.cofc.edu>
6
6
  License: MIT License
@@ -1,15 +1,16 @@
1
- gui.py,sha256=io7p7aYK9EqcP9lBbBO3CBycaAvaHs3IxNw5BrOhhgs,154434
1
+ animation.py,sha256=MwsZS9ERxs_99Q_LP732EOK7oP5E6ZDYKo--JbzxkmY,22893
2
+ gui.py,sha256=X3SDyR0hbGDVRzjc16JqzYiCeHf1hl5nHcY6UpXfn2w,151268
2
3
  iannix.py,sha256=jlBJhPzBcIl2iayG01ldoslAytxYOvP3z_vN-EgiJA4,17188
3
4
  image.py,sha256=wohpTTZH6QzN3wRfXI5ac7VTRsOKcwqk8_d3yha8oDw,3763
4
5
  markov.py,sha256=VhTXZfDDHETj6KhriFmOLHKNjgx08cWZvpLECuGRTG0,24653
5
6
  midi.py,sha256=avVY6dyTHbFazt10w2Uy0zUPXvRYleZM2O89gDX3-h4,27203
6
- music.py,sha256=VyYcjxEIRYG7YWFzBIbSFaR4mVcO2fHDZnkdCh0G4sM,268532
7
+ music.py,sha256=FTDEGCemb8Yy4SVgP7_6fklD7GLPNW7kk1Gvawr7huc,268347
7
8
  osc.py,sha256=GtjWV5LD4xiW9Yen3PzFYstWHhEZ_QyrKunAu-VsPrc,8353
8
9
  timer.py,sha256=uQ1BHWTjZ6DpJo-leGhxgOJcVcd1Lj0jvQQodXx1kw4,16341
9
10
  zipf.py,sha256=J6fWwUYz88LFAwanc0TmXJlU7VtpBM24_Rw5XJTOn9c,40312
10
11
  CreativePython/GuiHandler.py,sha256=ONnxhqivBrzCHQv7GcE4vkMgnONbuXXq7Fwe0Au5QPs,30974
11
- CreativePython/GuiRenderer.py,sha256=XUXhPjtLRUA3D0Q05ZP85gb3gPqg8Zvs732bSYBOVo4,136843
12
- CreativePython/RealtimeAudioPlayer.py,sha256=tjqBjSOvSvH1kP0t6q35xsbKHPr0wuLUvGdxdWAY73E,44590
12
+ CreativePython/GuiRenderer.py,sha256=bR6mr32G9-VZgLUxXUI34VGYPDCNBm19iONKdGdkUBc,136596
13
+ CreativePython/RealtimeAudioPlayer.py,sha256=Yx79vsXVCQyBw-VVaRHk2FUDTfjxS9Y8coLrnVhjleU,44773
13
14
  CreativePython/__init__.py,sha256=bT1nZ-Fp8-7uiKqhVdnFeOwL-8Jps2rvE6BfoOE8fZ0,451
14
15
  CreativePython/notationRenderer.py,sha256=2KIMvInNLEUZyn_r1KMmyt7X2XDrfekVkggFeHLRu1M,35440
15
16
  CreativePython/nevmuse/RunMetrics.py,sha256=wP5HeS7u5VmH9s-9yyF1EuvF8xFcU6oYTT90Qr16Mu0,4917
@@ -77,9 +78,9 @@ CreativePython/nevmuse/utilities/CSVWriter.py,sha256=k2GccvzD5IwdkmFdN5qtRA2qZX1
77
78
  CreativePython/nevmuse/utilities/PowerLawRandom.py,sha256=WIEI-p8wiiWhkkBR5pO9gJ2TwxZBw453T_xXzCNdKPY,3752
78
79
  CreativePython/nevmuse/utilities/__init__.py,sha256=pAGIMMGfLvR2DQfxr8qkfKQJ-4fIkVqlhnGK657JPL8,166
79
80
  bin/libportaudio.2.dylib,sha256=ooJlm7QeL7jYzc1c8YMmH-C8PhYHwDvBJZkNDVPFRbE,286592
80
- creativepython-1.1.6.dist-info/licenses/LICENSE,sha256=tz38qDa5RhHMqzZR1Q5QafqWdXn_EhheXYBP7FjgtEM,1992
81
- creativepython-1.1.6.dist-info/licenses/LICENSE-PSF,sha256=JN1jUPIsEUH1mq3AXxet-b1IdhAr-XzscXBZiz74c9c,3371
82
- creativepython-1.1.6.dist-info/METADATA,sha256=MGKp9oy0r3G1TsnJxa2M9WtRlP_4X7bGv-05HyB2z0s,8703
83
- creativepython-1.1.6.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
84
- creativepython-1.1.6.dist-info/top_level.txt,sha256=PzvvUyy-3YzTedVGnLS02ju1TCMuo76h1LuVd8XvHhA,69
85
- creativepython-1.1.6.dist-info/RECORD,,
81
+ creativepython-1.2.dist-info/licenses/LICENSE,sha256=tz38qDa5RhHMqzZR1Q5QafqWdXn_EhheXYBP7FjgtEM,1992
82
+ creativepython-1.2.dist-info/licenses/LICENSE-PSF,sha256=JN1jUPIsEUH1mq3AXxet-b1IdhAr-XzscXBZiz74c9c,3371
83
+ creativepython-1.2.dist-info/METADATA,sha256=D9cNlCjyoFp4BOp8Gpk5AUgnPEoUNpFkTm9eZPK_fAo,8701
84
+ creativepython-1.2.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
85
+ creativepython-1.2.dist-info/top_level.txt,sha256=mgvaKABt8E3WvEYKgGplbpiJqmjFXVCeD4RZIf7EsHs,79
86
+ creativepython-1.2.dist-info/RECORD,,
@@ -1,4 +1,5 @@
1
1
  CreativePython
2
+ animation
2
3
  bin
3
4
  gui
4
5
  iannix
gui.py CHANGED
@@ -2058,9 +2058,18 @@ class Circle(Oval):
2058
2058
  """
2059
2059
  Sets the item's radius (in pixels).
2060
2060
  """
2061
+ # find change in radius
2062
+ oldRadius = self.getRadius()
2063
+ change = oldRadius - radius
2064
+
2065
+ # adjust radius
2061
2066
  diameter = radius * 2
2062
2067
  self.setWidth(diameter)
2063
2068
 
2069
+ # adjust top-left corner position, since circles are positioned relative to center
2070
+ x, y = self.getPosition()
2071
+ self.setPosition(x + change, y + change)
2072
+
2064
2073
  # ── Hit testing ────────────────────────────────────────────────────────────
2065
2074
 
2066
2075
  def intersects(self, other):
@@ -4207,130 +4216,6 @@ class Menu():
4207
4216
  _handler().sendCommand('disable', self._objectId)
4208
4217
 
4209
4218
 
4210
- #######################################################################################
4211
- # Animate
4212
- #######################################################################################
4213
- # Animate is a static class that can register functions to call repeatedly.
4214
- # This is essentially a Timer, but with a different API designed for students.
4215
- # This is basic functionality, inspired by MIT Processing's draw() function.
4216
- # Anything more involved can be easily created using a Timer.
4217
-
4218
- class Animate:
4219
- """
4220
- Animate provides methods to animate graphics and other function behavior at a
4221
- global interval/framerate.
4222
- """
4223
- _ENGINE = None # animation timer, created the first time add() is called
4224
- _rate = 1000 / 60 # current timer interval (in ms)
4225
- _actionList = [] # list of registered callback functions
4226
-
4227
- @staticmethod
4228
- def _startEngine():
4229
- """
4230
- Starts the animation engine.
4231
- """
4232
- if Animate._ENGINE is None:
4233
- # create a new engine and start it
4234
- from timer import Timer
4235
- Animate._ENGINE = Timer(Animate._rate, Animate.tick, [], True)
4236
- Animate._ENGINE.start()
4237
-
4238
- @staticmethod
4239
- def tick():
4240
- """
4241
- Call each registered callback function once.
4242
- """
4243
- for action in Animate._actionList:
4244
- if callable(action):
4245
- action()
4246
-
4247
- @staticmethod
4248
- def resume():
4249
- """
4250
- Continues the animation.
4251
- """
4252
- if Animate._ENGINE is not None:
4253
- Animate._ENGINE.start()
4254
-
4255
- @staticmethod
4256
- def pause():
4257
- """
4258
- Temporarily stops the animation.
4259
- """
4260
- if Animate._ENGINE is not None:
4261
- Animate._ENGINE.stop()
4262
-
4263
- @staticmethod
4264
- def add(action):
4265
- """
4266
- Register a new callback function to animate.
4267
- A function can be registered multiple times, but must be removed
4268
- """
4269
- if callable(action):
4270
- Animate._actionList.append(action)
4271
- Animate._startEngine() # start lazily on first add, after the fork
4272
- else:
4273
- print(f"Animate.add(): '{action.__name__}()' isn't a callable function.")
4274
-
4275
- @staticmethod
4276
- def remove(action):
4277
- """
4278
- Deregisters a callback function that's being animated.
4279
- If a function is registered multiple times, this only removes the earliest time it was registered.
4280
- """
4281
- if action in Animate._actionList:
4282
- Animate._actionList.remove(action)
4283
-
4284
- @staticmethod
4285
- def getRate():
4286
- """
4287
- Returns the current animation rate (in frames per second).
4288
- """
4289
- return int(1000 / Animate._rate)
4290
-
4291
- @staticmethod
4292
- def setRate(frameRate):
4293
- """
4294
- Sets the current animation rate.
4295
- """
4296
- Animate._rate = (1000 / frameRate)
4297
-
4298
- if Animate._ENGINE is not None:
4299
- # if an animation is started, also update the engine
4300
- Animate._ENGINE.setDelay(Animate._rate)
4301
-
4302
- # aliases for backwards compatibility
4303
- def animate(action):
4304
- """
4305
- Adds a function to be called repeatedly by the animation engine.
4306
- """
4307
- Animate.add(action)
4308
-
4309
- def setAnimationRate(frameRate=60):
4310
- """
4311
- Set animation frame rate (frames per second).
4312
- """
4313
- Animate.setRate(frameRate)
4314
-
4315
- def getAnimationRate():
4316
- """
4317
- Returns animation frame rate (frames per second).
4318
- """
4319
- return Animate.getRate()
4320
-
4321
- def pauseAnimation():
4322
- """
4323
- Temporarily stops animation.
4324
- """
4325
- Animate.pause()
4326
-
4327
- def resumeAnimation():
4328
- """
4329
- Resumes stopped animation.
4330
- """
4331
- Animate.resume()
4332
-
4333
-
4334
4219
  #######################################################################################
4335
4220
  # Test
4336
4221
  #######################################################################################
music.py CHANGED
@@ -2565,7 +2565,7 @@ class Play:
2565
2565
  # (this needs to happen here every time, as we may be using the tempo from score, part, or phrase)
2566
2566
  FACTOR = 1000 * 60.0 / tempo
2567
2567
 
2568
- for index in range(phrase.length()): # traverse all notes in this phrase
2568
+ for index in range(phrase.getSize()): # traverse all notes in this phrase
2569
2569
  note = phrase.getNote(index) # and extract needed note data
2570
2570
  frequency = note.getFrequency()
2571
2571
  panning = note.getPan()
@@ -3087,6 +3087,40 @@ class Mod():
3087
3087
  compressScore(material, mean, ratio)
3088
3088
  # now, the dynamic of every note has been adjusted
3089
3089
 
3090
+ @staticmethod
3091
+ def crescendo(material, startTime, endTime, startVolume, endVolume):
3092
+ """
3093
+ Linearly interpolates volume from startVolume to endVolume for notes
3094
+ within [startTime, endTime] (quarter notes, MIDI volumes 0-127).
3095
+ Works on a Phrase, Part, or Score.
3096
+ """
3097
+ for name, val in [("startTime", startTime), ("endTime", endTime),
3098
+ ("startVolume", startVolume), ("endVolume", endVolume)]:
3099
+ if type(val) not in [int, float]:
3100
+ raise TypeError(f"Unrecognized {name} type {type(val)} - expected int or float.")
3101
+
3102
+ if startTime >= endTime:
3103
+ raise ValueError(f"startTime ({startTime}) must be less than endTime ({endTime}).")
3104
+
3105
+ if isinstance(material, Score):
3106
+ for part in material.getPartList():
3107
+ Mod.crescendo(part, startTime, endTime, startVolume, endVolume)
3108
+
3109
+ elif isinstance(material, Part):
3110
+ for phrase in material.getPhraseList():
3111
+ Mod.crescendo(phrase, startTime, endTime, startVolume, endVolume)
3112
+
3113
+ elif isinstance(material, Phrase):
3114
+ durationCounter = material.getStartTime()
3115
+ for note in material.getNoteList():
3116
+ if startTime <= durationCounter < endTime:
3117
+ newVolume = mapValue(durationCounter, startTime, endTime, startVolume, endVolume)
3118
+ note.setDynamic(newVolume)
3119
+ durationCounter += note.getDuration()
3120
+
3121
+ else:
3122
+ raise TypeError(f"Unrecognized material type {type(material)} - expected Phrase, Part, or Score.")
3123
+
3090
3124
  @staticmethod
3091
3125
  def consolidate(part):
3092
3126
  """
@@ -3203,106 +3237,63 @@ class Mod():
3203
3237
  @staticmethod
3204
3238
  def fadeIn(material, fadeLength):
3205
3239
  """
3206
- Linearly fades in the material (fadeLength is quarter notes).
3240
+ Linearly fades in the material from silence over fadeLength quarter notes,
3241
+ starting from time 0. Works on a Phrase, Part, or Score.
3207
3242
  """
3208
-
3209
- # define helper functions
3210
- def fadeInPhrase(phrase, startTime=0.0):
3211
- """Helper function to fadeIn a single phrase."""
3212
- durationCounter = startTime # track how far into piece we are
3213
-
3214
- # for each note in phrase
3215
- for note in phrase.getNoteList():
3216
- fadeFactor = durationCounter / fadeLength # calculate fraction of fading needed
3217
-
3218
- if fadeFactor >= 1: # check if fade is over
3219
- break
3220
-
3221
- newDynamic = note.getDynamic() * fadeFactor # calculate new faded dynamic
3222
- newDynamic = max(1, newDynamic) # keep dynamic above 0
3223
-
3224
- note.setDynamic(int(newDynamic)) # update note dynamic
3225
-
3226
- durationCounter += note.getDuration() # update time tracker
3227
-
3228
- def fadeInPart(part, startTime=0.0):
3229
- """Helper function to fadeIn a single part."""
3230
- for phrase in part.getPhraseList():
3231
- fadeInPhrase(phrase, startTime + phrase.getStartTime())
3232
-
3233
- def fadeInScore(score):
3234
- """Helper function to fadeIn a score."""
3235
- for part in score.getPartList():
3236
- fadeInPart(part, part.getStartTime())
3237
-
3238
- # check type of time
3239
3243
  if type(fadeLength) not in [float, int]:
3240
3244
  raise TypeError(f"Unrecognized fadeLength type {type(fadeLength)} - expected int or float.")
3241
3245
 
3242
- # check type of material and call the appropriate function
3243
3246
  if isinstance(material, Score):
3244
- fadeInScore(material)
3247
+ for part in material.getPartList():
3248
+ Mod.fadeIn(part, fadeLength)
3245
3249
 
3246
3250
  elif isinstance(material, Part):
3247
- fadeInPart(material)
3251
+ for phrase in material.getPhraseList():
3252
+ Mod.fadeIn(phrase, fadeLength)
3248
3253
 
3249
3254
  elif isinstance(material, Phrase):
3250
- fadeInPhrase(material)
3255
+ durationCounter = material.getStartTime()
3256
+ for note in material.getNoteList():
3257
+ fadeFactor = durationCounter / fadeLength
3258
+ if fadeFactor >= 1:
3259
+ break
3260
+ note.setDynamic(int(max(1, note.getDynamic() * fadeFactor)))
3261
+ durationCounter += note.getDuration()
3251
3262
 
3252
- else: # error check
3263
+ else:
3253
3264
  raise TypeError(f"Unrecognized material type {type(material)} - expected Phrase, Part, or Score.")
3254
3265
 
3255
3266
  @staticmethod
3256
- def fadeOut(material, fadeLength):
3267
+ def fadeOut(material, fadeLength, _endTime=None):
3257
3268
  """
3258
- Linearly fades out the material (fadeLength is quarter notes).
3269
+ Linearly fades out the material to silence over the last fadeLength quarter notes.
3270
+ Works on a Phrase, Part, or Score.
3259
3271
  """
3272
+ if type(fadeLength) not in [float, int]:
3273
+ raise TypeError(f"Unrecognized fadeLength type {type(fadeLength)} - expected int or float.")
3260
3274
 
3261
- # define helper functions
3262
- def fadeOutPhrase(phrase, endTime):
3263
- """Helper function to fadeOut a single phrase."""
3264
- durationCounter = endTime # how far from the end are we?
3265
-
3266
- # for each note in phrase, starting from end
3267
- for note in phrase.getNoteList()[::-1]:
3268
- fadeFactor = durationCounter / fadeLength # calculate fraction of fading needed
3269
-
3270
- if fadeFactor >= 1: # check if fade is over
3271
- break
3272
-
3273
- newDynamic = note.getDynamic() * fadeFactor # calculate new faded dynamic
3274
- newDynamic = max(1, newDynamic) # keep dynamic above 0
3275
-
3276
- note.setDynamic(int(newDynamic)) # update note dynamic
3277
-
3278
- durationCounter += note.getDuration() # update time tracker
3279
-
3280
- def fadeOutPart(part, endTime):
3281
- """Helper function to fadeOut a single part."""
3282
- for phrase in part.getPartList():
3283
- fadeOutPart(phrase, endTime - phrase.getEndTime())
3284
-
3285
- def fadeOutScore(score):
3286
- """Helper function to fadeOut a score."""
3287
- for part in score.getPartList():
3288
- fadeOutPart(part, score.getEndTime() - part.getEndTime())
3289
-
3290
- # check type of time
3291
- if type(fadeLength) not in [float, int]:
3292
- raise TypeError(f"Unrecognized fadeLength type {type(fadeLength)} - expected int or float.")
3275
+ if _endTime is None:
3276
+ _endTime = material.getEndTime()
3293
3277
 
3294
- # check type of material and call the appropriate function
3295
- if isinstance(material, Score):
3296
- fadeOutScore(material)
3278
+ if isinstance(material, Score):
3279
+ for part in material.getPartList():
3280
+ Mod.fadeOut(part, fadeLength, _endTime)
3297
3281
 
3298
- elif isinstance(material, Part):
3299
- fadeOutPart(material, 0.0)
3282
+ elif isinstance(material, Part):
3283
+ for phrase in material.getPhraseList():
3284
+ Mod.fadeOut(phrase, fadeLength, _endTime)
3300
3285
 
3301
- elif isinstance(material, Phrase):
3302
- fadeOutPhrase(material, 0.0)
3286
+ elif isinstance(material, Phrase):
3287
+ durationCounter = material.getStartTime()
3288
+ for note in material.getNoteList():
3289
+ distFromEnd = _endTime - durationCounter
3290
+ if distFromEnd < fadeLength:
3291
+ fadeFactor = max(0, distFromEnd / fadeLength)
3292
+ note.setDynamic(int(max(1, note.getDynamic() * fadeFactor)))
3293
+ durationCounter += note.getDuration()
3303
3294
 
3304
- else: # error check
3305
- raise TypeError(f"Unrecognized material type {type(material)} - expected Phrase, Part, or Score.")
3295
+ else:
3296
+ raise TypeError(f"Unrecognized material type {type(material)} - expected Phrase, Part, or Score.")
3306
3297
 
3307
3298
  @staticmethod
3308
3299
  def fillRests(material):