CreativePython 0.0.1__py3-none-any.whl → 0.0.2__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.
@@ -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
@@ -7,7 +7,9 @@
7
7
  # [LICENSING GOES HERE]
8
8
  #
9
9
  #######################################################################################
10
- #
10
+ # TODO:
11
+ # - fade in/out logic is incomplete. We can probably remove it to avoid overhead.
12
+ # Thought it might be nice to prevent popping sounds.
11
13
  #
12
14
  #######################################################################################
13
15
 
@@ -16,7 +18,6 @@ import soundfile as sf # for audio file reading
16
18
  import numpy as np # for array operations
17
19
  import os # for file path operations
18
20
  import math # for logarithmic calculations in pitch/frequency conversions
19
- import time # for timing operations
20
21
 
21
22
  # helper functions
22
23
  def noteToFreq(pitch):
@@ -88,12 +89,12 @@ class _RealtimeAudioPlayer:
88
89
  self.baseFrequency = noteToFreq(self.basePitch) # default 440 Hz
89
90
 
90
91
  # fades (master)
91
- self.fadeInDurationMs = 30
92
+ self.fadeInDurationMs = 20
92
93
  self.fadeInTotalFrames = max(1, int(self.sampleRate * (self.fadeInDurationMs / 1000.0)))
93
94
  self.fadeInFramesProcessed = 0
94
95
  self.isApplyingFadeIn = False
95
96
 
96
- self.fadeOutDurationMs = 50
97
+ self.fadeOutDurationMs = 30
97
98
  self.fadeOutTotalFrames = max(1, int(self.sampleRate * (self.fadeOutDurationMs / 1000.0)))
98
99
  self.fadeOutFramesProcessed = 0
99
100
  self.isApplyingFadeOut = False
@@ -434,7 +435,7 @@ class _RealtimeAudioPlayer:
434
435
  # self.playbackPosition is advanced by self.rateFactor. If rateFactor is 1.0, it moves one sample forward.
435
436
  # If rateFactor is 0.5, it effectively plays at half speed (each source sample is held for two output samples, due to interpolation).
436
437
  # If rateFactor is 2.0, it plays at double speed (skipping source samples, with interpolation filling the gaps).
437
- self.playbackPosition += self.rateFactor # RESTORED - No more * 0.5 hack
438
+ self.playbackPosition += self.rateFactor
438
439
 
439
440
  # This is the point at which we should either loop or stop for the current play segment,
440
441
  # so determine the effective end frame for the current segment/loop
@@ -610,10 +611,10 @@ class _RealtimeAudioPlayer:
610
611
 
611
612
  self.sdStream = sd.OutputStream(
612
613
  samplerate=self.sampleRate,
613
- blocksize=self.chunkSize,
614
- channels=self.numChannels,
615
- callback=self.audioCallback,
616
- finished_callback=self.onPlaybackFinished
614
+ blocksize=self.chunkSize,
615
+ channels=self.numChannels,
616
+ callback=self.audioCallback,
617
+ finished_callback=self.onPlaybackFinished
617
618
  )
618
619
  self.sdStream.start()
619
620
  self.isPlaying = True
@@ -669,6 +670,7 @@ class _RealtimeAudioPlayer:
669
670
  else: print(f"Error closing PyAudio stream: {pae}")
670
671
  except Exception as e: print(f"Generic error closing PyAudio stream: {e}")
671
672
  self.sdStream = None
673
+
672
674
  self.isPlaying = False # confirm state
673
675
  self.targetEndSourceFrame = -1.0 # ensure reset if stopped externally
674
676
  self.playDurationSourceFrames = -1.0 # ensure reset
@@ -679,6 +681,7 @@ class _RealtimeAudioPlayer:
679
681
  self.loopsPerformed = 0
680
682
  return
681
683
 
684
+ # if we want to skip the nice fade-out for immediacy
682
685
  if immediate:
683
686
  self.isPlaying = False # signal callback to stop producing audio
684
687
  self.isApplyingFadeIn = False # cancel any ongoing fade-in
@@ -704,12 +707,14 @@ class _RealtimeAudioPlayer:
704
707
  print(f"Error during immediate stream stop/close: {e}")
705
708
  finally:
706
709
  self.sdStream = None # discard stream instance
710
+
707
711
  self.targetEndSourceFrame = -1.0
708
712
  self.playDurationSourceFrames = -1.0
709
713
  self.loopRegionStartFrame = 0.0
710
714
  self.loopRegionEndFrame = -1.0
711
715
  self.loopCountTarget = -1 if self.looping else 0
712
716
  self.loopsPerformed = 0
717
+
713
718
  else: # gradual stop (fade out)
714
719
  if self.isPlaying or self.isApplyingFadeIn: # only start a fade-out if actually playing or was about to start
715
720
  self.isApplyingFadeIn = False # stop any fade-in
@@ -742,7 +747,7 @@ class _RealtimeAudioPlayer:
742
747
  else:
743
748
  print(f"PortAudioError during stream stop/close: {pae}")
744
749
  # raise # Optionally re-raise if it's a different PortAudio error
745
- print(f"Generic error during PortAudio stream stop/close: {e}")
750
+ print(f"Generic error during PortAudio stream stop/close: {pae}")
746
751
  # raise # Optionally re-raise
747
752
  finally:
748
753
  self.sdStream = None
@@ -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,16 @@
1
+ AudioSample.py,sha256=oI3C-Y4ECZnyu3W2vV96BlRs6HrKZYtqSn6bgtDvgVU,45217
2
+ _RealtimeAudioPlayer.py,sha256=bbH_R69yrGulkAV1J2apQzGS_jqADQhOenQNFapif4k,40138
3
+ __init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ creativepython_setup.py,sha256=NRi2_MaoeV1w8QOyWXan-VpIEJR0yTqTbBeY-SDfryE,3344
5
+ gui.py,sha256=gx0RDyh-5xF4KFh0F4G-V89ESc1Jl_UlmqUi0W7WD8w,130358
6
+ image.py,sha256=TEFjfqL-EikGlcMcj2QlHqwkEHaDp6oGAK2Vm--1dEs,2243
7
+ midi.py,sha256=sFJ8s--YYWJE8v9YHtW7bReyyaOd6eks4A9IVVyAFRE,40068
8
+ music.py,sha256=y4dNaW6SSkcz-dA3TIy5dm3w5DEuaTy-wive_yRRBkA,173577
9
+ osc.py,sha256=EhYo9DtGax8WQe1vD30xzdkRX1GJu65b1AkZiSmPeQY,16013
10
+ timer.py,sha256=bMBbdu3NtnmWFxcykiXdTBGK4J3QDEzPp-Nyb3OJzPs,22470
11
+ creativepython-0.0.2.dist-info/licenses/LICENSE,sha256=1hbJHc1vxY4vAasDE4CMC4wDH91CBz5B-rgfujDntTw,1073
12
+ creativepython-0.0.2.dist-info/METADATA,sha256=DwQ1P5R5YIc41IhovuWcDdT4zcwfNvPyVWSpn7D9a40,2865
13
+ creativepython-0.0.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
14
+ creativepython-0.0.2.dist-info/entry_points.txt,sha256=iYj4CILkX4SebZA63f7dvmz0up_xoTOUzgNifR6O1Pw,94
15
+ creativepython-0.0.2.dist-info/top_level.txt,sha256=S4p4nuCIv0l-lT3yPofgQ-Rf2QJrbZZKEqBGi4imI48,94
16
+ creativepython-0.0.2.dist-info/RECORD,,
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ cp-setup = creativepython_setup:run
3
+ cp-test = creativepython_setup:playNote
@@ -0,0 +1,10 @@
1
+ AudioSample
2
+ _RealtimeAudioPlayer
3
+ __init__
4
+ creativepython_setup
5
+ gui
6
+ image
7
+ midi
8
+ music
9
+ osc
10
+ timer
@@ -0,0 +1,101 @@
1
+ ### SOUNDFONT DOWNLOAD HELPERS
2
+
3
+ from __future__ import annotations
4
+ from pathlib import Path
5
+ from platformdirs import user_data_dir
6
+
7
+ APP = "CreativePython"
8
+ ORG = "CofC"
9
+
10
+ SOUNDFONT_NAME = "default.sf2"
11
+ CACHE_DIR = Path(user_data_dir(APP, ORG)) / "SoundFonts"
12
+ SOUNDFONT_PATH = CACHE_DIR / SOUNDFONT_NAME
13
+
14
+ SF2_URL = "https://www.dropbox.com/s/xixtvox70lna6m2/FluidR3%20GM2-2.SF2?dl=1"
15
+ SF2_SHA256 = "2ae766ab5c5deb6f7fffacd6316ec9f3699998cce821df3163e7b10a78a64066"
16
+
17
+ def find_soundfont(explicit: str | None = None) -> Path | None:
18
+ """
19
+ Finds a soundfont 'default.sf2' in the cache, and returns its location.
20
+ explicit can be another path that may have the soundfont needed.
21
+ """
22
+ import os
23
+
24
+ candidates = []
25
+ if explicit:
26
+ candidates.append(Path(explicit))
27
+ env = os.getenv("CREATIVEPYTHON_SOUNDFONT")
28
+ if env:
29
+ candidates.append(Path(env))
30
+ candidates += [SOUNDFONT_PATH, Path.home() / "SoundFonts" / SOUNDFONT_NAME]
31
+ for c in candidates:
32
+ if c and c.exists():
33
+ return c
34
+ return None
35
+
36
+ def download_soundfont(dest: Path = SOUNDFONT_PATH) -> Path:
37
+ """Downloads a default soundfont and saves it to the user's cache."""
38
+ from pooch import retrieve, Unzip
39
+ dest.parent.mkdir(parents=True, exist_ok=True)
40
+ p = retrieve(url=SF2_URL, known_hash=f"sha256:{SF2_SHA256}", progressbar=True, fname=dest.name, path=str(dest.parent))
41
+ return Path(p)
42
+
43
+
44
+
45
+ ### AUTOMATIC CHECK METHODS
46
+
47
+ def run():
48
+ import shutil, sys, os
49
+ missing = []
50
+
51
+ # Check for soundfont file
52
+ sf = find_soundfont()
53
+
54
+ # Attempt to download missing soundfont
55
+ if not sf:
56
+ print("CreativePython setup warning: No soundfont found.")
57
+ auto = os.getenv("CP_AUTO_DOWNLOAD") == "1"
58
+ if auto or input("Download the default soundfont now? [Y/n] ").strip().lower() in ("", "y", "yes"):
59
+ path = download_soundfont()
60
+ print(f"Downloaded to: {path}")
61
+ else:
62
+ print(f"\nPlace your .sf2 soundfont at:\n {SOUNDFONT_PATH}\nOr set CREATIVEPYTHON_SOUNDFONT to its location in your PATH.")
63
+ print("\n(see https://pypi.org/project/CreativePython/ for more details)")
64
+
65
+
66
+ # Check for required installations
67
+ if not shutil.which("ffmpeg"):
68
+ missing.append("ffmpeg")
69
+
70
+ if not shutil.which("fluidsynth"):
71
+ missing.append("FluidSynth")
72
+
73
+ if not shutil.which("portaudio"):
74
+ missing.append("PortAudio")
75
+
76
+ # Warn user about missing requirements
77
+ if missing:
78
+ print("\nCreativePython setup warning:")
79
+ for req in missing:
80
+ print(f" - {req} is not installed.")
81
+
82
+ if sys.platform.startswith("darwin"):
83
+ print("\nOn macOS, run:\n brew install portaudio fluidsynth ffmpeg")
84
+ elif sys.platform.startswith("win"):
85
+ print("\nOn Windows, install missing packages from developers, or run windows_setup.bat")
86
+ else:
87
+ print("\nOn Linux, install missing packages with your package manager (apt, dnf, etc.)")
88
+
89
+ print("\n(see https://pypi.org/project/CreativePython/ for more details)")
90
+
91
+ return len(missing) == 0
92
+
93
+
94
+ def playNote():
95
+ ready = run() # check for installation dependencies
96
+
97
+ if ready:
98
+ from music import Note, Play, C4, HN # can't use * within function, so naming needed pieces
99
+
100
+ note = Note(C4, HN) # create a middle C half note
101
+ Play.midi(note) # and play it!