CreativePython 0.1.6__py3-none-any.whl → 0.1.7__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.
@@ -15,11 +15,11 @@ from gui import *
15
15
  from music import *
16
16
  from random import *
17
17
  from math import *
18
- from sliderControl import *
19
-
18
+ from sliderControl import *
19
+
20
20
  class MusicalSphere:
21
21
  """Creates a revolving sphere that plays music."""
22
-
22
+
23
23
  def __init__(self, radius, density, velocity=0.01, frameRate=30):
24
24
  """
25
25
  Construct a revolving sphere with given 'radius', 'density'
@@ -28,151 +28,151 @@ class MusicalSphere:
28
28
  second. Each point plays a note when crossing the zero meridian (the
29
29
  sphere's meridian (vertical line) closest to the viewer).
30
30
  """
31
-
31
+
32
32
  # musical parameters
33
33
  self.instrument = PIANO
34
34
  self.scale = MAJOR_SCALE
35
35
  self.lowPitch = C1
36
36
  self.highPitch = C6
37
- self.noteDuration = 2000 # milliseconds (2 seconds)
38
-
37
+ self.noteDuration = 2000 # milliseconds (2 seconds)
38
+
39
39
  Play.setInstrument(self.instrument, 0) # set the instrument
40
-
40
+
41
41
  # visual parameters
42
42
  self.display = Display("3D Sphere", radius*3, radius*3) # display to draw sphere
43
43
  self.display.setColor( Color.BLACK ) # set background color to black
44
-
44
+
45
45
  self.radius = radius # how wide circle is
46
46
  self.numPoints = density # how many points to draw on sphere surface
47
47
  self.velocity = velocity # how far it rotates per animation frame
48
48
  self.frameRate = frameRate # how many animation frames to do per second
49
-
49
+
50
50
  self.xCenter = self.display.getWidth() / 2 # to place circle at display's center
51
51
  self.yCenter = self.display.getHeight() / 2
52
-
52
+
53
53
  # sphere data structure (parallel lists)
54
54
  self.points = [] # holds the points
55
55
  self.thetaValues = [] # holds the points' rotation (azimuthal angle)
56
56
  self.phiValues = [] # holds the points' latitude (polar angle)
57
-
57
+
58
58
  # timer to drive animation
59
59
  delay = 1000 / frameRate # convert from frame rate to timer delay (in milliseconds)
60
60
  self.timer = Timer(delay, self.movePoints) # timer to schedule movement
61
-
61
+
62
62
  # control surface for animation frame rate
63
63
  xPosition = self.display.getWidth() / 3 # set initial position of display
64
64
  yPosition = self.display.getHeight() + 45
65
65
  self.control = SliderControl(title="Frame Rate", updateFunction=self.setFrameRate,
66
66
  minValue=1, maxValue=120, startValue=self.frameRate,
67
67
  x=xPosition, y=yPosition)
68
-
68
+
69
69
  # orange color gradient (used to display depth, the further away, the darker)
70
70
  black = [0, 0, 0] # RGB values for black
71
71
  orange = [251, 147, 14] # RGB values for orange
72
72
  white = [255, 255, 255] # RGB values for white
73
-
73
+
74
74
  # create list of gradient colors from black to orange, and from orange to white
75
75
  # (a total of 25 colors)
76
76
  self.gradientColors = colorGradient(black, orange, 12) + colorGradient(orange, white, 12) + [white] # remember to include the final color
77
-
77
+
78
78
  self.initSphere() # create the circle
79
-
79
+
80
80
  self.start() # and start rotating!
81
-
81
+
82
82
  def start(self):
83
83
  """Starts sphere animation."""
84
84
  self.timer.start()
85
-
85
+
86
86
  def stop(self):
87
87
  """Stops sphere animation."""
88
88
  self.timer.stop()
89
-
89
+
90
90
  def setFrameRate(self, frameRate=30):
91
91
  """Controls speed of sphere animation (by setting how many times per second to move points)."""
92
-
92
+
93
93
  delay = 1000 / frameRate # convert from frame rate to delay between each update (in milliseconds)
94
94
  self.timer.setDelay(delay)
95
-
95
+
96
96
  def initSphere(self):
97
97
  """Generate a sphere of 'radius' out of points (placed on the surface of the sphere)."""
98
-
98
+
99
99
  for i in range(self.numPoints): # create all the points
100
-
100
+
101
101
  # get random spherical coordinates for this point
102
102
  r = self.radius # all points are placed *on* the surface
103
103
  theta = mapValue( random(), 0.0, 1.0, 0.0, 2*pi) # random rotation (azimuthal angle)
104
104
  phi = mapValue( random(), 0.0, 1.0, 0.0, pi) # random latitude (polar angle)
105
-
105
+
106
106
  # project from spherical to cartesian 3D coordinates (z is depth)
107
- x, y, z = self.sphericalToCartesian(r, phi, theta)
108
-
107
+ x, y, z = self.sphericalToCartesian(r, phi, theta)
108
+
109
109
  # convert depth (z) to color
110
- color = self.depthToColor(z, self.radius)
111
-
112
- # create a point (with thickness 1) at these x, y coordinates
113
- point = Point(x, y, color, 1)
114
-
110
+ color = self.depthToColor(z, self.radius)
111
+
112
+ # create a point at these x, y coordinates
113
+ point = Point(x, y, color)
114
+
115
115
  # remember this point and its spherical coordinates (r equals self.radius for all points)
116
116
  # (append data for this point to the three parallel lists)
117
117
  self.points.append( point )
118
118
  self.phiValues.append( phi )
119
119
  self.thetaValues.append( theta )
120
-
120
+
121
121
  # add this point to the display
122
122
  self.display.add( point )
123
-
123
+
124
124
  def sphericalToCartesian(self, r, phi, theta):
125
- """Convert spherical to cartesian coordinates."""
126
-
125
+ """Convert spherical to cartesian coordinates."""
126
+
127
127
  # adjust rotation so that theta is 0 at max z (i.e., closest to viewer)
128
128
  x = int( r * sin(phi) * cos(theta + pi/2) ) # horizontal axis (pixels are int)
129
129
  y = int( r * cos(phi) ) # vertical axis
130
- z = int( r * sin(phi) * sin(theta + pi/2) ) # depth axis
131
-
130
+ z = int( r * sin(phi) * sin(theta + pi/2) ) # depth axis
131
+
132
132
  # move sphere's center to display's center
133
133
  x = x + self.xCenter
134
134
  y = y + self.yCenter
135
-
135
+
136
136
  return x, y, z
137
-
137
+
138
138
  def depthToColor(self, depth, radius):
139
139
  """Map 'depth' to color using the 'gradientColors' RGB colors."""
140
-
140
+
141
141
  # create color based on position (points further away have less luminosity)
142
142
  colorIndex = mapValue(depth, -self.radius, self.radius, 0, len(self.gradientColors)) # map depth to color index
143
143
  colorRGB = self.gradientColors[colorIndex] # get corresponding RBG value
144
144
  color = Color(colorRGB[0], colorRGB[1], colorRGB[2]) # and create the color
145
-
145
+
146
146
  return color
147
-
147
+
148
148
  def movePoints(self):
149
149
  """Rotate points on y axis as specified by angular velocity."""
150
-
150
+
151
151
  for i in range(self.numPoints):
152
152
  point = self.points[i] # get this point
153
153
  theta = (self.thetaValues[i] + self.velocity) % (2*pi) # increment angle to simulate rotation
154
154
  phi = self.phiValues[i] # get latitude (altitude)
155
-
155
+
156
156
  # convert from spherical to cartesian 3D coordinates
157
157
  x, y, z = self.sphericalToCartesian(self.radius, phi, theta)
158
-
158
+
159
159
  if self.thetaValues[i] > theta: # did we just cross the primary meridian?
160
160
  color = Color.RED # yes, so sparkle for a split second
161
161
  pitch = mapScale(phi, 0, pi, self.lowPitch, self.highPitch, self.scale) # phi is latitude
162
162
  dynamic = randint(0, 127) # random dynamic
163
163
  Play.note(pitch, 0, self.noteDuration, dynamic) # and play a note (based on latitude)
164
-
164
+
165
165
  else: # we are not on the primary meridian, so
166
166
  # convert depth to color (points further away have less luminosity)
167
167
  color = self.depthToColor(z, self.radius)
168
-
168
+
169
169
  # adjust this point's position and color
170
170
  self.display.move(point, x, y)
171
171
  point.setColor(color)
172
-
172
+
173
173
  # now, remember this point's new theta coordinate
174
174
  self.thetaValues[i] = theta
175
-
175
+
176
176
  #################################################
177
177
  # create a sphere
178
178
  sphere = MusicalSphere(radius=200, density=200, velocity=0.01, frameRate=30)
_RealtimeAudioPlayer.py CHANGED
@@ -8,6 +8,7 @@
8
8
  #
9
9
  #######################################################################################
10
10
  # TODO:
11
+ # - rewrite to avoid early returns/breaks
11
12
  #
12
13
  #######################################################################################
13
14
 
@@ -18,7 +19,7 @@ import os # for file path operations
18
19
  import math # for logarithmic calculations in pitch/frequency conversions
19
20
 
20
21
 
21
- #### Helper Functions #################################################################
22
+ #### Helper Conversion Functions #################################################################
22
23
 
23
24
  def freqToNote(frequency):
24
25
  """Converts frequency to the closest MIDI note number with pitch bend value
@@ -48,7 +49,6 @@ def noteToFreq(pitch):
48
49
 
49
50
  return frequency
50
51
 
51
-
52
52
  ##### Real-Time Audio Player Class ########################################################
53
53
 
54
54
  class _RealtimeAudioPlayer:
@@ -173,7 +173,34 @@ class _RealtimeAudioPlayer:
173
173
  elif not self.looping: # not looping
174
174
  self.loopCountTarget = 0 # play once then stop
175
175
 
176
- # print(f"For '{os.path.basename(self.filepath)}', BasePitch={self.basePitch:.2f}, BaseFreq={self.baseFrequency:.2f}Hz.")
176
+ # IMPORTANT: Pre-allocate the audio stream for maximum efficiency.
177
+ # The stream is created but not started until play() is called.
178
+ # This eliminates the need for stream creation/deletion during each playback.
179
+ self._createStream()
180
+
181
+
182
+ def _createStream(self):
183
+ """
184
+ Creates and starts the audio stream. This method is called during initialization
185
+ to pre-allocate the stream for maximum efficiency.
186
+ """
187
+
188
+ try:
189
+ # create the sounddevice output stream for playback
190
+ self.sdStream = sd.OutputStream(
191
+ samplerate=self.sampleRate,
192
+ blocksize=self.chunkSize,
193
+ channels=self.numChannels,
194
+ callback=self.audioCallback
195
+ )
196
+
197
+ # Don't start the stream here - it will be started in play() method
198
+ # self.sdStream.start() # Stream will be started when needed
199
+
200
+ except Exception as e:
201
+ print(f"Error creating audio stream: {e}")
202
+ self.sdStream = None
203
+ raise
177
204
 
178
205
 
179
206
  def _findNextZeroCrossing(self, startFrameFloat, searchWindowFrames=256):
@@ -339,7 +366,7 @@ class _RealtimeAudioPlayer:
339
366
  originalTargetFrameFloat = timeSeconds * self.sampleRate
340
367
 
341
368
  # basic ZC adjustment for now, will be enhanced with fade-seek-fade
342
- actualTargetFrame = self.findNextZeroCrossing(originalTargetFrameFloat)
369
+ actualTargetFrame = self._findNextZeroCrossing(originalTargetFrameFloat)
343
370
 
344
371
  # if playing and conditions met for smooth seek
345
372
  if actualTargetFrame >= self.numFrames and not self.looping:
@@ -400,14 +427,12 @@ class _RealtimeAudioPlayer:
400
427
  self.isPlaying = False
401
428
  self.isApplyingFadeOut = False
402
429
  self.isFadingOutToStop = False
403
- # no need to raise CallbackStop here, as returning normally from a callback while
404
- # the stream is active and isPlaying is false will lead to it being stopped by play/stop logic.
405
430
 
406
- return # exit the callback early, providing silence.
431
+ return # exit the callback early, providing silence.
407
432
 
408
433
  numOutputChannels = outdata.shape[1] # get number of output channels (1=mono, 2=stereo)
409
434
 
410
- # initialize chunkBuffer matching the output stream's channel count and frame count for this callback.
435
+ # Initialize chunkBuffer matching the output stream's channel count and frame count for this callback.
411
436
  # This buffer will be filled with processed audio samples one by one before being copied to `outdata`.
412
437
  # Using an intermediate buffer like this is common for clarity and for complex processing steps.
413
438
  chunkBuffer = np.zeros((frames, numOutputChannels), dtype=np.float32)
@@ -419,9 +444,9 @@ class _RealtimeAudioPlayer:
419
444
  # If isPlaying became false (e.g., due to a fade-out completing and setting isPlaying to False,
420
445
  # or an external stop() call), we should fill the rest of this chunk with silence
421
446
  # and then break out of this sample-processing loop.
422
- chunkBuffer[i:] = 0.0 # fill remaining part of the buffer with silence
423
- break # exit per-sample loop
424
- # TODO: rewrite to avoid early break
447
+
448
+ chunkBuffer[i:] = 0.0 # fill remaining part of the buffer with silence
449
+ break # exit per-sample loop
425
450
 
426
451
  #### Determine current sample value with interpolation (and hard loop if enabled)
427
452
  # To play audio at different speeds (self.rateFactor != 1.0) or for smooth playback,
@@ -432,7 +457,7 @@ class _RealtimeAudioPlayer:
432
457
  readPosInt2 = readPosInt1 + 1 # next actual sample index
433
458
  fraction = readPosFloat - readPosInt1 # the fractional part, how far between readPosInt1 and readPosInt2 we are.
434
459
 
435
- # Clamp read positions to be safe for array access, *after* potential looping adjustment
460
+ # Clamp read positions to be safe for array access, *after* potential looping adjustment.
436
461
  # This ensures that we don't try to read outside the bounds of our audioData array.
437
462
  readPosInt1 = max(0, min(readPosInt1, self.numFrames - 1)) # ensure read position 1 is valid
438
463
  readPosInt2 = max(0, min(readPosInt2, self.numFrames - 1)) # ensure read position 2 is also valid
@@ -473,10 +498,10 @@ class _RealtimeAudioPlayer:
473
498
  # the overall volume of the sample is scaled by self.volumeFactor
474
499
  processedSample = currentSampleArray * self.volumeFactor
475
500
 
476
- #### Apply Master Fades (Fade-in and Fade-out) ---
501
+ #### Apply Master Fades (Fade-in and Fade-out)
477
502
  # Fades are applied by smoothly changing a gain envelope from 0 to 1 (fade-in)
478
503
  # or 1 to 0 (fade-out) over a specified number of frames.
479
- gainEnvelope = 1.0 # start with full gain, adjust if fading.
504
+ gainEnvelope = 1.0 # start with full gain, adjust if fading.
480
505
 
481
506
  if self.isApplyingFadeIn: # is fade-in currently being applied?
482
507
 
@@ -564,7 +589,7 @@ class _RealtimeAudioPlayer:
564
589
 
565
590
  # This is the point at which we should either loop or stop for the current play segment,
566
591
  # so determine the effective end frame for the current segment/loop
567
- effectiveSegmentEndFrame = self.numFrames -1 # default to end of file
592
+ effectiveSegmentEndFrame = self.numFrames -1 # default to end of file
568
593
 
569
594
  if self.looping and self.loopRegionEndFrame > 0:
570
595
  # if looping and a specific loop region end is defined, that's our segment end
@@ -614,7 +639,6 @@ class _RealtimeAudioPlayer:
614
639
  if not self.isPlaying:
615
640
  chunkBuffer[i:] = 0.0 # fill remaining part of this chunk with silence
616
641
  break # exit per-sample loop
617
- # TODO: rewrite to avoid break
618
642
 
619
643
  #### End of per-sample loop (for i in range(frames))
620
644
 
@@ -649,12 +673,9 @@ class _RealtimeAudioPlayer:
649
673
  outdata[:] = np.clip(chunkBuffer, -1.0, 1.0)
650
674
 
651
675
  #### Handle stream stopping conditions
652
- # NOTE: If isPlaying became False during this callback (e.g., by fade-out completion or natural end)
653
- # raise CallbackStop to tell PortAudio to stop invoking this callback.
654
- # This is the primary mechanism for cleanly stopping the audio stream from within the callback
655
- # when playback is logically complete or has been requested to stop.
656
- if not self.isPlaying:
657
- raise sd.CallbackStop
676
+ # NOTE: Streams are now properly started/stopped by play()/stop() methods
677
+ # This provides cleaner audio management and prevents interference between multiple voices
678
+ # The stream will be stopped when playback ends or stop() is called
658
679
 
659
680
  #### end of audioCallback
660
681
 
@@ -752,7 +773,7 @@ class _RealtimeAudioPlayer:
752
773
  if not self.isPlaying:
753
774
 
754
775
  if self.playbackPosition >= self.numFrames and not self.looping:
755
- self.playbackPosition = 0.0 # or self._findNextZeroCrossing(0.0)
776
+ self.playbackPosition = 0.0 # or self._findNextZeroCrossing(0.0)
756
777
 
757
778
  self.playbackEndedNaturally = False # playback did not reach the end
758
779
 
@@ -761,75 +782,31 @@ class _RealtimeAudioPlayer:
761
782
  self.isApplyingFadeIn = True
762
783
  self.fadeInFramesProcessed = 0
763
784
 
785
+ # Use the pre-allocated stream - start it if it's not already running
764
786
  if self.sdStream and not self.sdStream.closed:
765
- # NOTE: If stream exists but was stopped (e.g. by isPlaying = False in callback)
766
- # It needs to be closed and reopened, or sounddevice's start() might not work as expected
767
- # or might resume from an odd state. Safest is to ensure clean start.
768
- self.sdStream.close() # ensure previous instance is closed
769
-
770
- # create the sounddevice output stream for playback
771
- self.sdStream = sd.OutputStream(
772
- samplerate=self.sampleRate,
773
- blocksize=self.chunkSize,
774
- channels=self.numChannels,
775
- callback=self.audioCallback,
776
- finished_callback=self.onPlaybackFinished
777
- )
778
-
779
- # start the sounddevice output stream
780
- self.sdStream.start()
781
- self.isPlaying = True
782
- self.playbackEndedNaturally = False # reset this flag as we are starting
783
-
784
- except Exception as e: # sounddevice stream did not start
785
- print(f"Error starting sounddevice stream: {e}")
786
- self.isPlaying = False
787
+ if self.sdStream.stopped:
788
+ # Stream exists but is stopped, so start it
789
+ self.sdStream.start()
787
790
 
788
- if self.sdStream:
789
- self.sdStream.close() # ensure cleanup if start failed
790
- self.sdStream = None
791
-
792
- # do not re-raise here, allow AudioSample to handle or log
793
- return # exit since stream failed to start
794
- # TODO: rewrite to avoid early return
791
+ self.isPlaying = True
792
+ self.playbackEndedNaturally = False
793
+ else:
794
+ # Stream was somehow lost - recreate it
795
+ self._createStream()
796
+ self.sdStream.start() # Start the newly created stream
797
+ self.isPlaying = True
798
+ self.playbackEndedNaturally = False
799
+
800
+ except Exception as e: # stream recreation failed
801
+ print(f"Error with audio stream: {e}")
802
+ self.isPlaying = False
803
+ return # exit since stream failed
795
804
 
796
805
  if startAtBeginning and self.isPlaying: # check isPlaying again in case it was set by new stream
797
806
  self.isApplyingFadeIn = True
798
807
  self.fadeInFramesProcessed = 0
799
808
 
800
809
 
801
- def onPlaybackFinished(self):
802
- # NOTE: This callback is called when the stream is stopped or aborted.
803
- # self.isPlaying = False # Stream is already stopped, this reflects state
804
- # Don't reset looping here, it's a persistent setting.
805
- # Don't reset playbackPosition here, might be needed for resume or query.
806
-
807
- # close the sounddevice stream if it exists and is not already closed
808
- if self.sdStream and not self.sdStream.closed:
809
-
810
- try: # handle possible exceptions during stream closure (e.g., at interpreter exit)
811
- self.sdStream.close() # close the stream to release audio resources
812
-
813
- except sd.PortAudioError as pae:
814
-
815
- if pae.args[1] == sd.PaErrorCode.paNotInitialized:
816
- pass # portaudio already terminated, which is expected during atexit
817
-
818
- else:
819
- print(f"PortAudioError closing stream: {pae}")
820
-
821
- except Exception as e:
822
- print(f"Generic error closing PortAudio stream: {e}")
823
-
824
- self.sdStream = None # clear the stream reference after closing
825
-
826
- # Now, if it was a fade out to stop, the isPlaying should already be false.
827
- # If it stopped for other reasons (e.g. error, or CallbackStop raised not due to fade),
828
- # ensure isPlaying is False.
829
- # However, self.isPlaying is primarily controlled by play() and stop() and callback logic.
830
- # this finished_callback is more for resource cleanup.
831
-
832
-
833
810
  def getLoopsPerformed(self):
834
811
  return self.loopsPerformed # number of loops completed
835
812
 
@@ -846,25 +823,13 @@ class _RealtimeAudioPlayer:
846
823
  # handle case where already stopped (but may have pending fade-out)
847
824
  if not self.isPlaying and not self.isApplyingFadeOut:
848
825
 
849
- # ensure stream is actually closed if somehow isPlaying is False but stream exists
850
- if self.sdStream and not self.sdStream.closed:
851
-
826
+ # Ensure stream is stopped when not playing
827
+ if self.sdStream and not self.sdStream.stopped:
852
828
  try:
853
- self.sdStream.close() # close the audio stream
854
-
855
- except sd.PortAudioError as pae:
856
-
857
- if pae.args[1] == sd.PaErrorCode.paNotInitialized:
858
- pass # ignore if PortAudio already terminated
859
-
860
- else:
861
- print(f"Error closing PyAudio stream: {pae}")
862
-
829
+ self.sdStream.stop()
863
830
  except Exception as e:
864
- print(f"Generic error closing PyAudio stream: {e}")
865
-
866
- finally:
867
- self.sdStream = None # clear stream reference
831
+ # Ignore errors during cleanup
832
+ pass
868
833
 
869
834
  # reset all playback state variables
870
835
  self.isPlaying = False # confirm stopped state
@@ -877,7 +842,6 @@ class _RealtimeAudioPlayer:
877
842
  self.loopCountTarget = -1 if self.looping else 0 # reset to constructor state
878
843
  self.loopsPerformed = 0 # reset loop counter
879
844
  return # done, since already stopped
880
- # TODO: rewrite without early return
881
845
 
882
846
  # handle immediate stop (skip fade-out for instant termination)
883
847
  if immediate:
@@ -887,34 +851,13 @@ class _RealtimeAudioPlayer:
887
851
  self.isApplyingFadeOut = False # cancel any ongoing fade-out (e.g. from pause)
888
852
  self.isFadingOutToStop = False # ensure fade-out-to-stop is reset
889
853
 
890
- # handle immediate stream termination
891
- if self.sdStream:
892
-
854
+ # Stop the stream when stopping immediately
855
+ if self.sdStream and not self.sdStream.stopped:
893
856
  try:
894
- # check if stream is active before trying to stop
895
-
896
- if not self.sdStream.stopped:
897
- self.sdStream.stop() # stop the PortAudio stream immediately
898
-
899
- # re-check self.sdStream because .stop() could have called _onPlaybackFinished
900
- # which sets self.sdStream to None
901
-
902
- if self.sdStream and not self.sdStream.closed:
903
- self.sdStream.close() # close and release resources
904
-
905
- except sd.PortAudioError as pae:
906
-
907
- if pae.args[1] == sd.PaErrorCode.paNotInitialized:
908
- pass # PortAudio already terminated, expected during atexit or rapid stop/close
909
-
910
- else:
911
- print(f"PortAudioError during immediate stream stop/close: {pae}")
912
-
857
+ self.sdStream.stop()
913
858
  except Exception as e:
914
- print(f"Error during immediate stream stop/close: {e}")
915
-
916
- finally:
917
- self.sdStream = None # discard stream instance
859
+ # Ignore errors during cleanup
860
+ pass
918
861
 
919
862
  # reset all playback state variables for immediate stop
920
863
  self.targetEndSourceFrame = -1.0 # reset end frame target
@@ -946,18 +889,16 @@ class _RealtimeAudioPlayer:
946
889
  self.isFadingOutToStop = False
947
890
 
948
891
  if self.sdStream:
949
-
950
892
  try:
951
893
  # check if stream is active before trying to stop
952
894
  if not self.sdStream.stopped:
953
895
  self.sdStream.stop() # stop stream activity
954
896
 
955
- # re-check because .stop() could have called onPlaybackFinished
897
+ # re-check because .stop() could have been called
956
898
  if not self.sdStream.closed:
957
899
  self.sdStream.close() # release resources
958
900
 
959
901
  except sd.PortAudioError as pae:
960
-
961
902
  # if PortAudio is already uninitialized (e.g. during atexit), these calls can fail.
962
903
  if pae.args[1] == sd.PaErrorCode.paNotInitialized: # paNotInitialized = -10000
963
904
  pass # suppress error if PortAudio is already down
@@ -969,6 +910,27 @@ class _RealtimeAudioPlayer:
969
910
  self.sdStream = None
970
911
 
971
912
 
913
+ def forceCloseStream(self):
914
+ """
915
+ Explicitly closes the audio stream. This is useful when you want to ensure
916
+ a fresh stream is created on the next play() call, or when cleaning up resources.
917
+ """
918
+ if self.sdStream:
919
+ try:
920
+ if not self.sdStream.stopped:
921
+ self.sdStream.stop()
922
+ if not self.sdStream.closed:
923
+ self.sdStream.close()
924
+ except Exception as e:
925
+ # ignore errors during cleanup
926
+ pass
927
+ finally:
928
+ self.sdStream = None
929
+
930
+ # Recreate the stream for future use
931
+ self._createStream()
932
+
933
+
972
934
  def __del__(self):
973
935
  # call close() to ensure resources are released
974
936
  self.close()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: CreativePython
3
- Version: 0.1.6
3
+ Version: 0.1.7
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
@@ -33,6 +33,7 @@ Classifier: License :: OSI Approved :: MIT License
33
33
  Requires-Python: >=3.9
34
34
  Description-Content-Type: text/markdown
35
35
  License-File: LICENSE
36
+ Requires-Dist: setuptools>=69.0
36
37
  Requires-Dist: tinysoundfont>=0.3.6
37
38
  Requires-Dist: osc4py3>=1.0.8
38
39
  Requires-Dist: mido>=1.3.3
@@ -164,7 +165,7 @@ After you do, you should hear a single C4 half-note.
164
165
 
165
166
  Some of CreativePython's libraries need to compile C++ code during installation.
166
167
 
167
- - On Windows, download and install [Visual Studio Build Tools 2022](https://visualstudio.microsoft.com/downloads/). In the Visual Studio installer, make sure "C++ Build Tools" is checked.
168
+ - On Windows, download and install [Visual Studio Build Tools 2022](https://visualstudio.microsoft.com/downloads/). In the Visual Studio installer, make sure "Desktop Development with C++" is checked.
168
169
 
169
170
  - On MacOS, you can download and install [XCode from the App Store](https://apps.apple.com/us/app/xcode/id497799835?mt=12).
170
171
 
@@ -1,11 +1,11 @@
1
- _RealtimeAudioPlayer.py,sha256=KJjolBwX8GScKGnWE8Qpydb0bsWvu8Z6uQF78BnApUg,49068
2
- gui.py,sha256=jnh2oNv7eplZQe8HhMd5IIrl2vKSE0R9StI8-i5e5jI,136204
1
+ _RealtimeAudioPlayer.py,sha256=AHKDE4etmfRgwyUG3SIoEblXoPaFUk9oxQLy1BgDFP8,46808
2
+ gui.py,sha256=eKiJgMgSdHBoR6m5k0vIoVSoh2hmDNvJEgcWj1ima7w,140074
3
3
  iannix.py,sha256=Ixl_wiFqIMyAWO4eAcIJvyNAuJ8alTrkSGXu8ruhVug,17162
4
4
  image.py,sha256=8TEKOREWnIFpOKZBqrdTGeiQV2vN09texrb_nAzgorw,2739
5
5
  markov.py,sha256=UXXHDJ6Y1BItACpa9vSRjSb1wfIedv7T1GuHqpnvzWE,24407
6
- midi.py,sha256=phcaTdSfx9Q5LwojPD3nZp4PXHcnWsSREbZNDojYczs,40056
7
- music.py,sha256=SJ7cBQ5PXcFvJFzlF4FzYJlmxL4MSuu7iIC4G3WYyM0,223160
8
- osc.py,sha256=14zGel-KPiU226i1SWUGXYWEPG6v_AntGRw0KiPQu0Y,16244
6
+ midi.py,sha256=gpvrSBESD6z8XprDQD1xANQf-p1Id4X9arHcSw9W46o,42216
7
+ music.py,sha256=R26IEDcYvJ9LsO7n6zKNnrLSujHfSeTj2TfuBClpaGs,224290
8
+ osc.py,sha256=38I_wd2Nlxu3FgNnowidbDeGH5jSxJozL5zNVPadLq8,18638
9
9
  timer.py,sha256=jCOqnIc7oXbCTPYahvupO3wOo1AJmyPGA8FHVv5xaP0,22477
10
10
  zipf.py,sha256=u1xZxfiCEqsNuX9mMOHVunikWPjCGK-6kzl99ZfimH8,30119
11
11
  CreativePython/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -59,7 +59,7 @@ CreativePython/examples/midiOut.b.py,sha256=N1TazkSMXI5D3BODrf4faqtzu2ZaE2AdTh8f
59
59
  CreativePython/examples/midiSynthesizer.py,sha256=Bz7TbFsw2DKNaskxKsYiPq-7fJ9TNPPAUUjjiNgkW0g,808
60
60
  CreativePython/examples/midiSynthesizer2.py,sha256=a9KlKBOeqvJuIpbrB3Ybbr3mqh_PGI4IPbqByKoGH60,1552
61
61
  CreativePython/examples/moondog-bird_slament.wav,sha256=fcux66G_0DJW7tJAYgI328CvBW81NRy0vettW1w6Yos,727084
62
- CreativePython/examples/musicalSphere.py,sha256=bcdnwvnZR5s4UJ2xlm8qgByKqiFdM0mEKsR0NMUYsPc,8048
62
+ CreativePython/examples/musicalSphere.py,sha256=lg0Q8fbvxhEEL14IHh-Lv22HpV3Vyqo40uamzJDZiPc,7960
63
63
  CreativePython/examples/note.py,sha256=N51kqwd4B2a-ZR6GVH_mVS7wDYjpgyuskhQRAkii23A,1913
64
64
  CreativePython/examples/octoplus.py,sha256=XDTVjMKuRKbaCJKriN2CzBj2iIYZBTDr81TMJm_9JTc,1817
65
65
  CreativePython/examples/oscIn1.py,sha256=Er2o9wlsmiqxmJKg-NlMw6fGDEGISm8QCLe5Niko5IM,219
@@ -97,8 +97,8 @@ CreativePython/resources/.DS_Store,sha256=1lFlJ5EFymdzGAUAaI30vcaaLHt3F1LwpG7xIL
97
97
  CreativePython/resources/550973__luizguilherme_a__clean-guitarr-riff.mp3,sha256=hUO6BU7VbaRv6QpjjPvi5cVnrGuR8AXTVOcw-BaDd0k,147856
98
98
  CreativePython/resources/chopper.jpg,sha256=AykvUOwRSDi4g6OpfcNQ-LgwPE05_eVkCv5E9lnhUbk,135266
99
99
  CreativePython/resources/de-brazzas-monkey.jpg,sha256=AFCxemqkDYv_TPpcLwrHnTtoxGduvZD7O2S0xMqQ4C0,119054
100
- creativepython-0.1.6.dist-info/licenses/LICENSE,sha256=1hbJHc1vxY4vAasDE4CMC4wDH91CBz5B-rgfujDntTw,1073
101
- creativepython-0.1.6.dist-info/METADATA,sha256=UTJpq7BCP712qm4t2hD3CVBm95mr0zE6UKG-QyC_mmc,6380
102
- creativepython-0.1.6.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
103
- creativepython-0.1.6.dist-info/top_level.txt,sha256=hr9Bb2GEFRCKQRsdmtS62vxm9eK3knxLkZjePwcS72M,86
104
- creativepython-0.1.6.dist-info/RECORD,,
100
+ creativepython-0.1.7.dist-info/licenses/LICENSE,sha256=1hbJHc1vxY4vAasDE4CMC4wDH91CBz5B-rgfujDntTw,1073
101
+ creativepython-0.1.7.dist-info/METADATA,sha256=0hr44zeN928R6AvKgw0oU8Po0g_2ZYbnlq9aQJW55G0,6425
102
+ creativepython-0.1.7.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
103
+ creativepython-0.1.7.dist-info/top_level.txt,sha256=hr9Bb2GEFRCKQRsdmtS62vxm9eK3knxLkZjePwcS72M,86
104
+ creativepython-0.1.7.dist-info/RECORD,,