CreativePython 1.1.6__tar.gz → 1.2__tar.gz

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.
Files changed (103) hide show
  1. {creativepython-1.1.6/src/CreativePython.egg-info → creativepython-1.2}/PKG-INFO +1 -1
  2. {creativepython-1.1.6 → creativepython-1.2}/pyproject.toml +2 -2
  3. {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/GuiRenderer.py +0 -5
  4. {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/RealtimeAudioPlayer.py +9 -8
  5. {creativepython-1.1.6 → creativepython-1.2/src/CreativePython.egg-info}/PKG-INFO +1 -1
  6. {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython.egg-info/SOURCES.txt +3 -0
  7. {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython.egg-info/top_level.txt +1 -0
  8. creativepython-1.2/src/animation.py +554 -0
  9. {creativepython-1.1.6 → creativepython-1.2}/src/gui.py +9 -124
  10. {creativepython-1.1.6 → creativepython-1.2}/src/music.py +72 -81
  11. creativepython-1.2/tests/test_crescendo.py +82 -0
  12. creativepython-1.2/tests/test_crescendo_import.py +114 -0
  13. {creativepython-1.1.6 → creativepython-1.2}/LICENSE +0 -0
  14. {creativepython-1.1.6 → creativepython-1.2}/LICENSE-PSF +0 -0
  15. {creativepython-1.1.6 → creativepython-1.2}/MANIFEST.in +0 -0
  16. {creativepython-1.1.6 → creativepython-1.2}/README.md +0 -0
  17. {creativepython-1.1.6 → creativepython-1.2}/setup.cfg +0 -0
  18. {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/GuiHandler.py +0 -0
  19. {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/__init__.py +0 -0
  20. {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/nevmuse/Java-Comparison-Tests/advMetricRunner.pythonSurvey.py +0 -0
  21. {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/nevmuse/Java-Comparison-Tests/compareMetrics_Java-Vs-Python.py +0 -0
  22. {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/nevmuse/RunMetrics.py +0 -0
  23. {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/nevmuse/Surveyor.py +0 -0
  24. {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/nevmuse/__init__.py +0 -0
  25. {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/nevmuse/data/Confidence.py +0 -0
  26. {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/nevmuse/data/Contig.py +0 -0
  27. {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/nevmuse/data/ExtendedNote.py +0 -0
  28. {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/nevmuse/data/Histogram.py +0 -0
  29. {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/nevmuse/data/Judgement.py +0 -0
  30. {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/nevmuse/data/Measurement.py +0 -0
  31. {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/nevmuse/data/PianoRoll.py +0 -0
  32. {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/nevmuse/data/PianoRollOld.py +0 -0
  33. {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/nevmuse/data/__init__.py +0 -0
  34. {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/nevmuse/data/test_ExtendedNote.py +0 -0
  35. {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/nevmuse/data/test_Histogram.py +0 -0
  36. {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/nevmuse/data/test_Measurement.py +0 -0
  37. {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/nevmuse/data/test_PianoRoll_assertions.py +0 -0
  38. {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/nevmuse/data/test_PianoRoll_integration.py +0 -0
  39. {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/nevmuse/data/test_PianoRoll_quantization.py +0 -0
  40. {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/nevmuse/data/test_PianoRoll_unit.py +0 -0
  41. {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/nevmuse/metrics/Metric.py +0 -0
  42. {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/nevmuse/metrics/ZipfMetrics.py +0 -0
  43. {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/nevmuse/metrics/__init__.py +0 -0
  44. {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/nevmuse/metrics/simple/ChordDensityMetric.py +0 -0
  45. {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/nevmuse/metrics/simple/ChordDistanceMetric.py +0 -0
  46. {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/nevmuse/metrics/simple/ChordMetric.py +0 -0
  47. {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/nevmuse/metrics/simple/ChordNormalizedMetric.py +0 -0
  48. {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/nevmuse/metrics/simple/ChromaticToneMetric.py +0 -0
  49. {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/nevmuse/metrics/simple/ContourBasslineDurationMetric.py +0 -0
  50. {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/nevmuse/metrics/simple/ContourBasslineDurationQuantizedMetric.py +0 -0
  51. {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/nevmuse/metrics/simple/ContourBasslinePitchMetric.py +0 -0
  52. {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/nevmuse/metrics/simple/ContourMelodyDurationMetric.py +0 -0
  53. {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/nevmuse/metrics/simple/ContourMelodyDurationQuantizedMetric.py +0 -0
  54. {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/nevmuse/metrics/simple/ContourMelodyPitchMetric.py +0 -0
  55. {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/nevmuse/metrics/simple/DurationBigramMetric.py +0 -0
  56. {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/nevmuse/metrics/simple/DurationDistanceMetric.py +0 -0
  57. {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/nevmuse/metrics/simple/DurationMetric.py +0 -0
  58. {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/nevmuse/metrics/simple/DurationQuantizedBigramMetric.py +0 -0
  59. {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/nevmuse/metrics/simple/DurationQuantizedDistanceMetric.py +0 -0
  60. {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/nevmuse/metrics/simple/DurationQuantizedMetric.py +0 -0
  61. {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/nevmuse/metrics/simple/HarmonicBigramMetric.py +0 -0
  62. {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/nevmuse/metrics/simple/HarmonicConsonanceMetric.py +0 -0
  63. {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/nevmuse/metrics/simple/HarmonicIntervalMetric.py +0 -0
  64. {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/nevmuse/metrics/simple/MelodicBigramMetric.py +0 -0
  65. {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/nevmuse/metrics/simple/MelodicConsonanceMetric.py +0 -0
  66. {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/nevmuse/metrics/simple/MelodicIntervalMetric.py +0 -0
  67. {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/nevmuse/metrics/simple/PitchDistanceMetric.py +0 -0
  68. {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/nevmuse/metrics/simple/PitchDurationMetric.py +0 -0
  69. {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/nevmuse/metrics/simple/PitchDurationQuantizedMetric.py +0 -0
  70. {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/nevmuse/metrics/simple/PitchMetric.py +0 -0
  71. {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/nevmuse/metrics/simple/RestMetric.py +0 -0
  72. {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/nevmuse/metrics/simple/__init__.py +0 -0
  73. {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/nevmuse/metrics/simple/test_DurationMetric.py +0 -0
  74. {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/nevmuse/metrics/simple/test_Metrics_BasicIntervalsAndBigrams.py +0 -0
  75. {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/nevmuse/metrics/simple/test_Metrics_ChordsAndConsonance.py +0 -0
  76. {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/nevmuse/metrics/simple/test_Metrics_ContoursAndChromatic.py +0 -0
  77. {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/nevmuse/metrics/simple/test_Metrics_QuantizedDurationsAndDistances.py +0 -0
  78. {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/nevmuse/metrics/simple/test_PitchMetric.py +0 -0
  79. {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/nevmuse/metrics/simple/test_RestMetric.py +0 -0
  80. {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/nevmuse/metrics/test_Metric.py +0 -0
  81. {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/nevmuse/utilities/CSVWriter.py +0 -0
  82. {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/nevmuse/utilities/PowerLawRandom.py +0 -0
  83. {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/nevmuse/utilities/__init__.py +0 -0
  84. {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/notationRenderer.py +0 -0
  85. {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython.egg-info/dependency_links.txt +0 -0
  86. {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython.egg-info/requires.txt +0 -0
  87. {creativepython-1.1.6 → creativepython-1.2}/src/bin/libportaudio.2.dylib +0 -0
  88. {creativepython-1.1.6 → creativepython-1.2}/src/iannix.py +0 -0
  89. {creativepython-1.1.6 → creativepython-1.2}/src/image.py +0 -0
  90. {creativepython-1.1.6 → creativepython-1.2}/src/markov.py +0 -0
  91. {creativepython-1.1.6 → creativepython-1.2}/src/midi.py +0 -0
  92. {creativepython-1.1.6 → creativepython-1.2}/src/osc.py +0 -0
  93. {creativepython-1.1.6 → creativepython-1.2}/src/timer.py +0 -0
  94. {creativepython-1.1.6 → creativepython-1.2}/src/zipf.py +0 -0
  95. {creativepython-1.1.6 → creativepython-1.2}/tests/testAnimate.py +0 -0
  96. {creativepython-1.1.6 → creativepython-1.2}/tests/testCompress.py +0 -0
  97. {creativepython-1.1.6 → creativepython-1.2}/tests/testGameboard.py +0 -0
  98. {creativepython-1.1.6 → creativepython-1.2}/tests/testHitTesting.py +0 -0
  99. {creativepython-1.1.6 → creativepython-1.2}/tests/testPeer.py +0 -0
  100. {creativepython-1.1.6 → creativepython-1.2}/tests/testToolTips.py +0 -0
  101. {creativepython-1.1.6 → creativepython-1.2}/tests/test_keyEvent.py +0 -0
  102. {creativepython-1.1.6 → creativepython-1.2}/tests/test_midi.py +0 -0
  103. {creativepython-1.1.6 → creativepython-1.2}/tests/test_osc.py +0 -0
@@ -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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "CreativePython"
7
- version = "1.1.6"
7
+ version = "1.2"
8
8
  description = "A Python-based software environment for developing algorithmic art projects."
9
9
  readme = "README.md"
10
10
  license = { file = "LICENSE" }
@@ -40,7 +40,7 @@ dependencies = [
40
40
 
41
41
  [tool.setuptools]
42
42
  package-dir = {"" = "src"}
43
- py-modules = ["gui", "image", "midi", "music", "osc", "timer", "iannix", "markov", "zipf"]
43
+ py-modules = ["gui", "image", "midi", "music", "osc", "timer", "animation", "iannix", "markov", "zipf"]
44
44
 
45
45
  [tool.setuptools.packages.find]
46
46
  where = ["src"]
@@ -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)
@@ -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
@@ -3,6 +3,7 @@ LICENSE-PSF
3
3
  MANIFEST.in
4
4
  README.md
5
5
  pyproject.toml
6
+ src/animation.py
6
7
  src/gui.py
7
8
  src/iannix.py
8
9
  src/image.py
@@ -93,6 +94,8 @@ tests/testGameboard.py
93
94
  tests/testHitTesting.py
94
95
  tests/testPeer.py
95
96
  tests/testToolTips.py
97
+ tests/test_crescendo.py
98
+ tests/test_crescendo_import.py
96
99
  tests/test_keyEvent.py
97
100
  tests/test_midi.py
98
101
  tests/test_osc.py
@@ -1,4 +1,5 @@
1
1
  CreativePython
2
+ animation
2
3
  bin
3
4
  gui
4
5
  iannix
@@ -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