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.
Files changed (30) hide show
  1. creativepython-0.0.2/MANIFEST.in +1 -0
  2. creativepython-0.0.2/PKG-INFO +65 -0
  3. creativepython-0.0.2/README.md +25 -0
  4. creativepython-0.0.2/pyproject.toml +45 -0
  5. {creativepython-0.0.1/src/CreativePython → creativepython-0.0.2/src}/AudioSample.py +257 -31
  6. creativepython-0.0.2/src/CreativePython.egg-info/PKG-INFO +65 -0
  7. creativepython-0.0.2/src/CreativePython.egg-info/SOURCES.txt +20 -0
  8. creativepython-0.0.2/src/CreativePython.egg-info/entry_points.txt +3 -0
  9. creativepython-0.0.2/src/CreativePython.egg-info/requires.txt +4 -0
  10. creativepython-0.0.2/src/CreativePython.egg-info/top_level.txt +11 -0
  11. {creativepython-0.0.1/src/CreativePython → creativepython-0.0.2/src}/_RealtimeAudioPlayer.py +15 -10
  12. creativepython-0.0.2/src/creativepython_setup.py +101 -0
  13. {creativepython-0.0.1/src/CreativePython → creativepython-0.0.2/src}/gui.py +52 -52
  14. {creativepython-0.0.1/src/CreativePython → creativepython-0.0.2/src}/midi.py +5 -5
  15. {creativepython-0.0.1/src/CreativePython → creativepython-0.0.2/src}/music.py +549 -203
  16. {creativepython-0.0.1/src/CreativePython → creativepython-0.0.2/src}/timer.py +323 -106
  17. creativepython-0.0.1/MANIFEST.in +0 -2
  18. creativepython-0.0.1/PKG-INFO +0 -19
  19. creativepython-0.0.1/README.md +0 -5
  20. creativepython-0.0.1/pyproject.toml +0 -25
  21. creativepython-0.0.1/src/CreativePython/images/de-brazzas-monkey.jpg +0 -0
  22. creativepython-0.0.1/src/CreativePython.egg-info/PKG-INFO +0 -19
  23. creativepython-0.0.1/src/CreativePython.egg-info/SOURCES.txt +0 -18
  24. creativepython-0.0.1/src/CreativePython.egg-info/top_level.txt +0 -1
  25. {creativepython-0.0.1 → creativepython-0.0.2}/LICENSE +0 -0
  26. {creativepython-0.0.1 → creativepython-0.0.2}/setup.cfg +0 -0
  27. {creativepython-0.0.1 → creativepython-0.0.2}/src/CreativePython.egg-info/dependency_links.txt +0 -0
  28. {creativepython-0.0.1/src/CreativePython → creativepython-0.0.2/src}/__init__.py +0 -0
  29. {creativepython-0.0.1/src/CreativePython → creativepython-0.0.2/src}/image.py +0 -0
  30. {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
- 'active': False, # signifies it's a play-once (or play-duration)
182
- 'loopCountTarget': 0, # ensures it's treated as non-looping by RTA if loop=False
183
- 'loopRegionStartFrame': loop_region_start_f,
184
- 'loopRegionEndFrame': -1.0, # not critical for non-looping, RTA uses targetEndSourceFrame based on duration
185
- 'loopsPerformedCurrent': 0,
186
- 'playDurationSourceFrames': calculated_play_duration_source_frames # store this for resume
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
- 'active': playerShouldLoop,
263
- 'loopCountTarget': actualLoopCountTarget,
264
- 'loopRegionStartFrame': loopRegionStartFrames,
265
- 'loopRegionEndFrame': loopRegionEndFrames,
266
- 'loopsPerformedCurrent': 0, # Reset on new loop command
267
- 'playDurationSourceFrames': -1.0 # Not used for active looping
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) # Corresponds to API "Stops sample playback immediately"
293
+ player.stop(immediate=True) # stop sample playback immediately
290
294
 
291
- # Reset loop settings for this voice to default non-looping
295
+ # reset loop settings for this voice to default non-looping
292
296
  self._currentLoopSettings[voice] = {
293
- 'active': False,
294
- 'loopCountTarget': 0,
295
- 'loopRegionStartFrame': 0.0,
296
- 'loopRegionEndFrame': -1.0,
297
- 'loopsPerformedCurrent': 0,
298
- 'playDurationSourceFrames': -1.0
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 # Reset pause state on stop
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
- startAtBeginning=False, # resume from current player.playbackPosition
383
- loop=loop_settings['active'],
384
- playDurationSourceFrames=play_duration_for_resume,
385
- loopRegionStartFrame=loop_settings['loopRegionStartFrame'],
386
- loopRegionEndFrame=loop_settings['loopRegionEndFrame'],
387
- loopCountTarget=loop_settings['loopCountTarget'],
388
- initialLoopsPerformed=loop_settings['loopsPerformedCurrent'] # pass stored count
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.has_key(pitch): # new pitch (not sounding already)?
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.has_key(pitch) and len( self.voicesAllocatedToPitch[pitch] ) > 0: # does this pitch have voices allocated to it?
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.has_key(pitch) and len( self.voicesAllocatedToPitch[pitch] ) > 0: # does this pitch have voices allocated to it?
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
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ cp-setup = creativepython_setup:run
3
+ cp-test = creativepython_setup:playNote
@@ -0,0 +1,4 @@
1
+
2
+ [dev]
3
+ build
4
+ twine
@@ -0,0 +1,11 @@
1
+ AudioSample
2
+ _RealtimeAudioPlayer
3
+ __init__
4
+ creativepython_setup
5
+ gui
6
+ image
7
+ images
8
+ midi
9
+ music
10
+ osc
11
+ timer