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.
- CreativePython/examples/musicalSphere.py +49 -49
- _RealtimeAudioPlayer.py +93 -131
- {creativepython-0.1.6.dist-info → creativepython-0.1.7.dist-info}/METADATA +3 -2
- {creativepython-0.1.6.dist-info → creativepython-0.1.7.dist-info}/RECORD +11 -11
- gui.py +402 -246
- midi.py +110 -105
- music.py +30 -3
- osc.py +85 -32
- {creativepython-0.1.6.dist-info → creativepython-0.1.7.dist-info}/WHEEL +0 -0
- {creativepython-0.1.6.dist-info → creativepython-0.1.7.dist-info}/licenses/LICENSE +0 -0
- {creativepython-0.1.6.dist-info → creativepython-0.1.7.dist-info}/top_level.txt +0 -0
|
@@ -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
|
|
113
|
-
point = Point(x, y, color
|
|
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
|
-
#
|
|
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.
|
|
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
|
|
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
|
-
#
|
|
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
|
-
|
|
423
|
-
|
|
424
|
-
#
|
|
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
|
|
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
|
|
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:
|
|
653
|
-
#
|
|
654
|
-
#
|
|
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
|
|
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
|
-
|
|
766
|
-
|
|
767
|
-
|
|
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
|
-
|
|
789
|
-
self.
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
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
|
-
#
|
|
850
|
-
if self.sdStream and not self.sdStream.
|
|
851
|
-
|
|
826
|
+
# Ensure stream is stopped when not playing
|
|
827
|
+
if self.sdStream and not self.sdStream.stopped:
|
|
852
828
|
try:
|
|
853
|
-
self.sdStream.
|
|
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
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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++
|
|
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=
|
|
2
|
-
gui.py,sha256=
|
|
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=
|
|
7
|
-
music.py,sha256=
|
|
8
|
-
osc.py,sha256=
|
|
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=
|
|
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.
|
|
101
|
-
creativepython-0.1.
|
|
102
|
-
creativepython-0.1.
|
|
103
|
-
creativepython-0.1.
|
|
104
|
-
creativepython-0.1.
|
|
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,,
|