CreativePython 0.0.1__tar.gz → 0.0.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.
- creativepython-0.0.2/MANIFEST.in +1 -0
- creativepython-0.0.2/PKG-INFO +65 -0
- creativepython-0.0.2/README.md +25 -0
- creativepython-0.0.2/pyproject.toml +45 -0
- {creativepython-0.0.1/src/CreativePython → creativepython-0.0.2/src}/AudioSample.py +257 -31
- creativepython-0.0.2/src/CreativePython.egg-info/PKG-INFO +65 -0
- creativepython-0.0.2/src/CreativePython.egg-info/SOURCES.txt +20 -0
- creativepython-0.0.2/src/CreativePython.egg-info/entry_points.txt +3 -0
- creativepython-0.0.2/src/CreativePython.egg-info/requires.txt +4 -0
- creativepython-0.0.2/src/CreativePython.egg-info/top_level.txt +11 -0
- {creativepython-0.0.1/src/CreativePython → creativepython-0.0.2/src}/_RealtimeAudioPlayer.py +15 -10
- creativepython-0.0.2/src/creativepython_setup.py +101 -0
- {creativepython-0.0.1/src/CreativePython → creativepython-0.0.2/src}/gui.py +52 -52
- {creativepython-0.0.1/src/CreativePython → creativepython-0.0.2/src}/midi.py +5 -5
- {creativepython-0.0.1/src/CreativePython → creativepython-0.0.2/src}/music.py +549 -203
- {creativepython-0.0.1/src/CreativePython → creativepython-0.0.2/src}/timer.py +323 -106
- creativepython-0.0.1/MANIFEST.in +0 -2
- creativepython-0.0.1/PKG-INFO +0 -19
- creativepython-0.0.1/README.md +0 -5
- creativepython-0.0.1/pyproject.toml +0 -25
- creativepython-0.0.1/src/CreativePython/images/de-brazzas-monkey.jpg +0 -0
- creativepython-0.0.1/src/CreativePython.egg-info/PKG-INFO +0 -19
- creativepython-0.0.1/src/CreativePython.egg-info/SOURCES.txt +0 -18
- creativepython-0.0.1/src/CreativePython.egg-info/top_level.txt +0 -1
- {creativepython-0.0.1 → creativepython-0.0.2}/LICENSE +0 -0
- {creativepython-0.0.1 → creativepython-0.0.2}/setup.cfg +0 -0
- {creativepython-0.0.1 → creativepython-0.0.2}/src/CreativePython.egg-info/dependency_links.txt +0 -0
- {creativepython-0.0.1/src/CreativePython → creativepython-0.0.2/src}/__init__.py +0 -0
- {creativepython-0.0.1/src/CreativePython → creativepython-0.0.2/src}/image.py +0 -0
- {creativepython-0.0.1/src/CreativePython → creativepython-0.0.2/src}/osc.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
recursive-exclude */.DS_Store
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: CreativePython
|
|
3
|
+
Version: 0.0.2
|
|
4
|
+
Summary: A Python-based software environment for developing algorithmic art projects.
|
|
5
|
+
Author-email: "Dr. Bill Manaris" <manaris@cofc.edu>, Taj Ballinger <ballingertj@g.cofc.edu>, Trevor Ritchie <ritchiets@g.cofc.edu>
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2025 Dr. Bill Manaris
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
|
|
28
|
+
Project-URL: Homepage, https://jythonmusic.me
|
|
29
|
+
Keywords: audio,midi,learning,algorithmic art,algoart
|
|
30
|
+
Classifier: Programming Language :: Python :: 3
|
|
31
|
+
Classifier: Operating System :: OS Independent
|
|
32
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
33
|
+
Requires-Python: >=3.9
|
|
34
|
+
Description-Content-Type: text/markdown
|
|
35
|
+
License-File: LICENSE
|
|
36
|
+
Provides-Extra: dev
|
|
37
|
+
Requires-Dist: build; extra == "dev"
|
|
38
|
+
Requires-Dist: twine; extra == "dev"
|
|
39
|
+
Dynamic: license-file
|
|
40
|
+
|
|
41
|
+
# CreativePython
|
|
42
|
+
|
|
43
|
+
CreativePython is a Python-based software environment for developing algorithmic art projects. It mirrors the [JythonMusic API](https://jythonmusic.me/api-reference/).
|
|
44
|
+
|
|
45
|
+
This package is still under development.
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## Quick Install
|
|
50
|
+
|
|
51
|
+
Download the [CreativePython Install Scripts](https://www.dropbox.com/scl/fo/if4jt56r9noqgj47rk2cq/AFRANez_l8BmKUU19-GAbh4?rlkey=uws2jof8hq2zpj0oke0rt7886&dl=1)
|
|
52
|
+
|
|
53
|
+
### Windows
|
|
54
|
+
1. Right-click `windows_setup.bat` → **Run as Administrator** (it will open PowerShell and run).
|
|
55
|
+
2. Follow any prompts and let it finish.
|
|
56
|
+
|
|
57
|
+
### macOS
|
|
58
|
+
1. Control-click `mac_setup.command` → **Open** (it will open Terminal and run).
|
|
59
|
+
2. Follow any prompts and let it finish.
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## Custom Installation
|
|
64
|
+
|
|
65
|
+
See "Advanced Installation" in [INSTALL.md](https://www.dropbox.com/scl/fi/us1j6im3ef67lzyfvq6ub/INSTALL.md?rlkey=fu9yjzj1hk11p6wgmhsc27s8y&dl=0).
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# CreativePython
|
|
2
|
+
|
|
3
|
+
CreativePython is a Python-based software environment for developing algorithmic art projects. It mirrors the [JythonMusic API](https://jythonmusic.me/api-reference/).
|
|
4
|
+
|
|
5
|
+
This package is still under development.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Quick Install
|
|
10
|
+
|
|
11
|
+
Download the [CreativePython Install Scripts](https://www.dropbox.com/scl/fo/if4jt56r9noqgj47rk2cq/AFRANez_l8BmKUU19-GAbh4?rlkey=uws2jof8hq2zpj0oke0rt7886&dl=1)
|
|
12
|
+
|
|
13
|
+
### Windows
|
|
14
|
+
1. Right-click `windows_setup.bat` → **Run as Administrator** (it will open PowerShell and run).
|
|
15
|
+
2. Follow any prompts and let it finish.
|
|
16
|
+
|
|
17
|
+
### macOS
|
|
18
|
+
1. Control-click `mac_setup.command` → **Open** (it will open Terminal and run).
|
|
19
|
+
2. Follow any prompts and let it finish.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Custom Installation
|
|
24
|
+
|
|
25
|
+
See "Advanced Installation" in [INSTALL.md](https://www.dropbox.com/scl/fi/us1j6im3ef67lzyfvq6ub/INSTALL.md?rlkey=fu9yjzj1hk11p6wgmhsc27s8y&dl=0).
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = [
|
|
3
|
+
"setuptools>=69.0",
|
|
4
|
+
"tinysoundfont>=0.3.6",
|
|
5
|
+
"osc4py3>=1.0.8",
|
|
6
|
+
"mido>=1.3.3",
|
|
7
|
+
"numpy>=2.3.2",
|
|
8
|
+
"pyaudio>=0.2.14",
|
|
9
|
+
"PySide6>=6.9.1",
|
|
10
|
+
"sounddevice>=0.5.2",
|
|
11
|
+
"soundfile>=0.13.1",
|
|
12
|
+
"pooch>=1.8",
|
|
13
|
+
"platformdirs>=4.2",
|
|
14
|
+
"pypianoroll>=1.0"
|
|
15
|
+
]
|
|
16
|
+
build-backend = "setuptools.build_meta"
|
|
17
|
+
|
|
18
|
+
[project]
|
|
19
|
+
name = "CreativePython"
|
|
20
|
+
version = "0.0.2"
|
|
21
|
+
description = "A Python-based software environment for developing algorithmic art projects."
|
|
22
|
+
readme = "README.md"
|
|
23
|
+
license = { file = "LICENSE" }
|
|
24
|
+
authors = [
|
|
25
|
+
{ name = "Dr. Bill Manaris", email = "manaris@cofc.edu" },
|
|
26
|
+
{ name = "Taj Ballinger", email = "ballingertj@g.cofc.edu" },
|
|
27
|
+
{ name = "Trevor Ritchie", email = "ritchiets@g.cofc.edu" },
|
|
28
|
+
]
|
|
29
|
+
keywords = ["audio", "midi", "learning", "algorithmic art", "algoart"]
|
|
30
|
+
requires-python = ">=3.9"
|
|
31
|
+
classifiers = [
|
|
32
|
+
"Programming Language :: Python :: 3",
|
|
33
|
+
"Operating System :: OS Independent",
|
|
34
|
+
"License :: OSI Approved :: MIT License",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
[project.urls]
|
|
38
|
+
Homepage = "https://jythonmusic.me"
|
|
39
|
+
|
|
40
|
+
[project.scripts]
|
|
41
|
+
cp-setup = "creativepython_setup:run"
|
|
42
|
+
cp-test = "creativepython_setup:playNote"
|
|
43
|
+
|
|
44
|
+
[project.optional-dependencies]
|
|
45
|
+
dev = ["build", "twine"]
|
|
@@ -137,6 +137,10 @@ class AudioSample:
|
|
|
137
137
|
# and corresponds to basePitch/baseFrequency. No need to call setPitch/setFrequency here
|
|
138
138
|
# unless we wanted it to start differently from its base.
|
|
139
139
|
|
|
140
|
+
# Initialize voice management attributes
|
|
141
|
+
self.freeVoices = list(range(self.maxVoices)) # holds list of free voices (numbered 0 to maxVoices-1)
|
|
142
|
+
self.voicesAllocatedToPitch = {} # a dictionary of voice lists - indexed by pitch (several voices per pitch is possible)
|
|
143
|
+
|
|
140
144
|
_activeAudioSamples.append(self)
|
|
141
145
|
|
|
142
146
|
def play(self, start=0, size=-1, voice=0):
|
|
@@ -178,12 +182,12 @@ class AudioSample:
|
|
|
178
182
|
|
|
179
183
|
# Store non-looping settings (or settings for a single iteration if size is given)
|
|
180
184
|
self._currentLoopSettings[voice] = {
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
185
|
+
'active': False, # signifies it's a play-once (or play-duration)
|
|
186
|
+
'loopCountTarget': 0, # ensures it's treated as non-looping by RTA if loop=False
|
|
187
|
+
'loopRegionStartFrame': loop_region_start_f,
|
|
188
|
+
'loopRegionEndFrame': -1.0, # not critical for non-looping, RTA uses targetEndSourceFrame based on duration
|
|
189
|
+
'loopsPerformedCurrent': 0,
|
|
190
|
+
'playDurationSourceFrames': calculated_play_duration_source_frames # store this for resume
|
|
187
191
|
}
|
|
188
192
|
|
|
189
193
|
# ensure the player is set to not loop for this single play command.
|
|
@@ -259,12 +263,12 @@ class AudioSample:
|
|
|
259
263
|
|
|
260
264
|
# Store loop settings for this voice
|
|
261
265
|
self._currentLoopSettings[voice] = {
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
266
|
+
'active': playerShouldLoop,
|
|
267
|
+
'loopCountTarget': actualLoopCountTarget,
|
|
268
|
+
'loopRegionStartFrame': loopRegionStartFrames,
|
|
269
|
+
'loopRegionEndFrame': loopRegionEndFrames,
|
|
270
|
+
'loopsPerformedCurrent': 0, # Reset on new loop command
|
|
271
|
+
'playDurationSourceFrames': -1.0 # Not used for active looping
|
|
268
272
|
}
|
|
269
273
|
|
|
270
274
|
player.play(startAtBeginning=startAtBeginningOfLoopSegment, # True to make it start from loop_region_start_frames
|
|
@@ -286,18 +290,18 @@ class AudioSample:
|
|
|
286
290
|
voice = 0
|
|
287
291
|
|
|
288
292
|
player = self._players[voice]
|
|
289
|
-
player.stop(immediate=True) #
|
|
293
|
+
player.stop(immediate=True) # stop sample playback immediately
|
|
290
294
|
|
|
291
|
-
#
|
|
295
|
+
# reset loop settings for this voice to default non-looping
|
|
292
296
|
self._currentLoopSettings[voice] = {
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
297
|
+
'active': False,
|
|
298
|
+
'loopCountTarget': 0,
|
|
299
|
+
'loopRegionStartFrame': 0.0,
|
|
300
|
+
'loopRegionEndFrame': -1.0,
|
|
301
|
+
'loopsPerformedCurrent': 0,
|
|
302
|
+
'playDurationSourceFrames': -1.0
|
|
299
303
|
}
|
|
300
|
-
self._isPausedFlags[voice] = False #
|
|
304
|
+
self._isPausedFlags[voice] = False # reset pause state on stop
|
|
301
305
|
|
|
302
306
|
def isPlaying(self, voice=0):
|
|
303
307
|
"""
|
|
@@ -379,13 +383,13 @@ class AudioSample:
|
|
|
379
383
|
play_duration_for_resume = loop_settings.get('playDurationSourceFrames', -1.0)
|
|
380
384
|
|
|
381
385
|
player.play(
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
386
|
+
startAtBeginning=False, # resume from current player.playbackPosition
|
|
387
|
+
loop=loop_settings['active'],
|
|
388
|
+
playDurationSourceFrames=play_duration_for_resume,
|
|
389
|
+
loopRegionStartFrame=loop_settings['loopRegionStartFrame'],
|
|
390
|
+
loopRegionEndFrame=loop_settings['loopRegionEndFrame'],
|
|
391
|
+
loopCountTarget=loop_settings['loopCountTarget'],
|
|
392
|
+
initialLoopsPerformed=loop_settings['loopsPerformedCurrent'] # pass stored count
|
|
389
393
|
)
|
|
390
394
|
self._isPausedFlags[voice] = False
|
|
391
395
|
else:
|
|
@@ -603,7 +607,7 @@ class AudioSample:
|
|
|
603
607
|
|
|
604
608
|
if voiceForThisPitch != None: # if a free voice exists...
|
|
605
609
|
# associate it with this pitch
|
|
606
|
-
if not self.voicesAllocatedToPitch
|
|
610
|
+
if pitch not in self.voicesAllocatedToPitch: # new pitch (not sounding already)?
|
|
607
611
|
self.voicesAllocatedToPitch[pitch] = [voiceForThisPitch] # remember that this voice is playing this pitch
|
|
608
612
|
else: # there is at least one other voice playing this pitch, so...
|
|
609
613
|
self.voicesAllocatedToPitch[pitch].append( voiceForThisPitch ) # append this voice (mimicking MIDI standard for polyphony of same pitches!!!)
|
|
@@ -642,7 +646,7 @@ class AudioSample:
|
|
|
642
646
|
|
|
643
647
|
voice = None # initialize
|
|
644
648
|
|
|
645
|
-
if self.voicesAllocatedToPitch
|
|
649
|
+
if pitch in self.voicesAllocatedToPitch and len( self.voicesAllocatedToPitch[pitch] ) > 0: # does this pitch have voices allocated to it?
|
|
646
650
|
voice = self.voicesAllocatedToPitch[pitch][0] # first voice used for this pitch
|
|
647
651
|
else: # pitch is not currently sounding, so...
|
|
648
652
|
raise ValueError("Pitch (" + str(pitch) + ") is not currently playing!!!")
|
|
@@ -664,7 +668,7 @@ class AudioSample:
|
|
|
664
668
|
raise TypeError("Pitch (" + str(pitch) + ") should be an int (range 0 and 127) or float (such as 440.0).")
|
|
665
669
|
|
|
666
670
|
# now, assume pitch contains a frequency (float)
|
|
667
|
-
if self.voicesAllocatedToPitch
|
|
671
|
+
if pitch in self.voicesAllocatedToPitch and len( self.voicesAllocatedToPitch[pitch] ) > 0: # does this pitch have voices allocated to it?
|
|
668
672
|
freedVoice = self.voicesAllocatedToPitch[pitch].pop(0) # deallocate first voice used for this pitch
|
|
669
673
|
self.freeVoices.append( freedVoice ) # and return it back to the pool of free voices
|
|
670
674
|
|
|
@@ -708,6 +712,228 @@ class AudioSample:
|
|
|
708
712
|
# this can happen if close() is called multiple times.
|
|
709
713
|
pass
|
|
710
714
|
|
|
715
|
+
|
|
716
|
+
class Envelope():
|
|
717
|
+
""" This class knows how to adjust the volume of an Audio Sample over time, in order to help shape its sound.
|
|
718
|
+
|
|
719
|
+
It consists of:
|
|
720
|
+
|
|
721
|
+
- a list of attack times (in milliseconds, relative from the previous time),
|
|
722
|
+
- a list of volumes - to be reached at the correspondng attack times (parallel lists),
|
|
723
|
+
- the delay time (in milliseconds - relative from the last attack time), of how long to wait to get to the sustain value (see next),
|
|
724
|
+
- the sustain value (volume to maintain while sustaining), and
|
|
725
|
+
- the release time (in milliseconds, relative from the END of the sound) - how long the fade-out is, i.e., to reach a volume of zero.
|
|
726
|
+
|
|
727
|
+
NOTE: Notice how all time values are relative to the previous one, with the exception of
|
|
728
|
+
|
|
729
|
+
- the first attack value, which is relative the start of the sound, and
|
|
730
|
+
- the release time, which is relative to (goes beyond) the end of the sound.
|
|
731
|
+
|
|
732
|
+
This last one is VERY important - i.e., release time goes past the end of the sound!!!
|
|
733
|
+
"""
|
|
734
|
+
|
|
735
|
+
def __init__(self, attackTimes = [2, 20], attackVolumes = [0.5, 0.8], delayTime = 20, sustainVolume = 1.0, releaseTime = 150):
|
|
736
|
+
"""
|
|
737
|
+
attack times - in milliseconds, first one is from start of sound, all others are relative to the previous one
|
|
738
|
+
attack volumes - range from 0.0 (silence) to 1.0 (max), parallel to attack times - volumes to reach at corresponding times
|
|
739
|
+
delay time - in milliseconds, relative from the last attack time - how long to wait to reach to sustain volume (see next)
|
|
740
|
+
sustain volume - 0.0 to 1.0, volume to maintain while playing the main body of the sound
|
|
741
|
+
release time - in milliseconds, relative to the END of the sound - how long to fade out (after end of sound).
|
|
742
|
+
"""
|
|
743
|
+
|
|
744
|
+
self.attackTimes = None # in milliseconds, relative from previous time
|
|
745
|
+
self.attackVolumes = None # and the corresponding volumes
|
|
746
|
+
self.delayTime = None # in milliseconds, relative from previous time
|
|
747
|
+
self.sustainVolume = None # to reach this volume
|
|
748
|
+
self.releaseTime = None # in milliseconds, length of fade out - beyond END of sound
|
|
749
|
+
|
|
750
|
+
# udpate above values (this will do appropriate error checks, so that we do not repeat that code twice here)
|
|
751
|
+
self.setAttackTimesAndVolumes(attackTimes, attackVolumes)
|
|
752
|
+
self.setDelayTime(delayTime)
|
|
753
|
+
self.setSustainVolume(sustainVolume)
|
|
754
|
+
self.setReleaseTime(releaseTime)
|
|
755
|
+
|
|
756
|
+
|
|
757
|
+
def setAttackTimesAndVolumes(self, attackTimes, attackVolumes):
|
|
758
|
+
""" Sets attack times and volumes. Attack times are in milliseconds, relative from previous time (first one is from start of sound).
|
|
759
|
+
Attack volumes are between 0.0 and 1.0, and are the corresponding volumes to be set at the given times.
|
|
760
|
+
|
|
761
|
+
NOTE: We do not provide individual functions to set attack times and to set volumes,
|
|
762
|
+
as it is very hard to check for parallelism (same length constraint) - a chicken-and-egg problem...
|
|
763
|
+
"""
|
|
764
|
+
|
|
765
|
+
# make sure attack times and volumes are parallel
|
|
766
|
+
if len(attackTimes) != len(attackVolumes):
|
|
767
|
+
|
|
768
|
+
raise IndexError("Attack times and volumes must have the same length.")
|
|
769
|
+
|
|
770
|
+
# make sure attack times are all ints, greater than zero
|
|
771
|
+
for attackTime in attackTimes:
|
|
772
|
+
|
|
773
|
+
if attackTime < 0:
|
|
774
|
+
|
|
775
|
+
raise ValueError("Attack times should be zero or positive (found " + str(attackTime) + ").")
|
|
776
|
+
|
|
777
|
+
# make sure attack volumes are all floats between 0.0 and 1.0 (inclusive).
|
|
778
|
+
for attackVolume in attackVolumes:
|
|
779
|
+
|
|
780
|
+
if attackVolume < 0.0 or 1.0 < attackVolume:
|
|
781
|
+
|
|
782
|
+
raise ValueError("Attack volumes should be between 0.0 and 1.0 (found " + str(attackVolume) + ").")
|
|
783
|
+
|
|
784
|
+
# all well, so update
|
|
785
|
+
self.attackTimes = attackTimes
|
|
786
|
+
self.attackVolumes = attackVolumes
|
|
787
|
+
|
|
788
|
+
|
|
789
|
+
def getAttackTimesAndVolumes(self):
|
|
790
|
+
""" Returns list of attack times and corresponding volumes. No need for individual getter functions - these lists go together. """
|
|
791
|
+
|
|
792
|
+
return [self.attackTimes, self.attackVolumes]
|
|
793
|
+
|
|
794
|
+
|
|
795
|
+
def setDelayTime(self, delayTime):
|
|
796
|
+
""" Sets delay time. """
|
|
797
|
+
|
|
798
|
+
# make input value is appropriate
|
|
799
|
+
if delayTime < 0:
|
|
800
|
+
|
|
801
|
+
raise ValueError("Delay time must 0 or greater (in milliseconds).")
|
|
802
|
+
|
|
803
|
+
# all well, so update
|
|
804
|
+
self.delayTime = delayTime
|
|
805
|
+
|
|
806
|
+
|
|
807
|
+
def getDelayTime(self):
|
|
808
|
+
""" Returns delay time. """
|
|
809
|
+
|
|
810
|
+
return self.delayTime
|
|
811
|
+
|
|
812
|
+
|
|
813
|
+
def setSustainVolume(self, sustainVolume):
|
|
814
|
+
""" Sets sustain volume. """
|
|
815
|
+
|
|
816
|
+
# make input value is appropriate
|
|
817
|
+
if sustainVolume < 0.0 or sustainVolume > 1.0:
|
|
818
|
+
|
|
819
|
+
raise ValueError("Sustain volume must be between 0.0 and 1.0.")
|
|
820
|
+
|
|
821
|
+
# all well, so update
|
|
822
|
+
self.sustainVolume = sustainVolume
|
|
823
|
+
|
|
824
|
+
|
|
825
|
+
def getSustainVolume(self):
|
|
826
|
+
""" Returns sustain volume. """
|
|
827
|
+
|
|
828
|
+
return self.sustainVolume
|
|
829
|
+
|
|
830
|
+
|
|
831
|
+
def setReleaseTime(self, releaseTime):
|
|
832
|
+
""" Sets release time. """
|
|
833
|
+
|
|
834
|
+
# make input value is appropriate
|
|
835
|
+
if releaseTime < 0:
|
|
836
|
+
|
|
837
|
+
raise ValueError("Release time must 0 or greater (in milliseconds).")
|
|
838
|
+
|
|
839
|
+
# all well, so update
|
|
840
|
+
self.releaseTime = releaseTime
|
|
841
|
+
|
|
842
|
+
|
|
843
|
+
def getReleaseTime(self):
|
|
844
|
+
""" Returns release time. """
|
|
845
|
+
|
|
846
|
+
return self.releaseTime
|
|
847
|
+
|
|
848
|
+
|
|
849
|
+
def performAttackDelaySustain(self, audioSample, volume, voice):
|
|
850
|
+
""" Applies the beginning of the envelope to the given voice of the provided audio sample. This involves setting up appropriate timers
|
|
851
|
+
to adjust volume, at appropriate times, as dictated by the envelope settings.
|
|
852
|
+
"""
|
|
853
|
+
|
|
854
|
+
# NOTE: In order to allow the same envelope to be re-used by different audio samples, we place inside the audio sample
|
|
855
|
+
# a dictionary of timers, indexed by voice. This way different audio samples will not compete with each other, if they are all
|
|
856
|
+
# using the same envelope.
|
|
857
|
+
#
|
|
858
|
+
# Each voice has its own list of timers - implementing the envelope, while it is sounding
|
|
859
|
+
# This way, we can stop these timers, if the voice sounds less time than what the envelope - not an error (we will try and do our best)
|
|
860
|
+
|
|
861
|
+
# initialize envelope timers for this audio sample
|
|
862
|
+
if "envelopeTimers" not in dir(audioSample): # is this the first time we see this audio sample?
|
|
863
|
+
|
|
864
|
+
audioSample.envelopeTimers = {} # yes, so initiliaze dictionary of envelope timers
|
|
865
|
+
|
|
866
|
+
# now, we have a dictionary of envelope timers
|
|
867
|
+
|
|
868
|
+
# next, initiliaze list of timers for this voice (we may assume that none exists...)
|
|
869
|
+
audioSample.envelopeTimers[voice] = []
|
|
870
|
+
|
|
871
|
+
# set initial volume to zero
|
|
872
|
+
audioSample.setVolume(volume = 0, delay = 2, voice = voice)
|
|
873
|
+
|
|
874
|
+
# initialize variables
|
|
875
|
+
maxVolume = volume # audio sample's requested volume... everything will be adjusted relative to that
|
|
876
|
+
nextTime = 0 # next time to begin volume adjustment - start at beginning of sound
|
|
877
|
+
|
|
878
|
+
# schedule attack timers
|
|
879
|
+
for attackTime, attackVolume in zip(self.attackTimes, self.attackVolumes):
|
|
880
|
+
|
|
881
|
+
# adjust volume appropriately
|
|
882
|
+
volume = int(maxVolume * attackVolume) # attackVolume ranges between 0.0 and 1.0, so we treat it as relative factor
|
|
883
|
+
|
|
884
|
+
# schedule volume change over this attack time
|
|
885
|
+
# NOTE: attackTime indicates how long this volume change should take!!!
|
|
886
|
+
timer = Timer2(nextTime, audioSample.setVolume, [volume, attackTime, voice], False)
|
|
887
|
+
#print "attack set - volume, delay, voice =", volume, nextTime, voice #***
|
|
888
|
+
|
|
889
|
+
# remember timer
|
|
890
|
+
audioSample.envelopeTimers[voice].append( timer )
|
|
891
|
+
|
|
892
|
+
# advance time
|
|
893
|
+
nextTime = nextTime + attackTime
|
|
894
|
+
|
|
895
|
+
# now, all attack timers have been created
|
|
896
|
+
|
|
897
|
+
# next, create timer to handle delay and sustain setting
|
|
898
|
+
volume = int(maxVolume * self.sustainVolume) # sustainVolume ranges between 0.0 and 1.0, so we treat it as relative factor
|
|
899
|
+
|
|
900
|
+
# schedule volume change over delay time
|
|
901
|
+
# NOTE: delay time indicates how long this volume change should take!!!
|
|
902
|
+
timer = Timer2(nextTime, audioSample.setVolume, [volume, self.delayTime, voice], False)
|
|
903
|
+
#print "delay set - volume, voice =", volume, voice #***
|
|
904
|
+
|
|
905
|
+
# remember timer
|
|
906
|
+
audioSample.envelopeTimers[voice].append( timer )
|
|
907
|
+
|
|
908
|
+
# beginning of envelope has been set up, so start timers to make things happen
|
|
909
|
+
for timer in audioSample.envelopeTimers[voice]:
|
|
910
|
+
timer.start()
|
|
911
|
+
|
|
912
|
+
# done!!!
|
|
913
|
+
|
|
914
|
+
|
|
915
|
+
def performReleaseAndStop(self, audioSample, voice):
|
|
916
|
+
""" Applies the release time (fade out) to the given voice of the provided audioSample. """
|
|
917
|
+
|
|
918
|
+
# stop any remaining timers, and empty list
|
|
919
|
+
for timer in audioSample.envelopeTimers[voice]:
|
|
920
|
+
timer.stop()
|
|
921
|
+
|
|
922
|
+
# empty list of timers - they are not needed anymore (clean up for next use...)
|
|
923
|
+
del audioSample.envelopeTimers[voice]
|
|
924
|
+
|
|
925
|
+
# turn volume down to zero, slowly, over release time milliseconds
|
|
926
|
+
audioSample.setVolume(volume = 0, delay = self.releaseTime, voice = voice)
|
|
927
|
+
#print "release set - volume, voice =", 0, voice #***
|
|
928
|
+
|
|
929
|
+
# and schedule sound to stop, after volume has been turned down completely
|
|
930
|
+
someMoreTime = 5 # to give a little extra time for things to happen (just in case) - in milliseconds (avoids clicking...)
|
|
931
|
+
timer = Timer2(self.releaseTime + someMoreTime, audioSample.stop, [voice], False)
|
|
932
|
+
timer.start()
|
|
933
|
+
|
|
934
|
+
# done!!!
|
|
935
|
+
|
|
936
|
+
|
|
711
937
|
def _cleanupAudioSamples():
|
|
712
938
|
"""Stops and closes all active AudioSample players registered with atexit."""
|
|
713
939
|
# iterate over a copy because sample.__del__() will modify _activeAudioSamples
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: CreativePython
|
|
3
|
+
Version: 0.0.2
|
|
4
|
+
Summary: A Python-based software environment for developing algorithmic art projects.
|
|
5
|
+
Author-email: "Dr. Bill Manaris" <manaris@cofc.edu>, Taj Ballinger <ballingertj@g.cofc.edu>, Trevor Ritchie <ritchiets@g.cofc.edu>
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2025 Dr. Bill Manaris
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
|
|
28
|
+
Project-URL: Homepage, https://jythonmusic.me
|
|
29
|
+
Keywords: audio,midi,learning,algorithmic art,algoart
|
|
30
|
+
Classifier: Programming Language :: Python :: 3
|
|
31
|
+
Classifier: Operating System :: OS Independent
|
|
32
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
33
|
+
Requires-Python: >=3.9
|
|
34
|
+
Description-Content-Type: text/markdown
|
|
35
|
+
License-File: LICENSE
|
|
36
|
+
Provides-Extra: dev
|
|
37
|
+
Requires-Dist: build; extra == "dev"
|
|
38
|
+
Requires-Dist: twine; extra == "dev"
|
|
39
|
+
Dynamic: license-file
|
|
40
|
+
|
|
41
|
+
# CreativePython
|
|
42
|
+
|
|
43
|
+
CreativePython is a Python-based software environment for developing algorithmic art projects. It mirrors the [JythonMusic API](https://jythonmusic.me/api-reference/).
|
|
44
|
+
|
|
45
|
+
This package is still under development.
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## Quick Install
|
|
50
|
+
|
|
51
|
+
Download the [CreativePython Install Scripts](https://www.dropbox.com/scl/fo/if4jt56r9noqgj47rk2cq/AFRANez_l8BmKUU19-GAbh4?rlkey=uws2jof8hq2zpj0oke0rt7886&dl=1)
|
|
52
|
+
|
|
53
|
+
### Windows
|
|
54
|
+
1. Right-click `windows_setup.bat` → **Run as Administrator** (it will open PowerShell and run).
|
|
55
|
+
2. Follow any prompts and let it finish.
|
|
56
|
+
|
|
57
|
+
### macOS
|
|
58
|
+
1. Control-click `mac_setup.command` → **Open** (it will open Terminal and run).
|
|
59
|
+
2. Follow any prompts and let it finish.
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## Custom Installation
|
|
64
|
+
|
|
65
|
+
See "Advanced Installation" in [INSTALL.md](https://www.dropbox.com/scl/fi/us1j6im3ef67lzyfvq6ub/INSTALL.md?rlkey=fu9yjzj1hk11p6wgmhsc27s8y&dl=0).
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
MANIFEST.in
|
|
3
|
+
README.md
|
|
4
|
+
pyproject.toml
|
|
5
|
+
src/AudioSample.py
|
|
6
|
+
src/_RealtimeAudioPlayer.py
|
|
7
|
+
src/__init__.py
|
|
8
|
+
src/creativepython_setup.py
|
|
9
|
+
src/gui.py
|
|
10
|
+
src/image.py
|
|
11
|
+
src/midi.py
|
|
12
|
+
src/music.py
|
|
13
|
+
src/osc.py
|
|
14
|
+
src/timer.py
|
|
15
|
+
src/CreativePython.egg-info/PKG-INFO
|
|
16
|
+
src/CreativePython.egg-info/SOURCES.txt
|
|
17
|
+
src/CreativePython.egg-info/dependency_links.txt
|
|
18
|
+
src/CreativePython.egg-info/entry_points.txt
|
|
19
|
+
src/CreativePython.egg-info/requires.txt
|
|
20
|
+
src/CreativePython.egg-info/top_level.txt
|