diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b201f56 --- /dev/null +++ b/.gitignore @@ -0,0 +1,64 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.pytest_cache/ +.ipynb_checkpoints/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*,cover +.hypothesis/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# pyenv python configuration file +.python-version diff --git a/README.rst b/README.rst old mode 100644 new mode 100755 index b7a2536..1fa44ea --- a/README.rst +++ b/README.rst @@ -1,4 +1,3 @@ - python-sonic - Programming Music with Python, Sonic Pi or Supercollider ======================================================================= @@ -8,26 +7,25 @@ great music software created by Sam Aaron (http://sonic-pi.net). At the moment Python-Sonic works with Sonic Pi. It is planned, that it will work with Supercollider, too. +This version supports Sonic Pi versions > 4 when OSC run-code security +was added. + If you like it, use it. If you have some suggestions, tell me (gkvoelkl@nelson-games.de). Installation ------------ -- First you need Python 3 (https://www.python.org, ) - Python 3.4 - should work, because it's the development environment +- First you need Python 3 (https://www.python.org, ) - Python 3.5 + should work, because it’s the development environment - Then Sonic Pi (https://sonic-pi.net) - That makes the sound - Modul python-osc (https://pypi.python.org/pypi/python-osc) - Connection between Python and Sonic Pi Server -- And this modul python-sonic - simply copy the source - -Or try +- Add the module `python-sonic` - simply copy the source, or try `pip install python-sonic` That should work. -.. code:: python +For local development you might want to locally install using - $ pip install python-sonic - -That should work. +$ pip install -e . Limitations ----------- @@ -36,24 +34,94 @@ Limitations python-sonic - Only the notes from C2 to C6 +Changelog +--------- + ++--------+-------------------------------------------------------------+ +| V | | +| ersion | | ++========+=============================================================+ +| 0.2.0 | Some changes for Sonic Pi 2.11. Simpler multi-threading | +| | with decorator *@in_thread*. Messaging with *cue* and | +| | *sync*. | ++--------+-------------------------------------------------------------+ +| 0.3.0 | OSC Communication | ++--------+-------------------------------------------------------------+ +| 0.3.1. | Update, sort and duration of samples | ++--------+-------------------------------------------------------------+ +| 0.3.2. | Restructured | ++--------+-------------------------------------------------------------+ +| 0.4.0 | Changes communication ports and recording | ++--------+-------------------------------------------------------------+ +| 0.4.4 | Enables GUI Token | ++--------+-------------------------------------------------------------+ + +Communication +------------- + +The API *python-sonic* communications with *Sonic Pi* over UDP and two +ports. One port is an internal *Sonic Pi* GUI port and the second is the +external OSC cue port. + +For >v4 a Token is needed to communicate with Sonic Pi. This token is generated +randomly at runtime when Sonic-Pi is started. The Token is extracted from the sonic-pi log files. +Similarly, the GUI udp port is now randomised at start and must be read from the log file too. +In order to play notes (and not just send OSC cues), a connection must be made to the GUI udp port +using the Token as the first argument in the message. The Token is automatically used on send. + +.. code-block:: python + + from psonic import * + set_server_parameter('127.0.0.1', -2005799440, 30129, 4560) + + +These values are found from the file `~/.sonic-pi/log/spider.log` +:: + Sonic Pi Spider Server booting... + The time is 2023-10-01 11:01:41 +0100 + Using primary protocol: udp + Detecting port numbers... + Ports: {:server_port=>30129, :gui_port=>30130, :scsynth_port=>30131, :scsynth_send_port=>30131, :osc_cues_port=>4560, :tau_port=>30132, :listen_to_tau_port=>30136} + Token: -2005799440 + Opening UDP Server to listen to GUI on port: 30129 + Spider - Pulling in modules... + Spider - Starting Runtime Server + ... + +This can be automated by using the function `set_server_parameter_from_log` + +.. code-block:: python + + from psonic import * + set_server_parameter_from_log('127.0.0.1') + set_server_parameter_from_log('127.0.0.1', "path-to-log-file") + +Note if the `set_server_parameter` functions are not used, a default connection is created which will not work. + +There is a simple example file `psonic_example.py` which you can run to check that things work. First open sonic-pi, then run the following: + +$ python psonic_example.py + +and a note should be played. + Examples -------- Many of the examples are inspired from the help menu in *Sonic Pi*. -.. code:: python +.. code-block:: python from psonic import * The first sound -.. code:: python +.. code-block:: python play(70) #play MIDI note 70 Some more notes -.. code:: python +.. code-block:: python play(72) sleep(1) @@ -63,7 +131,7 @@ Some more notes In more tratitional music notation -.. code:: python +.. code-block:: python play(C5) sleep(0.5) @@ -73,7 +141,7 @@ In more tratitional music notation Play sharp notes like *F#* or dimished ones like *Eb* -.. code:: python +.. code-block:: python play(Fs5) sleep(0.5) @@ -82,7 +150,7 @@ Play sharp notes like *F#* or dimished ones like *Eb* Play louder (parameter amp) or from a different direction (parameter pan) -.. code:: python +.. code-block:: python play(72,amp=2) sleep(0.5) @@ -90,7 +158,7 @@ pan) Different synthesizer sounds -.. code:: python +.. code-block:: python use_synth(SAW) play(38) @@ -103,46 +171,46 @@ Different synthesizer sounds ADSR *(Attack, Decay, Sustain and Release)* Envelope -.. code:: python +.. code-block:: python play (60, attack=0.5, decay=1, sustain_level=0.4, sustain=2, release=0.5) sleep(4) Play some samples -.. code:: python +.. code-block:: python sample(AMBI_LUNAR_LAND, amp=0.5) -.. code:: python +.. code-block:: python sample(LOOP_AMEN,pan=-1) sleep(0.877) sample(LOOP_AMEN,pan=1) -.. code:: python +.. code-block:: python sample(LOOP_AMEN,rate=0.5) -.. code:: python +.. code-block:: python sample(LOOP_AMEN,rate=1.5) -.. code:: python +.. code-block:: python sample(LOOP_AMEN,rate=-1)#back -.. code:: python +.. code-block:: python sample(DRUM_CYMBAL_OPEN,attack=0.01,sustain=0.3,release=0.1) -.. code:: python +.. code-block:: python sample(LOOP_AMEN,start=0.5,finish=0.8,rate=-0.2,attack=0.3,release=1) Play some random notes -.. code:: python +.. code-block:: python import random @@ -150,15 +218,29 @@ Play some random notes play(random.randrange(50, 100)) sleep(0.5) -.. code:: python +.. code-block:: python for i in range(3): play(random.choice([C5,E5,G5])) sleep(1) +Sample slicing + +.. code-block:: python + + from psonic import * + + number_of_pieces = 8 + + for i in range(16): + s = random.randrange(0,number_of_pieces)/number_of_pieces #sample starts at 0.0 and finishes at 1.0 + f = s + (1.0/number_of_pieces) + sample(LOOP_AMEN,beat_stretch=2,start=s,finish=f) + sleep(2.0/number_of_pieces) + An infinite loop and if -.. code:: python +.. code-block:: python while True: if one_in(2): @@ -188,14 +270,14 @@ An infinite loop and if --> 589 time.sleep(duration) 590 _debug('sleep', duration) 591 - + KeyboardInterrupt: If you want to hear more than one sound at a time, use Threads. -.. code:: python +.. code-block:: python import random from psonic import * @@ -222,10 +304,27 @@ If you want to hear more than one sound at a time, use Threads. while True: pass -Every function *bass\_sound* and *snare\_sound* have its own thread. -Your can hear them running. -.. code:: python +:: + + + --------------------------------------------------------------------------- + + KeyboardInterrupt Traceback (most recent call last) + + in + 22 + 23 while True: + ---> 24 pass + + + KeyboardInterrupt: + + +Every function *bass_sound* and *snare_sound* have its own thread. Your +can hear them running. + +.. code-block:: python from psonic import * from threading import Thread, Condition @@ -266,8 +365,8 @@ Your can hear them running. .. parsed-literal:: - Press Enter to continue... - + Press Enter to continue... + @@ -282,9 +381,76 @@ you can use *Condition*. One function sends a message with *condition.notifyAll* the other waits until the message comes *condition.wait*. +More simple with decorator \_\_@in_thread_\_ + +.. code-block:: python + + from psonic import * + from random import choice + + tick = Message() + + @in_thread + def random_riff(): + use_synth(PROPHET) + sc = scale(E3, MINOR) + while True: + s = random.choice([0.125,0.25,0.5]) + tick.sync() + for i in range(8): + r = random.choice([0.125, 0.25, 1, 2]) + n = random.choice(sc) + co = random.randint(30,100) + play(n, release = r, cutoff = co) + sleep(s) + + @in_thread + def drums(): + while True: + tick.cue() + for i in range(16): + r = random.randrange(1,10) + sample(DRUM_BASS_HARD, rate=r) + sleep(0.125) + + random_riff() + drums() + + input("Press Enter to continue...") + + +.. parsed-literal:: + + Press Enter to continue... + + +.. code-block:: python + + from psonic import * + + tick = Message() + + @in_thread + def metronom(): + while True: + tick.cue() + sleep(1) + + @in_thread + def instrument(): + while True: + tick.sync() + sample(DRUM_HEAVY_KICK) + + metronom() + instrument() + + while True: + pass + Play a list of notes -.. code:: python +.. code-block:: python from psonic import * @@ -295,7 +461,7 @@ Play a list of notes Play chords -.. code:: python +.. code-block:: python play(chord(E4, MINOR)) sleep(1) @@ -308,7 +474,7 @@ Play chords Play arpeggios -.. code:: python +.. code-block:: python play_pattern( chord(E4, 'm7')) play_pattern_timed( chord(E4, 'm7'), 0.25) @@ -316,20 +482,52 @@ Play arpeggios Play scales -.. code:: python +.. code-block:: python play_pattern_timed(scale(C3, MAJOR), 0.125, release = 0.1) play_pattern_timed(scale(C3, MAJOR, num_octaves = 2), 0.125, release = 0.1) play_pattern_timed(scale(C3, MAJOR_PENTATONIC, num_octaves = 2), 0.125, release = 0.1) +The function *scale* returns a list with all notes of a scale. So you +can use list methodes or functions. For example to play arpeggios +descending or shuffeld. + +.. code-block:: python + + import random + from psonic import * + + s = scale(C3, MAJOR) + s + + + + +.. parsed-literal:: + + [48, 50, 52, 53, 55, 57, 59, 60] + + + +.. code-block:: python + + s.reverse() + +.. code-block:: python + + + play_pattern_timed(s, 0.125, release = 0.1) + random.shuffle(s) + play_pattern_timed(s, 0.125, release = 0.1) + Live Loop ~~~~~~~~~ One of the best in SONIC PI is the *Live Loop*. While a loop is playing -music you can change it and hear the change. Let's try it in Python, +music you can change it and hear the change. Let’s try it in Python, too. -.. code:: python +.. code-block:: python from psonic import * from threading import Thread @@ -352,7 +550,7 @@ too. .. parsed-literal:: Press Enter to continue...Y - + @@ -362,23 +560,23 @@ too. -Now change the function *my\_loop* und you can hear it. +Now change the function *my_loop* und you can hear it. -.. code:: python +.. code-block:: python def my_loop(): use_synth(TB303) play (60, release= 0.3) sleep (0.25) -.. code:: python +.. code-block:: python def my_loop(): use_synth(TB303) play (chord(E3, MINOR), release= 0.3) sleep(0.5) -.. code:: python +.. code-block:: python def my_loop(): use_synth(TB303) @@ -386,12 +584,12 @@ Now change the function *my\_loop* und you can hear it. play(random.choice(chord(E3, MINOR)), release= 0.2, cutoff=random.randrange(60, 130)) sleep(0.25) -To stop the sound you have to end the kernel. In IPython with Kernel --> +To stop the sound you have to end the kernel. In IPython with Kernel –> Restart Now with two live loops which are synch. -.. code:: python +.. code-block:: python from psonic import * from threading import Thread, Condition @@ -432,7 +630,7 @@ Now with two live loops which are synch. .. parsed-literal:: Press Enter to continue...y - + @@ -442,13 +640,13 @@ Now with two live loops which are synch. -.. code:: python +.. code-block:: python def loop_foo(): play (A4, release = 0.5) sleep (0.5) -.. code:: python +.. code-block:: python def loop_bar(): sample (DRUM_HEAVY_KICK) @@ -457,7 +655,7 @@ Now with two live loops which are synch. If would be nice if we can stop the loop with a simple command. With stop event it works. -.. code:: python +.. code-block:: python from psonic import * from threading import Thread, Condition, Event @@ -501,7 +699,7 @@ stop event it works. .. parsed-literal:: Press Enter to continue...y - + @@ -511,13 +709,13 @@ stop event it works. -.. code:: python +.. code-block:: python stop_event.set() More complex live loops -.. code:: python +.. code-block:: python sc = Ring(scale(E3, MINOR_PENTATONIC)) @@ -534,7 +732,7 @@ More complex live loops Now a simple structure with four live loops -.. code:: python +.. code-block:: python import random from psonic import * @@ -594,7 +792,7 @@ Now a simple structure with four live loops .. parsed-literal:: Press Enter to continue...y - + @@ -606,28 +804,28 @@ Now a simple structure with four live loops After starting the loops you can change them -.. code:: python +.. code-block:: python def live_1(): sample(BD_HAUS,amp=2) sleep(0.5) pass -.. code:: python +.. code-block:: python def live_2(): #sample(AMBI_CHOIR, rate=0.4) #sleep(1) pass -.. code:: python +.. code-block:: python def live_3(): use_synth(TB303) play(E2, release=4,cutoff=120,cutoff_attack=1) sleep(4) -.. code:: python +.. code-block:: python def live_4(): notes = scale(E3, MINOR_PENTATONIC, num_octaves=2) @@ -637,18 +835,136 @@ After starting the loops you can change them And stop. -.. code:: python +.. code-block:: python stop_event.set() +Creating Sound +~~~~~~~~~~~~~~ + +.. code-block:: python + + from psonic import * + + synth(SINE, note=D4) + synth(SQUARE, note=D4) + synth(TRI, note=D4, amp=0.4) + +.. code-block:: python + + detune = 0.7 + synth(SQUARE, note = E4) + synth(SQUARE, note = E4+detune) + +.. code-block:: python + + detune=0.1 # Amplitude shaping + synth(SQUARE, note = E2, release = 2) + synth(SQUARE, note = E2+detune, amp = 2, release = 2) + synth(GNOISE, release = 2, amp = 1, cutoff = 60) + synth(GNOISE, release = 0.5, amp = 1, cutoff = 100) + synth(NOISE, release = 0.2, amp = 1, cutoff = 90) + +Next Step +~~~~~~~~~ + +Using FX *Not implemented yet* + +.. code-block:: python + + from psonic import * + + with Fx(SLICER): + synth(PROPHET,note=E2,release=8,cutoff=80) + synth(PROPHET,note=E2+4,release=8,cutoff=80) + +.. code-block:: python + + with Fx(SLICER, phase=0.125, probability=0.6,prob_pos=1): + synth(TB303, note=E2, cutoff_attack=8, release=8) + synth(TB303, note=E3, cutoff_attack=4, release=8) + synth(TB303, note=E4, cutoff_attack=2, release=8) + +OSC Communication (Sonic Pi Ver. 3.x or better) +----------------------------------------------- + +In Sonic Pi version 3 or better you can work with messages. + +.. code-block:: python + + from psonic import * + +First you need a programm in the Sonic Pi server that receives messages. +You can write it in th GUI or send one with Python. + +.. code-block:: python + + run("""live_loop :foo do + use_real_time + a, b, c = sync "/osc*/trigger/prophet" + synth :prophet, note: a, cutoff: b, sustain: c + end """) + +Now send a message to Sonic Pi. + +.. code-block:: python + + send_message('/trigger/prophet', 70, 100, 8) + +.. code-block:: python + + stop() + +Recording +--------- + +With python-sonic you can record wave files. + +.. code-block:: python + + from psonic import * + +.. code-block:: python + + # start recording + start_recording() + + play(chord(E4, MINOR)) + sleep(1) + play(chord(E4, MAJOR)) + sleep(1) + play(chord(E4, MINOR7)) + sleep(1) + play(chord(E4, DOM7)) + sleep(1) + +.. code-block:: python + + # stop recording + stop_recording + + + + +.. parsed-literal:: + + + + + +.. code-block:: python + + # save file + save_recording('/Volumes/jupyter/python-sonic/test.wav') + More Examples ------------- -.. code:: python +.. code-block:: python from psonic import * -.. code:: python +.. code-block:: python #Inspired by Steve Reich Clapping Music @@ -661,23 +977,11 @@ More Examples if clapping[(i+k)%12] == 1: sample(DRUM_HEAVY_KICK,pan=0.5) sleep (0.25) -More Informations ------------------ - -Sonic Pi -~~~~~~~~ - -.. +Projects that use Python-Sonic +------------------------------ -OSC -~~~ - -.. - -MIDI -~~~~ - -.. +Raspberry Pi sonic-track.py a Sonic-pi Motion Track Demo +https://github.com/pageauc/sonic-track Sources ------- diff --git a/psonic.py b/psonic.py deleted file mode 100644 index 5930b36..0000000 --- a/psonic.py +++ /dev/null @@ -1,716 +0,0 @@ -#The MIT License (MIT) -# -#Copyright (c) 2016 G. Völkl -# -#Permission is hereby granted, free of charge, to any person obtaining a copy -#of this software and associated documentation files (the "Software"), to deal -#in the Software without restriction, including without limitation the rights -#to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -#copies of the Software, and to permit persons to whom the Software is -#furnished to do so, subject to the following conditions: -# -#The above copyright notice and this permission notice shall be included in all -#copies or substantial portions of the Software. -# -#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -#IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -#FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -#AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -#LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -#OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -#SOFTWARE. -import random -import time - -from pythonosc import osc_message_builder # osc support -from pythonosc import udp_client - -__debug = False - - -## Base Classes ## - -class Synth: - """ - Synthesizer - """ - def __init__(self, name): - self.name = name - - -class Sample: - """ - Sample - """ - def __init__(self, name): - self.name = name - - -class ChordQuality: - """ - Chord Quality - """ - def __init__(self, name, inter): - self.name = name - self.inter = inter - - -## Notes ## -C2 = 36 -Cs2 = 37 -Db2 = Cs2 -D2 = 38 -Ds2 = 39 -Eb2 = Ds2 -E2 = 40 -F2 = 41 -Fs2 = 42 -Gb2 = Fs2 -G2 = 43 -Gs2 = 44 -Ab2 = Gs2 -A2 = 45 -As2 = 46 -Bb2 = As2 -B3 = 47 -C3 = 48 -Cs3 = 49 -Db3 = Cs3 -D3 = 50 -Ds3 = 51 -Eb3 = Ds3 -E3 = 52 -F3 = 53 -Fs3 = 54 -Gb3 = Fs3 -G3 = 55 -Gs3 = 56 -Ab3 = Gs3 -A3 = 57 -As3 = 58 -Bb3 = As3 -B3 = 59 -C4 = 60 -Cs4 = 61 -Db4 = Cs4 -D4 = 62 -Ds4 = 63 -Eb4 = Ds4 -E4 = 64 -F4 = 65 -Fs4 = 66 -Gb4 = Fs4 -G4 = 67 -Gs4 = 68 -Ab4 = Gs4 -A4 = 69 -As4 = 70 -Bb4 = As4 -B4 = 71 -C5 = 72 -Cs5 = 73 -Db5 = Cs5 -D5 = 74 -Ds5 = 75 -Eb5 = Ds5 -E5 = 76 -F5 = 77 -Fs5 = 78 -Gb5 = Fs5 -G5 = 79 -Gs5 = 80 -Ab5 = Gs5 -A5 = 81 -As5 = 82 -Bb5 = As5 -B5 = 83 -C6 = 84 - -R = 0 - -## Synthezier ## -DULL_BELL = Synth('dull_bell') -PRETTY_BELL = Synth('pretty_bell') -SQUARE = Synth('square') -PULSE = Synth('pulse') -SUBPULSE = Synth('subpulse') -DTRI = Synth('dtri') -DPULSE = Synth('dpulse') -FM = Synth('fm') -MOD_FM = Synth('mod_fm') -MOD_SAW = Synth('mod_saw') -MOD_DSAW = Synth('mod_dsaw') -MOD_SINE = Synth('mod_sine') -MOD_TRI = Synth('mod_tri') -MOD_PULSE = Synth('mod_pulse') -SUPERSAW = Synth('supersaw') -HOOVER = Synth('hoover') -SYNTH_VIOLIN = Synth('synth_violin') -PLUCK = Synth('pluck') -PIANO = Synth('piano') -GROWL = Synth('growl') -DARK_AMBIENCE = Synth('dark_ambience') -DARK_SEA_HORN = Synth('dark_sea_horn') -HOLLOW = Synth('hollow') -ZAWA = Synth('zawa') -NOISE = Synth('noise') -GNOISE = Synth('gnoise') -BNOISE = Synth('bnoise') -CNOISE = Synth('cnoise') -DSAW = Synth('dsaw') -TB303 = Synth('tb303') -BLADE = Synth('blade') -PROPHET = Synth('prophet') -SAW = Synth('saw') -BEEP = Synth('beep') -TRI = Synth('tri') - -## Scale Mode (from sonic pi)## -DIATONIC = 'diatonic' -IONIAN = 'ionian' -MAJOR = 'major' -DORIAN = 'dorian' -PHRYGIAN = 'phrygian' -LYDIAN = 'lydian' -MIXOLYDIAN = 'mixolydian' -AEOLIAN = 'aeolian' -MINOR = 'minor' -LOCRIAN = 'locrian' -HEX_MAJOR6 = 'hex_major6' -HEX_DORIAN = 'hex_dorian' -HEX_PHRYGIAN = 'hex_phrygian' -HEX_MAJOR7 = 'hex_major7' -HEX_SUS = 'hex_sus' -HEX_AEOLIAN = 'hex_aeolian' -MINOR_PENTATONIC = 'minor_pentatonic' -YU = 'yu' -MAJOR_PENTATONIC = 'major_pentatonic' -GONG = 'gong' -EGYPTIAN = 'egyptian' -SHANG = 'shang' -JIAO = 'jiao' -ZHI = 'zhi' -RITUSEN = 'ritusen' -WHOLE_TONE = 'whole_tone' -WHOLE = 'whole' -CHROMATIC = 'chromatic' -HARMONIC_MINOR = 'harmonic_minor' -MELODIC_MINOR_ASC = 'melodic_minor_asc' -HUNGARIAN_MINOR = 'hungarian_minor' -OCTATONIC = 'octatonic' -MESSIAEN1 = 'messiaen1' -MESSIAEN2 = 'messiaen2' -MESSIAEN3 = 'messiaen3' -MESSIAEN4 = 'messiaen4' -MESSIAEN5 = 'messiaen5' -MESSIAEN6 = 'messiaen6' -MESSIAEN7 = 'messiaen7' -SUPER_LOCRIAN = 'super_locrian' -HIRAJOSHI = 'hirajoshi' -KUMOI = 'kumoi' -NEAPLOLITAN_MAJOR = 'neapolitan_major' -BARTOK = 'bartok' -BHAIRAV = 'bhairav' -LOCRIAN_MAJOR = 'locrian_major' -AHIRBHAIRAV = 'ahirbhairav' -ENIGMATIC = 'enigmatic' -NEAPLOLITAN_MINOR = 'neapolitan_minor' -PELOG = 'pelog' -AUGMENTED2 = 'augmented2' -SCRIABIN = 'scriabin' -HARMONIC_MAJOR = 'harmonic_major' -MELODIC_MINOR_DESC = 'melodic_minor_desc' -ROMANIAN_MINOR = 'romanian_minor' -HINDU = 'hindu' -IWATO = 'iwato' -MELODIC_MINOR = 'melodic_minor' -DIMISHED2 = 'diminished2' -MARVA = 'marva' -MELODIC_MAJOR = 'melodic_major' -INDIAN = 'indian' -SPANISH = 'spanish' -PROMETHEUS = 'prometheus' -DIMISHED = 'diminished' -TODI = 'todi' -LEADING_WHOLE = 'leading_whole' -AUGMENTED = 'augmented' -PRUVI = 'purvi' -CHINESE = 'chinese' -LYDIAN_MINOR = 'lydian_minor' -I = 'i' -II = 'ii' -III = 'iii' -IV = 'iv' -V = 'v' -VI = 'vi' -VII = 'vii' -VIII = 'viii' -_ionian_sequence = [2, 2, 1, 2, 2, 2, 1] -_hex_sequence = [2, 2, 1, 2, 2, 3] -_pentatonic_sequence = [3, 2, 2, 3, 2] - -_SCALE_MODE = { - 'diatonic': _ionian_sequence, - 'ionian': _ionian_sequence, - 'major': _ionian_sequence, - 'dorian': _ionian_sequence[1:]+_ionian_sequence[:1], #rotate 1 - 'phrygian': _ionian_sequence[2:]+_ionian_sequence[:2], #rotate(2) - 'lydian': _ionian_sequence[3:]+_ionian_sequence[:3], #rotate(3) - 'mixolydian': _ionian_sequence[4:]+_ionian_sequence[:4], #rotate(4) - 'aeolian': _ionian_sequence[5:]+_ionian_sequence[:5], #rotate(5) - 'minor': _ionian_sequence[5:]+_ionian_sequence[:5], #rotate(5) - 'locrian': _ionian_sequence[6:]+_ionian_sequence[:6], #rotate(6) - 'hex_major6': _hex_sequence, - 'hex_dorian': _hex_sequence[1:]+_hex_sequence[:1], #rotate(1) - 'hex_phrygian': _hex_sequence[2:]+_hex_sequence[:2], #rotate(2) - 'hex_major7': _hex_sequence[3:]+_hex_sequence[:3], #rotate(3) - 'hex_sus': _hex_sequence[4:]+_hex_sequence[:4], #rotate(4) - 'hex_aeolian': _hex_sequence[5:]+_hex_sequence[:5], #rotate(5) - 'minor_pentatonic': _pentatonic_sequence, - 'yu': _pentatonic_sequence, - 'major_pentatonic': _pentatonic_sequence[1:]+_pentatonic_sequence[:1], #rotate(1) - 'gong': _pentatonic_sequence[1:]+_pentatonic_sequence[:1], #rotate(1) - 'egyptian': _pentatonic_sequence[2:]+_pentatonic_sequence[:2], #rotate(2) - 'shang': _pentatonic_sequence[2:]+_pentatonic_sequence[:2], #rotate(2) - 'jiao': _pentatonic_sequence[3:]+_pentatonic_sequence[:3], #rotate(3) - 'zhi': _pentatonic_sequence[4:]+_pentatonic_sequence[:4], #rotate(4) - 'ritusen': _pentatonic_sequence[4:]+_pentatonic_sequence[:4], #rotate(4) - 'whole_tone': [2, 2, 2, 2, 2, 2], - 'whole': [2, 2, 2, 2, 2, 2], - 'chromatic': [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], - 'harmonic_minor': [2, 1, 2, 2, 1, 3, 1], - 'melodic_minor_asc': [2, 1, 2, 2, 2, 2, 1], - 'hungarian_minor': [2, 1, 3, 1, 1, 3, 1], - 'octatonic': [2, 1, 2, 1, 2, 1, 2, 1], - 'messiaen1': [2, 2, 2, 2, 2, 2], - 'messiaen2': [1, 2, 1, 2, 1, 2, 1, 2], - 'messiaen3': [2, 1, 1, 2, 1, 1, 2, 1, 1], - 'messiaen4': [1, 1, 3, 1, 1, 1, 3, 1], - 'messiaen5': [1, 4, 1, 1, 4, 1], - 'messiaen6': [2, 2, 1, 1, 2, 2, 1, 1], - 'messiaen7': [1, 1, 1, 2, 1, 1, 1, 1, 2, 1], - 'super_locrian': [1, 2, 1, 2, 2, 2, 2], - 'hirajoshi': [2, 1, 4, 1, 4], - 'kumoi': [2, 1, 4, 2, 3], - 'neapolitan_major': [1, 2, 2, 2, 2, 2, 1], - 'bartok': [2, 2, 1, 2, 1, 2, 2], - 'bhairav': [1, 3, 1, 2, 1, 3, 1], - 'locrian_major': [2, 2, 1, 1, 2, 2, 2], - 'ahirbhairav': [1, 3, 1, 2, 2, 1, 2], - 'enigmatic': [1, 3, 2, 2, 2, 1, 1], - 'neapolitan_minor': [1, 2, 2, 2, 1, 3, 1], - 'pelog': [1, 2, 4, 1, 4], - 'augmented2': [1, 3, 1, 3, 1, 3], - 'scriabin': [1, 3, 3, 2, 3], - 'harmonic_major': [2, 2, 1, 2, 1, 3, 1], - 'melodic_minor_desc': [2, 1, 2, 2, 1, 2, 2], - 'romanian_minor': [2, 1, 3, 1, 2, 1, 2], - 'hindu': [2, 2, 1, 2, 1, 2, 2], - 'iwato': [1, 4, 1, 4, 2], - 'melodic_minor': [2, 1, 2, 2, 2, 2, 1], - 'diminished2': [2, 1, 2, 1, 2, 1, 2, 1], - 'marva': [1, 3, 2, 1, 2, 2, 1], - 'melodic_major': [2, 2, 1, 2, 1, 2, 2], - 'indian': [4, 1, 2, 3, 2], - 'spanish': [1, 3, 1, 2, 1, 2, 2], - 'prometheus': [2, 2, 2, 5, 1], - 'diminished': [1, 2, 1, 2, 1, 2, 1, 2], - 'todi': [1, 2, 3, 1, 1, 3, 1], - 'leading_whole': [2, 2, 2, 2, 2, 1, 1], - 'augmented': [3, 1, 3, 1, 3, 1], - 'purvi': [1, 3, 2, 1, 1, 3, 1], - 'chinese': [4, 2, 1, 4, 1], - 'lydian_minor': [2, 2, 2, 1, 1, 2, 2], - 'i': _ionian_sequence, - 'ii': _ionian_sequence[1:]+_ionian_sequence[:1], #rotate(1) - 'iii': _ionian_sequence[2:]+_ionian_sequence[:2], #rotate(2) - 'iv': _ionian_sequence[3:]+_ionian_sequence[:3], #rotate(3) - 'v': _ionian_sequence[4:]+_ionian_sequence[:4], #rotate(4) - 'vi': _ionian_sequence[5:]+_ionian_sequence[:5], #rotate(5) - 'vii': _ionian_sequence[6:]+_ionian_sequence[:6], #rotate(6), - 'viii': _ionian_sequence[7:]+_ionian_sequence[:7]} #rotate(7) - -## Chord Quality (from sonic pi) ## -MAJOR7 = "major7" -DOM7 = "dom7" -MINOR7 = "minor7" -AUG = "aug" -DIM = "dim" -DIM7 = "dim7" - -_CHORD_QUALITY = { - 'major': [0, 4, 7], - 'minor': [0, 3, 7], - 'major7': [0, 4, 7, 11], - 'dom7': [0, 4, 7, 10], - 'minor7': [0, 3, 7, 10], - 'aug': [0, 4, 8], - 'dim': [0, 3, 6], - 'dim7': [0, 3, 6, 9], - '1': [0], - "5": [0, 7], - "+5": [0, 4, 8], - "m+5": [0, 3, 8], - "sus2": [0, 2, 7], - "sus4": [0, 5, 7], - "6": [0, 4, 7, 9], - "m6": [0, 3, 7, 9], - "7sus2": [0, 2, 7, 10], - "7sus4": [0, 5, 7, 10], - "7-5": [0, 4, 6, 10], - "m7-5": [0, 3, 6, 10], - "7+5": [0, 4, 8, 10], - "m7+5": [0, 3, 8, 10], - "9": [0, 4, 7, 10, 14], - "m9": [0, 3, 7, 10, 14], - "m7+9": [0, 3, 7, 10, 14], - "maj9": [0, 4, 7, 11, 14], - "9sus4": [0, 5, 7, 10, 14], - "6*9": [0, 4, 7, 9, 14], - "m6*9": [0, 3, 9, 7, 14], - "7-9": [0, 4, 7, 10, 13], - "m7-9": [0, 3, 7, 10, 13], - "7-10": [0, 4, 7, 10, 15], - "9+5": [0, 10, 13], - "m9+5": [0, 10, 14], - "7+5-9": [0, 4, 8, 10, 13], - "m7+5-9": [0, 3, 8, 10, 13], - "11": [0, 4, 7, 10, 14, 17], - "m11": [0, 3, 7, 10, 14, 17], - "maj11": [0, 4, 7, 11, 14, 17], - "11+": [0, 4, 7, 10, 14, 18], - "m11+": [0, 3, 7, 10, 14, 18], - "13": [0, 4, 7, 10, 14, 17, 21], - "m13": [0, 3, 7, 10, 14, 17, 21], - "M": [0, 4, 7], - "m": [0, 3, 7], - "7": [0, 4, 7, 10], - "M7": [0, 4, 7, 11], - "m7": [0, 3, 7], - "augmented": [0, 4, 8], - "a": [0, 4, 8], - "diminished": [0, 3, 6], - "i": [0, 3, 6], - "diminished7": [0, 3, 6, 9], - "i7": [0, 3, 6, 9]} - -## Sample ## - -## Drum Sounds -DRUM_HEAVY_KICK = Sample('drum_heavy_kick') -DRUM_TOM_MID_SOFT = Sample('drum_tom_mid_soft') -DRUM_TOM_MID_HARD = Sample('drum_tom_mid_hard') -DRUM_TOM_LO_SOFT = Sample('drum_tom_lo_soft') -DRUM_TOM_LO_HARD = Sample('drum_tom_lo_hard') -DRUM_TOM_HI_SOFT = Sample('drum_tom_hi_soft') -DRUM_TOM_HI_HARD = Sample('drum_tom_hi_hard') -DRUM_SPLASH_SOFT = Sample('drum_splash_soft') -DRUM_SPLASH_HARD = Sample('drum_splash_hard') -DRUM_SNARE_SOFT = Sample('drum_snare_soft') -DRUM_SNARE_HARD = Sample('drum_snare_hard') -DRUM_CYMBAL_SOFT = Sample('drum_cymbal_soft') -DRUM_CYMBAL_HARD = Sample('drum_cymbal_hard') -DRUM_CYMBAL_OPEN = Sample('drum_cymbal_open') -DRUM_CYMBAL_CLOSED = Sample('drum_cymbal_closed') -DRUM_CYMBAL_PEDAL = Sample('drum_cymbal_pedal') -DRUM_BASS_SOFT = Sample('drum_bass_soft') -DRUM_BASS_HARD = Sample('drum_bass_hard') - -## Electric Sounds -ELEC_TRIANGLE = Sample('elec_triangle') -ELEC_SNARE = Sample('elec_snare') -ELEC_LO_SNARE = Sample('elec_lo_snare') -ELEC_HI_SNARE = Sample('elec_hi_snare') -ELEC_MID_SNARE = Sample('elec_mid_snare') -ELEC_CYMBAL = Sample('elec_cymbal') -ELEC_SOFT_KICK = Sample('elec_soft_kick') -ELEC_FILT_SNARE = Sample('elec_filt_snare') -ELEC_FUZZ_TOM = Sample('elec_fuzz_tom') -ELEC_CHIME = Sample('elec_chime') -ELEC_BONG = Sample('elec_bong') -ELEC_TWANG = Sample('elec_twang') -ELEC_WOOD = Sample('elec_wood') -ELEC_POP = Sample('elec_pop') -ELEC_BEEP = Sample('elec_beep') -ELEC_BLIP = Sample('elec_blip') -ELEC_BLIP2 = Sample('elec_blip2') -ELEC_PING = Sample('elec_ping') -ELEC_BELL = Sample('elec_bell') -ELEC_FLIP = Sample('elec_flip') -ELEC_TICK = Sample('elec_tick') -ELEC_HOLLOW_KICK = Sample('elec_hollow_kick') -ELEC_TWIP = Sample('elec_twip') -ELEC_PLIP = Sample('elec_plip') -ELEC_BLUP = Sample('elec_blup') - -## Sounds featuring guitars -GUIT_HARMONICS = Sample('guit_harmonics') -GUIT_E_FIFTHS = Sample('guit_e_fifths') -GUIT_E_SLIDE = Sample('guit_e_slide') - -## Miscellaneous Sounds -MISC_BURP = Sample('misc_burp') - -## Percurssive Sounds -PERC_BELL = Sample('perc_bell') - -## Ambient Sounds -AMBI_SOFT_BUZZ = Sample('ambi_soft_buzz') -AMBI_SWOOSH = Sample('ambi_swoosh') -AMBI_DRONE = Sample('ambi_drone') -AMBI_GLASS_HUM = Sample('ambi_glass_hum') -AMBI_GLASS_RUB = Sample('ambi_glass_rub') -AMBI_HAUNTED_HUM = Sample('ambi_haunted_hum') -AMBI_PIANO = Sample('ambi_piano') -AMBI_LUNAR_LAND = Sample('ambi_lunar_land') -AMBI_DARK_WOOSH = Sample('ambi_dark_woosh') -AMBI_CHOIR = Sample('ambi_choir') - -## Bass Sounds -BASS_HIT_C = Sample('bass_hit_c') -BASS_HARD_C = Sample('bass_hard_c') -BASS_THICK_C = Sample('bass_thick_c') -BASS_DROP_C = Sample('bass_drop_c') -BASS_WOODSY_C = Sample('bass_woodsy_c') -BASS_VOXY_C = Sample('bass_voxy_c') -BASS_VOXY_HIT_C = Sample('bass_voxy_hit_c') -BASS_DNB_F = Sample('bass_dnb_f') - -BD_808 = Sample('bd_808') -BD_ADA = Sample('bd_ada') -BD_BOOM = Sample('bd_boom') -BD_FAT = Sample('bd_fat') -BD_GAS = Sample('bd_gas') -BD_HAUS = Sample('bd_haus') -BD_KLUB = Sample('bd_klub') -BD_PURE = Sample('bd_pure') -BD_SONE = Sample('bd_sone') -BD_TEK = Sample('bd_tek') -BD_ZOME = Sample('bd_zome') -BD_ZUM = Sample('bd_zum') - -## Sounds for Looping -LOOP_INDUSTRIAL = Sample('loop_industrial') -LOOP_COMPUS = Sample('loop_compus') -LOOP_AMEN = Sample('loop_amen') -LOOP_AMEN_FULL = Sample('loop_amen_full') - -## Module attributes ## -_current_synth = BEEP - - -## Module methodes ## -def use_synth(synth): - global _current_synth - _current_synth = synth - - -def play(note, attack=None, decay=None, sustain_level=None, sustain=None, release=None, cutoff=None, - cutoff_attack=None, amp=None, pan=None): - parameters = [] - parameter = '' - - if attack is not None: parameters.append('attack: {0}'.format(attack)) - if cutoff_attack is not None: parameters.append('cutoff_attack: {0}'.format(cutoff_attack)) - if decay is not None: parameters.append('decay: {0}'.format(decay)) - if sustain_level is not None: parameters.append('sustain_level: {0}'.format(sustain_level)) - if sustain is not None: parameters.append('sustain: {0}'.format(sustain)) - if release is not None: parameters.append('release: {0}'.format(release)) - if cutoff is not None: parameters.append('cutoff: {0}'.format(cutoff)) - - if amp is not None: parameters.append('amp: {0}'.format(amp)) - if pan is not None: parameters.append('pan: {0}'.format(pan)) - - if len(parameters) > 0: parameter = ',' + ','.join(parameters) - - command = 'play {0}{1}'.format(note, parameter) - - command = 'use_synth :{0}\n'.format(_current_synth.name) + command - _debug('play command={}'.format(command)) - synthServer.run(command) - - -def play_pattern_timed(notes, times, release=None): - """ - play notes - :param notes: - :param times: - :return: - """ - if not type(notes) is list: notes = [notes] - if not type(times) is list: times = [times] - - for t in times: - for i in notes: - play(i,release=release) - sleep(t) - - -def play_pattern(notes): - """ - - :param notes: - :return: - """ - play_pattern_timed(notes, 1) - - -def sample(sample, rate=None, attack=None, sustain=None, release=None, start=None, finish=None, amp=None, pan=None): - parameters = [] - parameter = '' - command = '' - - if rate is not None: parameters.append('rate: {0}'.format(rate)) - if attack is not None: parameters.append('attack: {0}'.format(attack)) - if sustain is not None: parameters.append('sustain: {0}'.format(sustain)) - if release is not None: parameters.append('release: {0}'.format(release)) - if start is not None: parameters.append('start: {0}'.format(start)) - if finish is not None: parameters.append('finish: {0}'.format(finish)) - if amp is not None: parameters.append('amp: {0}'.format(amp)) - if pan is not None: parameters.append('pan: {0}'.format(pan)) - - if len(parameters) > 0: parameter = ',' + ','.join(parameters) - - if type(sample) == Sample: - command = 'sample :{0}{1}'.format(sample.name, parameter) - else: - command = 'sample "{0}"{1}'.format(sample, parameter) - - _debug('sample command={}'.format(command)) - synthServer.run(command) - - -def sleep(duration): - """ - the same as time.sleep - :param duration: - :return: - """ - time.sleep(duration) - _debug('sleep', duration) - - -def one_in(max): - """ - random function returns True in one of max cases - :param max: - :return: boolean - """ - return random.randint(1, max) == 1 - - -def chord(root_note, chord_quality): - """ - Generates a list of notes of a chord - - :param root_note: - :param chord_quality: - :return: list - """ - result = [] - n = root_note - - half_tone_steps = _CHORD_QUALITY[chord_quality] - - for i in half_tone_steps: - n = n + i - result.append(n) - - return result - - -def scale(root_note, scale_mode, num_octaves=1): - """ - Genarates a liste of notes of scale - - :param root_note: - :param scale_mode: - :param num_octaves: - :return: list - """ - result = [] - n = root_note - - half_tone_steps = _SCALE_MODE[scale_mode] - - for o in range(num_octaves): - n = root_note + o * 12 - result.append(n) - for i in half_tone_steps: - n = n + i - result.append(n) - - return result - -## Compound classes ## - -class Ring: - """ - ring buffer - """ - - def __init__(self, data): - self.data = data - self.index = -1 - - def __iter__(self): # return Iterator - return self - - def __next__(self): # return Iterator next element - self.index += 1 - if self.index == len(self.data): - self.index = 0 - return self.data[self.index] - - def choose(self): # random choose - return random.choice(self.data) - - -## Connection classes ## - -class SonicPi(): - """ - Communiction to Sonic Pi - """ - - UDP_IP = "127.0.0.1" - UDP_PORT = 4557 - GUI_ID = 'SONIC_PI_PYTHON' - - RUN_COMMAND = "/run-code" - STOP_COMMAND = "/stop-all-jobs" - - def __init__(self): - self.client = udp_client.UDPClient(SonicPi.UDP_IP, SonicPi.UDP_PORT) - - def run(self, command): - self.send_command(SonicPi.RUN_COMMAND, command) - - def stop(self): - self.send_command(SonicPi.STOP_COMMAND) - - def test_connection(self): - # OSC::Server.new(PORT) - # abort("ERROR: Sonic Pi is not listening on #{PORT} - is it running?") - pass - - def send_command(self, address, argument=''): - msg = osc_message_builder.OscMessageBuilder(address=address) - msg.add_arg('SONIC_PI_PYTHON') - msg.add_arg(argument) - msg = msg.build() - - self.client.send(msg) - - -synthServer = SonicPi() - - -## system functions ## - -def _debug(*allargs): # simple debug function for working in different environments - if __debug: print(allargs) - -if __name__ == '__main__': - use_synth(SAW) - play(C5, amp=2, pan=-1) diff --git a/psonic/__init__.py b/psonic/__init__.py new file mode 100644 index 0000000..0a0e47b --- /dev/null +++ b/psonic/__init__.py @@ -0,0 +1 @@ +from .api import * diff --git a/psonic/api.py b/psonic/api.py new file mode 100644 index 0000000..910ad00 --- /dev/null +++ b/psonic/api.py @@ -0,0 +1,84 @@ +import random +import threading +from .synth_server import ( + SonicPi, + SonicPiNew, + use_synth, +) +from .psonic import * +from .notes import * +from .scales import * +from .synthesizers import * +from .effects import * +from .samples import * +from .samples.loops import * +from .samples.ambient import * +from .samples.bass import * +from .samples.drums import * +from .samples.electric import * +from .samples.guitars import * +from .samples.misc import * +from .samples.percussions import * +from .samples.tabla import * +from .samples.vinyl import * +from .internals.chords import * +from .internals.scales import * + + +def in_thread(func): + """Thread decorator""" + + def wrapper(): + _thread = threading.Thread(target=func) + _thread.start() + + return wrapper + +class Fx: + """FX Effects""" + + def __init__(self, mode, phase=0.24, probability=0, prob_pos=0): + self.mode = mode + self.phase = phase + self.probability = probability + self.prob_pos = prob_pos + + def __enter__(self): + pass + + def __exit__(self, exc_type, exc_val, exc_tb): + pass + +class Ring: + """ring buffer""" + + def __init__(self, data): + self.data = data + self.index = -1 + + def __iter__(self): # return Iterator + return self + + def __next__(self): # return Iterator next element + self.index += 1 + if self.index == len(self.data): + self.index = 0 + return self.data[self.index] + + def choose(self): # random choose + return random.choice(self.data) + +# Not used anywhere +class Message: + """For sending messages between threads""" + + def __init__(self): + self._condition = threading.Condition() + + def cue(self): + with self._condition: + self._condition.notifyAll() # Message to threads + + def sync(self): + with self._condition: + self._condition.wait() # Wait for message diff --git a/psonic/effects.py b/psonic/effects.py new file mode 100644 index 0000000..58fc901 --- /dev/null +++ b/psonic/effects.py @@ -0,0 +1,20 @@ +"""Effects""" + +class FxName: + """FX name""" + + def __init__(self, name): + self.name = name + + +BITCRUSHER = FxName('bitcrusher') +COMPRESSOR = FxName('compressor') +ECHO = FxName('echo') +FLANGER = FxName('flanger') +KRUSH = FxName('krush') +LPF = FxName('lpf') +PAN = FxName('pan') +PANSLICER = FxName('panslicer') +REVERB = FxName('reverb') +SLICER = FxName('slicer') +WOBBLE = FxName('wobble') diff --git a/psonic/internals/__init__.py b/psonic/internals/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/psonic/internals/chords.py b/psonic/internals/chords.py new file mode 100644 index 0000000..d505969 --- /dev/null +++ b/psonic/internals/chords.py @@ -0,0 +1,74 @@ + +# Not used anywhere +class ChordQuality: + """Chord Quality""" + + def __init__(self, name, inter): + self.name = name + self.inter = inter + +_CHORD_QUALITY = { + 'major': [0, 4, 7], + 'minor': [0, 3, 7], + 'major7': [0, 4, 7, 11], + 'dom7': [0, 4, 7, 10], + 'minor7': [0, 3, 7, 10], + 'aug': [0, 4, 8], + 'dim': [0, 3, 6], + 'dim7': [0, 3, 6, 9], + '1': [0], + "5": [0, 7], + "+5": [0, 4, 8], + "m+5": [0, 3, 8], + "sus2": [0, 2, 7], + "sus4": [0, 5, 7], + "6": [0, 4, 7, 9], + "m6": [0, 3, 7, 9], + "7sus2": [0, 2, 7, 10], + "7sus4": [0, 5, 7, 10], + "7-5": [0, 4, 6, 10], + "m7-5": [0, 3, 6, 10], + "7+5": [0, 4, 8, 10], + "m7+5": [0, 3, 8, 10], + "9": [0, 4, 7, 10, 14], + "m9": [0, 3, 7, 10, 14], + "m7+9": [0, 3, 7, 10, 14], + "maj9": [0, 4, 7, 11, 14], + "9sus4": [0, 5, 7, 10, 14], + "6*9": [0, 4, 7, 9, 14], + "m6*9": [0, 3, 9, 7, 14], + "7-9": [0, 4, 7, 10, 13], + "m7-9": [0, 3, 7, 10, 13], + "7-10": [0, 4, 7, 10, 15], + "9+5": [0, 10, 13], + "m9+5": [0, 10, 14], + "7+5-9": [0, 4, 8, 10, 13], + "m7+5-9": [0, 3, 8, 10, 13], + "11": [0, 4, 7, 10, 14, 17], + "m11": [0, 3, 7, 10, 14, 17], + "maj11": [0, 4, 7, 11, 14, 17], + "11+": [0, 4, 7, 10, 14, 18], + "m11+": [0, 3, 7, 10, 14, 18], + "13": [0, 4, 7, 10, 14, 17, 21], + "m13": [0, 3, 7, 10, 14, 17, 21], + "M": [0, 4, 7], + "m": [0, 3, 7], + "7": [0, 4, 7, 10], + "M7": [0, 4, 7, 11], + "m7": [0, 3, 7], + "augmented": [0, 4, 8], + "a": [0, 4, 8], + "diminished": [0, 3, 6], + "i": [0, 3, 6], + "diminished7": [0, 3, 6, 9], + "i7": [0, 3, 6, 9] +} +## Chord Quality (from sonic pi) ## +MAJOR7 = "major7" +DOM7 = "dom7" +MINOR7 = "minor7" +AUG = "aug" +DIM = "dim" +DIM7 = "dim7" + + diff --git a/psonic/internals/scales.py b/psonic/internals/scales.py new file mode 100644 index 0000000..f338790 --- /dev/null +++ b/psonic/internals/scales.py @@ -0,0 +1,87 @@ +_ionian_sequence = [2, 2, 1, 2, 2, 2, 1] +_hex_sequence = [2, 2, 1, 2, 2, 3] +_pentatonic_sequence = [3, 2, 2, 3, 2] + +_SCALE_MODE = { + 'diatonic': _ionian_sequence, + 'ionian': _ionian_sequence, + 'major': _ionian_sequence, + 'dorian': _ionian_sequence[1:] + _ionian_sequence[:1], # rotate 1 + 'phrygian': _ionian_sequence[2:] + _ionian_sequence[:2], # rotate(2) + 'lydian': _ionian_sequence[3:] + _ionian_sequence[:3], # rotate(3) + 'mixolydian': _ionian_sequence[4:] + _ionian_sequence[:4], # rotate(4) + 'aeolian': _ionian_sequence[5:] + _ionian_sequence[:5], # rotate(5) + 'minor': _ionian_sequence[5:] + _ionian_sequence[:5], # rotate(5) + 'locrian': _ionian_sequence[6:] + _ionian_sequence[:6], # rotate(6) + 'hex_major6': _hex_sequence, + 'hex_dorian': _hex_sequence[1:] + _hex_sequence[:1], # rotate(1) + 'hex_phrygian': _hex_sequence[2:] + _hex_sequence[:2], # rotate(2) + 'hex_major7': _hex_sequence[3:] + _hex_sequence[:3], # rotate(3) + 'hex_sus': _hex_sequence[4:] + _hex_sequence[:4], # rotate(4) + 'hex_aeolian': _hex_sequence[5:] + _hex_sequence[:5], # rotate(5) + 'minor_pentatonic': _pentatonic_sequence, + 'yu': _pentatonic_sequence, + 'major_pentatonic': _pentatonic_sequence[1:] + _pentatonic_sequence[:1], # rotate(1) + 'gong': _pentatonic_sequence[1:] + _pentatonic_sequence[:1], # rotate(1) + 'egyptian': _pentatonic_sequence[2:] + _pentatonic_sequence[:2], # rotate(2) + 'shang': _pentatonic_sequence[2:] + _pentatonic_sequence[:2], # rotate(2) + 'jiao': _pentatonic_sequence[3:] + _pentatonic_sequence[:3], # rotate(3) + 'zhi': _pentatonic_sequence[4:] + _pentatonic_sequence[:4], # rotate(4) + 'ritusen': _pentatonic_sequence[4:] + _pentatonic_sequence[:4], # rotate(4) + 'whole_tone': [2, 2, 2, 2, 2, 2], + 'whole': [2, 2, 2, 2, 2, 2], + 'chromatic': [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + 'harmonic_minor': [2, 1, 2, 2, 1, 3, 1], + 'melodic_minor_asc': [2, 1, 2, 2, 2, 2, 1], + 'hungarian_minor': [2, 1, 3, 1, 1, 3, 1], + 'octatonic': [2, 1, 2, 1, 2, 1, 2, 1], + 'messiaen1': [2, 2, 2, 2, 2, 2], + 'messiaen2': [1, 2, 1, 2, 1, 2, 1, 2], + 'messiaen3': [2, 1, 1, 2, 1, 1, 2, 1, 1], + 'messiaen4': [1, 1, 3, 1, 1, 1, 3, 1], + 'messiaen5': [1, 4, 1, 1, 4, 1], + 'messiaen6': [2, 2, 1, 1, 2, 2, 1, 1], + 'messiaen7': [1, 1, 1, 2, 1, 1, 1, 1, 2, 1], + 'super_locrian': [1, 2, 1, 2, 2, 2, 2], + 'hirajoshi': [2, 1, 4, 1, 4], + 'kumoi': [2, 1, 4, 2, 3], + 'neapolitan_major': [1, 2, 2, 2, 2, 2, 1], + 'bartok': [2, 2, 1, 2, 1, 2, 2], + 'bhairav': [1, 3, 1, 2, 1, 3, 1], + 'locrian_major': [2, 2, 1, 1, 2, 2, 2], + 'ahirbhairav': [1, 3, 1, 2, 2, 1, 2], + 'enigmatic': [1, 3, 2, 2, 2, 1, 1], + 'neapolitan_minor': [1, 2, 2, 2, 1, 3, 1], + 'pelog': [1, 2, 4, 1, 4], + 'augmented2': [1, 3, 1, 3, 1, 3], + 'scriabin': [1, 3, 3, 2, 3], + 'harmonic_major': [2, 2, 1, 2, 1, 3, 1], + 'melodic_minor_desc': [2, 1, 2, 2, 1, 2, 2], + 'romanian_minor': [2, 1, 3, 1, 2, 1, 2], + 'hindu': [2, 2, 1, 2, 1, 2, 2], + 'iwato': [1, 4, 1, 4, 2], + 'melodic_minor': [2, 1, 2, 2, 2, 2, 1], + 'diminished2': [2, 1, 2, 1, 2, 1, 2, 1], + 'marva': [1, 3, 2, 1, 2, 2, 1], + 'melodic_major': [2, 2, 1, 2, 1, 2, 2], + 'indian': [4, 1, 2, 3, 2], + 'spanish': [1, 3, 1, 2, 1, 2, 2], + 'prometheus': [2, 2, 2, 5, 1], + 'diminished': [1, 2, 1, 2, 1, 2, 1, 2], + 'todi': [1, 2, 3, 1, 1, 3, 1], + 'leading_whole': [2, 2, 2, 2, 2, 1, 1], + 'augmented': [3, 1, 3, 1, 3, 1], + 'purvi': [1, 3, 2, 1, 1, 3, 1], + 'chinese': [4, 2, 1, 4, 1], + 'lydian_minor': [2, 2, 2, 1, 1, 2, 2], + 'i': _ionian_sequence, + 'ii': _ionian_sequence[1:] + _ionian_sequence[:1], # rotate(1) + 'iii': _ionian_sequence[2:] + _ionian_sequence[:2], # rotate(2) + 'iv': _ionian_sequence[3:] + _ionian_sequence[:3], # rotate(3) + 'v': _ionian_sequence[4:] + _ionian_sequence[:4], # rotate(4) + 'vi': _ionian_sequence[5:] + _ionian_sequence[:5], # rotate(5) + 'vii': _ionian_sequence[6:] + _ionian_sequence[:6], # rotate(6), + 'viii': _ionian_sequence[7:] + _ionian_sequence[:7], # rotate(7) +} + + diff --git a/psonic/notes.py b/psonic/notes.py new file mode 100755 index 0000000..2cd84f0 --- /dev/null +++ b/psonic/notes.py @@ -0,0 +1,30 @@ +"""Notes""" + +root_notes = { + "C0": 12, + "Cs0": 13, + "Db0": 13, + "D0": 14, + "Ds0": 15, + "Eb0": 15, + "E0": 16, + "F0": 17, + "Fs0": 18, + "Gb0": 18, + "G0": 19, + "Gs0": 20, + "Ab0": 20, + "A0": 21, + "As0": 22, + "Bb0": 22, + "B0": 23, +} + +notes = {} +for octave in range(9): + for key in root_notes: + notes[key.replace("0", str(octave))] = root_notes[key] + (12 * octave) + +globals().update(notes) + +R = 0 diff --git a/psonic/psonic.py b/psonic/psonic.py new file mode 100755 index 0000000..5606525 --- /dev/null +++ b/psonic/psonic.py @@ -0,0 +1,219 @@ +import os +import re +import random +from .samples import Sample +from .synthesizers import SAW +from .notes import C5 +from .internals.chords import _CHORD_QUALITY +from .internals.scales import _SCALE_MODE +from .synth_server import ( + SonicPi, + use_synth, +) + +__debug = False + +def synth(name, note=None, attack=None, decay=None, + sustain_level=None, sustain=None, release=None, + cutoff=None, cutoff_attack=None, amp=None, pan=None): + + arguments = locals() + arguments.pop('name') + parameters = ['{0}: {1}'.format(k, v) for k, v in arguments.items() if v is not None] + parameter = '' + if len(parameters) > 0: parameter = ',' + ','.join(parameters) + + command = 'synth :{0}{1}'.format(name.name, parameter) + + _debug('synth command={}'.format(command)) + synth_server.synth(command) + +def play(note, attack=None, decay=None, + sustain_level=None, sustain=None, release=None, + cutoff=None, cutoff_attack=None, amp=None, pan=None): + + arguments = locals() + arguments.pop('note') + parameters = ['{0}: {1}'.format(k, v) for k, v in arguments.items() if v is not None] + parameter = '' + if len(parameters) > 0: parameter = ',' + ','.join(parameters) + + command = 'play {0}{1}'.format(note, parameter) + + _debug('play command={}'.format(command)) + synth_server.play(command) + +def play_pattern_timed(notes, times, release=None): + """play notes + :param notes: + :param times: + :return: + """ + if not type(notes) is list: notes = [notes] + if not type(times) is list: times = [times] + + for t in times: + for i in notes: + play(i, release=release) + sleep(t) + +def play_pattern(notes): + """:param notes: + :return: + """ + play_pattern_timed(notes, 1) + +def sample(sample, rate=None, attack=None, sustain=None, + release=None, beat_stretch=None, start=None, + finish=None, amp=None, pan=None): + + arguments = locals() + arguments.pop('sample') + parameters = ['{0}: {1}'.format(k, v) for k, v in arguments.items() if v is not None] + parameter = '' + command = '' + + if len(parameters) > 0: parameter = ',' + ','.join(parameters) + if type(sample) == Sample: + command = 'sample :{0}{1}'.format(sample.name, parameter) + else: + command = 'sample "{0}"{1}'.format(sample, parameter) + _debug('sample command={}'.format(command)) + synth_server.sample(command) + +def sleep(duration): + """the same as time.sleep + :param duration: + :return: + """ + synth_server.sleep(duration) + _debug('sleep', duration) + +def sample_duration(sample): + """Returns the duration of the sample (in seconds) + :param sample: + :return: number + """ + return sample.duration + +def one_in(max): + """random function returns True in one of max cases + :param max: + :return: boolean + """ + return random.randint(1, max) == 1 + +def invert_pitches(pitches, inversion): + """Inverts a list of pitches, wrapping the top pitches an octave below the root + :param pitches: list + :param inversion: int + :return: list + """ + + for i in range(1, (inversion % (len(pitches)))+1): + pitches[-i] = pitches[-i] - 12 + + pitches.sort() + + return pitches + +def chord(root_note, chord_quality, inversion=None): + """Generates a list of notes of a chord + :param root_note: + :param chord_quality: + :param inversion: + :return: list + """ + result = [] + n = root_note + + half_tone_steps = _CHORD_QUALITY[chord_quality] + + for i in half_tone_steps: + q = n + i + result.append(q) + + if inversion: + result = invert_pitches(result, inversion) + + return result + +def scale(root_note, scale_mode, num_octaves=1): + """Genarates a liste of notes of scale + :param root_note: + :param scale_mode: + :param num_octaves: + :return: list + """ + result = [] + n = root_note + + half_tone_steps = _SCALE_MODE[scale_mode] + + for o in range(num_octaves): + n = root_note + o * 12 + result.append(n) + for i in half_tone_steps: + n = n + i + result.append(n) + + return result + +def run(command): + synth_server.run(command) + +def stop(): + synth_server.stop() + +def send_message(message, *parameter): + synth_server.send_message(message, *parameter) + + +def start_recording(): + synth_server.start_recording() + + +def stop_recording(): + synth_server.stop_recording() + + +def save_recording(name): + synth_server.save_recording(name) + +synth_server = SonicPi() + +def set_server_parameter(udp_ip, token, udp_port=-1, udp_port_osc_message=-1): + synth_server.set_parameter(udp_ip, token, udp_port, udp_port_osc_message) + +def set_server_parameter_from_log(udp_ip, log_file=None): + if log_file is None: + # Automatically find the log file + home = os.path.expanduser("~") + log_file = os.path.join(home, ".sonic-pi", "log", "spider.log") + print(f"Reading Parameters From Log File: {log_file}") + + with open(log_file, 'r') as f: + text = f.read() + + # Use regular expressions to extract the values + token_match = re.search(r'Token: (-?\d+)', text) + server_port_match = re.search(r':server_port=>(\d+)', text) + osc_cues_port_match = re.search(r':osc_cues_port=>(\d+)', text) + + token = int(token_match.group(1)) if token_match else None + udp_port = int(server_port_match.group(1)) if server_port_match else None + udp_port_osc_message = int(osc_cues_port_match.group(1)) if osc_cues_port_match else None + + if token and udp_port and udp_port_osc_message: + print(f"Reading Parameters Succeeded. ip: {udp_ip}, token: \"{token}\", server_port: \"{udp_port}\", osc_port: {udp_port_osc_message}") + synth_server.set_parameter(udp_ip, token, udp_port, udp_port_osc_message) + else: + raise RuntimeError(f"Reading Parameters Failed. token: {token}, server_port: {udp_port}, osc_port: {udp_port_osc_message}") + +def _debug(*args): + if __debug: print(args) + + +if __name__ == '__main__': + use_synth(SAW) + play(C5, amp=2, pan=-1) diff --git a/psonic/samples/__init__.py b/psonic/samples/__init__.py new file mode 100644 index 0000000..b53120d --- /dev/null +++ b/psonic/samples/__init__.py @@ -0,0 +1,6 @@ +class Sample(object): + """Sample""" + + def __init__(self, name, duration): + self.name = name + self.duration = duration diff --git a/psonic/samples/ambient.py b/psonic/samples/ambient.py new file mode 100644 index 0000000..6a1bdb7 --- /dev/null +++ b/psonic/samples/ambient.py @@ -0,0 +1,15 @@ +"""Ambient sounds""" + +from . import Sample + + +AMBI_CHOIR = Sample('ambi_choir', 1.5715419501133787) +AMBI_DARK_WOOSH = Sample('ambi_dark_woosh', 3.7021315192743764) +AMBI_DRONE = Sample('ambi_drone', 4.40843537414966) +AMBI_GLASS_HUM = Sample('ambi_glass_hum', 10.0) +AMBI_GLASS_RUB = Sample('ambi_glass_rub', 3.1493650793650794) +AMBI_HAUNTED_HUM = Sample('ambi_haunted_hum', 9.78156462585034) +AMBI_LUNAR_LAND = Sample('ambi_lunar_land', 7.394240362811791) +AMBI_PIANO = Sample('ambi_piano', 2.811746031746032) +AMBI_SOFT_BUZZ = Sample('ambi_soft_buzz', 0.7821541950113379) +AMBI_SWOOSH = Sample('ambi_swoosh', 1.8484580498866212) diff --git a/psonic/samples/bass.py b/psonic/samples/bass.py new file mode 100644 index 0000000..bd6a1c3 --- /dev/null +++ b/psonic/samples/bass.py @@ -0,0 +1,13 @@ +"""Bass sounds""" + +from . import Sample + + +BASS_DNB_F = Sample('bass_dnb_f', 0.8705442176870748) +BASS_DROP_C = Sample('bass_drop_c', 2.3589115646258505) +BASS_HARD_C = Sample('bass_hard_c', 1.5) +BASS_HIT_C = Sample('bass_hit_c', 0.6092517006802721) +BASS_THICK_C = Sample('bass_thick_c', 3.9680725623582767) +BASS_VOXY_C = Sample('bass_voxy_c', 6.23469387755102) +BASS_VOXY_HIT_C = Sample('bass_voxy_hit_c', 0.457437641723356) +BASS_WOODSY_C = Sample('bass_woodsy_c', 3.252267573696145) diff --git a/psonic/samples/drums.py b/psonic/samples/drums.py new file mode 100644 index 0000000..b73f0f5 --- /dev/null +++ b/psonic/samples/drums.py @@ -0,0 +1,45 @@ +"""Drum sounds""" + +from . import Sample + + +## Drum Sounds +DRUM_BASS_HARD = Sample('drum_bass_hard', 0.6951927437641723) +DRUM_BASS_SOFT = Sample('drum_bass_soft', 0.5624489795918367) +DRUM_COWBELL = Sample('drum_cowbell', 0.34988662131519277) +DRUM_CYMBAL_CLOSED = Sample('drum_cymbal_closed', 0.2069387755102041) +DRUM_CYMBAL_HARD = Sample('drum_cymbal_hard', 1.6388208616780044) +DRUM_CYMBAL_OPEN = Sample('drum_cymbal_open', 1.8043764172335601) +DRUM_CYMBAL_PEDAL = Sample('drum_cymbal_pedal', 0.2721995464852608) +DRUM_CYMBAL_SOFT = Sample('drum_cymbal_soft', 0.9497278911564626) +DRUM_HEAVY_KICK = Sample('drum_heavy_kick', 0.2701360544217687) +DRUM_ROLL = Sample('drum_roll', 6.241678004535148) +DRUM_SNARE_HARD = Sample('drum_snare_hard', 0.44492063492063494) +DRUM_SNARE_SOFT = Sample('drum_snare_soft', 0.3147845804988662) +DRUM_SPLASH_HARD = Sample('drum_splash_hard', 2.5961904761904764) +DRUM_SPLASH_SOFT = Sample('drum_splash_soft', 1.857981859410431) +DRUM_TOM_HI_HARD = Sample('drum_tom_hi_hard', 0.7376643990929705) +DRUM_TOM_HI_SOFT = Sample('drum_tom_hi_soft', 0.6290249433106576) +DRUM_TOM_LO_HARD = Sample('drum_tom_lo_hard', 1.0002267573696144) +DRUM_TOM_LO_SOFT = Sample('drum_tom_lo_soft', 0.8634013605442177) +DRUM_TOM_MID_HARD = Sample('drum_tom_mid_hard', 0.7342176870748299) +DRUM_TOM_MID_SOFT = Sample('drum_tom_mid_soft', 0.6721315192743764) + +## Snare Drums Sounds +SN_DOLF = Sample('sn_dolf', 0.37759637188208617) +SN_DUB = Sample('sn_dub', 0.2781179138321995) +SN_ZOME = Sample('sn_zome', 0.4787528344671202) + +## Bass Drums Sounds +BD_808 = Sample('bd_808', 0.5597505668934241) +BD_ADA = Sample('bd_ada', 0.10179138321995465) +BD_BOOM = Sample('bd_boom', 1.7142857142857142) +BD_FAT = Sample('bd_fat', 0.23219954648526078) +BD_GAS = Sample('bd_gas', 0.4471428571428571) +BD_HAUS = Sample('bd_haus', 0.21993197278911564) +BD_KLUB = Sample('bd_klub', 0.368843537414966) +BD_PURE = Sample('bd_pure', 0.43324263038548755) +BD_SONE = Sample('bd_sone', 0.4089115646258503) +BD_TEK = Sample('bd_tek', 0.24024943310657595) +BD_ZOME = Sample('bd_zome', 0.45972789115646256) +BD_ZUM = Sample('bd_zum', 0.13158730158730159) diff --git a/psonic/samples/electric.py b/psonic/samples/electric.py new file mode 100644 index 0000000..2af4640 --- /dev/null +++ b/psonic/samples/electric.py @@ -0,0 +1,30 @@ +"""Electric Sounds""" + +from . import Sample + + +ELEC_BEEP = Sample('elec_beep', 0.10578231292517007) +ELEC_BELL = Sample('elec_bell', 0.27825396825396825) +ELEC_BLIP = Sample('elec_blip', 0.15816326530612246) +ELEC_BLIP2 = Sample('elec_blip2', 0.1840816326530612) +ELEC_BLUP = Sample('elec_blup', 0.6632199546485261) +ELEC_BONG = Sample('elec_bong', 0.3464852607709751) +ELEC_CHIME = Sample('elec_chime', 2.2775510204081635) +ELEC_CYMBAL = Sample('elec_cymbal', 0.563219954648526) +ELEC_FILT_SNARE = Sample('elec_filt_snare', 1.5158503401360544) +ELEC_FLIP = Sample('elec_flip', 0.07476190476190477) +ELEC_FUZZ_TOM = Sample('elec_fuzz_tom', 0.37179138321995464) +ELEC_HI_SNARE = Sample('elec_hi_snare', 0.19736961451247165) +ELEC_HOLLOW_KICK = Sample('elec_hollow_kick', 0.15564625850340136) +ELEC_LO_SNARE = Sample('elec_lo_snare', 0.6607936507936508) +ELEC_MID_SNARE = Sample('elec_mid_snare', 0.7256235827664399) +ELEC_PING = Sample('elec_ping', 0.21226757369614513) +ELEC_PLIP = Sample('elec_plip', 0.19882086167800453) +ELEC_POP = Sample('elec_pop', 0.08680272108843537) +ELEC_SNARE = Sample('elec_snare', 0.3893197278911565) +ELEC_SOFT_KICK = Sample('elec_soft_kick', 0.1364172335600907) +ELEC_TICK = Sample('elec_tick', 0.01943310657596372) +ELEC_TRIANGLE = Sample('elec_triangle', 0.22294784580498866) +ELEC_TWANG = Sample('elec_twang', 0.6243083900226757) +ELEC_TWIP = Sample('elec_twip', 0.10140589569160997) +ELEC_WOOD = Sample('elec_wood', 0.47811791383219954) diff --git a/psonic/samples/guitars.py b/psonic/samples/guitars.py new file mode 100644 index 0000000..012de6a --- /dev/null +++ b/psonic/samples/guitars.py @@ -0,0 +1,9 @@ +"""Sounds featuring guitars""" + +from . import Sample + + +GUIT_E_FIFTHS = Sample('guit_e_fifths', 5.971791383219955) +GUIT_E_SLIDE = Sample('guit_e_slide', 4.325192743764172) +GUIT_EM9 = Sample('guit_em9', 9.972063492063493) +GUIT_HARMONICS = Sample('guit_harmonics', 3.5322675736961453) diff --git a/psonic/samples/loops/__init__.py b/psonic/samples/loops/__init__.py new file mode 100644 index 0000000..514fbb5 --- /dev/null +++ b/psonic/samples/loops/__init__.py @@ -0,0 +1,14 @@ +"""Sounds for Looping""" + +from psonic.samples import Sample + + +LOOP_AMEN = Sample('loop_amen', 1.753310657596372) +LOOP_AMEN_FULL = Sample('loop_amen_full', 6.857142857142857) +LOOP_BREAKBEAT = Sample('loop_breakbeat', 1.9047619047619047) +LOOP_COMPUS = Sample('loop_compus', 6.486485260770975) +LOOP_GARZUL = Sample('loop_garzul', 8.0) +LOOP_INDUSTRIAL = Sample('loop_industrial', 0.8837414965986394) +LOOP_MIKA = Sample('loop_mika', 8.0) +LOOP_SAFARI = Sample('loop_safari', 8.005079365079364) +LOOP_TABLA = Sample('loop_tabla', 10.673990929705216) diff --git a/psonic/samples/misc.py b/psonic/samples/misc.py new file mode 100644 index 0000000..6773be2 --- /dev/null +++ b/psonic/samples/misc.py @@ -0,0 +1,8 @@ +"""Miscellaneous Sounds""" + +from . import Sample + + +MISC_BURP = Sample('misc_burp', 0.7932879818594104) +MISC_CINEBOOM = Sample('misc_cineboom', 7.922675736961451) +MISC_CROW = Sample('misc_crow', 0.48063492063492064) diff --git a/psonic/samples/percussions.py b/psonic/samples/percussions.py new file mode 100644 index 0000000..ad03a56 --- /dev/null +++ b/psonic/samples/percussions.py @@ -0,0 +1,10 @@ +"""Percurssive Sounds""" + +from . import Sample + + +PERC_BELL = Sample('perc_bell', 6.719206349206349) +PERC_SNAP = Sample('perc_snap', 0.30795918367346936) +PERC_SNAP2 = Sample('perc_snap2', 0.17414965986394557) +PERC_SWASH = Sample('perc_swash', 0.3195011337868481) +PERC_TILL = Sample('perc_till', 2.665736961451247) diff --git a/psonic/samples/tabla.py b/psonic/samples/tabla.py new file mode 100644 index 0000000..7bd64be --- /dev/null +++ b/psonic/samples/tabla.py @@ -0,0 +1,31 @@ +"""Tabla""" + +from . import Sample + + +TABLA_DHEC = Sample('tabla_dhec', 0.3250793650793651) +TABLA_GHE1 = Sample('tabla_ghe1', 0.5912244897959184) +TABLA_GHE2 = Sample('tabla_ghe2', 2.6607256235827665) +TABLA_GHE3 = Sample('tabla_ghe3', 2.3908163265306124) +TABLA_GHE4 = Sample('tabla_ghe4', 0.7960997732426304) +TABLA_GHE5 = Sample('tabla_ghe5', 3.560045351473923) +TABLA_GHE6 = Sample('tabla_ghe6', 3.6011337868480724) +TABLA_GHE7 = Sample('tabla_ghe7', 2.1512698412698414) +TABLA_GHE8 = Sample('tabla_ghe8', 0.5817913832199546) +TABLA_KE1 = Sample('tabla_ke1', 0.04342403628117914) +TABLA_KE2 = Sample('tabla_ke2', 0.04403628117913832) +TABLA_KE3 = Sample('tabla_ke3', 0.07571428571428572) +TABLA_NA = Sample('tabla_na', 0.7198185941043084) +TABLA_NA_O = Sample('tabla_na_o', 1.4889795918367348) +TABLA_NA_S = Sample('tabla_na_s', 0.23582766439909297) +TABLA_RE = Sample('tabla_re', 0.2815419501133787) +TABLA_TAS1 = Sample('tabla_tas1', 1.1116553287981858) +TABLA_TAS2 = Sample('tabla_tas2', 1.4338321995464853) +TABLA_TAS3 = Sample('tabla_tas3', 1.2364625850340136) +TABLA_TE1 = Sample('tabla_te1', 0.17777777777777778) +TABLA_TE2 = Sample('tabla_te2', 0.33233560090702946) +TABLA_TE_M = Sample('tabla_te_m', 0.28879818594104306) +TABLA_TE_NE = Sample('tabla_te_ne', 0.15310657596371882) +TABLA_TUN1 = Sample('tabla_tun1', 2.3394104308390022) +TABLA_TUN2 = Sample('tabla_tun2', 2.693514739229025) +TABLA_TUN3 = Sample('tabla_tun3', 2.0956009070294783) diff --git a/psonic/samples/vinyl.py b/psonic/samples/vinyl.py new file mode 100644 index 0000000..e96ef67 --- /dev/null +++ b/psonic/samples/vinyl.py @@ -0,0 +1,8 @@ +"""Vinyl""" +from . import Sample + + +VINYL_BACKSPIN = Sample('vinyl_backspin', 1.06124716553288) +VINYL_HISS = Sample('vinyl_hiss', 8.0) +VINYL_REWIND = Sample('vinyl_rewind', 2.6804761904761905) +VINYL_SCRATCH = Sample('vinyl_scratch', 0.27383219954648524) diff --git a/psonic/scales.py b/psonic/scales.py new file mode 100644 index 0000000..a1de503 --- /dev/null +++ b/psonic/scales.py @@ -0,0 +1,81 @@ +"""Scale Mode (from sonic pi)""" + +DIATONIC = 'diatonic' +IONIAN = 'ionian' +MAJOR = 'major' +DORIAN = 'dorian' +PHRYGIAN = 'phrygian' +LYDIAN = 'lydian' +MIXOLYDIAN = 'mixolydian' +AEOLIAN = 'aeolian' +MINOR = 'minor' +LOCRIAN = 'locrian' +HEX_MAJOR6 = 'hex_major6' +HEX_DORIAN = 'hex_dorian' +HEX_PHRYGIAN = 'hex_phrygian' +HEX_MAJOR7 = 'hex_major7' +HEX_SUS = 'hex_sus' +HEX_AEOLIAN = 'hex_aeolian' +MINOR_PENTATONIC = 'minor_pentatonic' +YU = 'yu' +MAJOR_PENTATONIC = 'major_pentatonic' +GONG = 'gong' +EGYPTIAN = 'egyptian' +SHANG = 'shang' +JIAO = 'jiao' +ZHI = 'zhi' +RITUSEN = 'ritusen' +WHOLE_TONE = 'whole_tone' +WHOLE = 'whole' +CHROMATIC = 'chromatic' +HARMONIC_MINOR = 'harmonic_minor' +MELODIC_MINOR_ASC = 'melodic_minor_asc' +HUNGARIAN_MINOR = 'hungarian_minor' +OCTATONIC = 'octatonic' +MESSIAEN1 = 'messiaen1' +MESSIAEN2 = 'messiaen2' +MESSIAEN3 = 'messiaen3' +MESSIAEN4 = 'messiaen4' +MESSIAEN5 = 'messiaen5' +MESSIAEN6 = 'messiaen6' +MESSIAEN7 = 'messiaen7' +SUPER_LOCRIAN = 'super_locrian' +HIRAJOSHI = 'hirajoshi' +KUMOI = 'kumoi' +NEAPLOLITAN_MAJOR = 'neapolitan_major' +BARTOK = 'bartok' +BHAIRAV = 'bhairav' +LOCRIAN_MAJOR = 'locrian_major' +AHIRBHAIRAV = 'ahirbhairav' +ENIGMATIC = 'enigmatic' +NEAPLOLITAN_MINOR = 'neapolitan_minor' +PELOG = 'pelog' +AUGMENTED2 = 'augmented2' +SCRIABIN = 'scriabin' +HARMONIC_MAJOR = 'harmonic_major' +MELODIC_MINOR_DESC = 'melodic_minor_desc' +ROMANIAN_MINOR = 'romanian_minor' +HINDU = 'hindu' +IWATO = 'iwato' +MELODIC_MINOR = 'melodic_minor' +DIMISHED2 = 'diminished2' +MARVA = 'marva' +MELODIC_MAJOR = 'melodic_major' +INDIAN = 'indian' +SPANISH = 'spanish' +PROMETHEUS = 'prometheus' +DIMISHED = 'diminished' +TODI = 'todi' +LEADING_WHOLE = 'leading_whole' +AUGMENTED = 'augmented' +PRUVI = 'purvi' +CHINESE = 'chinese' +LYDIAN_MINOR = 'lydian_minor' +I = 'i' +II = 'ii' +III = 'iii' +IV = 'iv' +V = 'v' +VI = 'vi' +VII = 'vii' +VIII = 'viii' diff --git a/psonic/synth_server.py b/psonic/synth_server.py new file mode 100755 index 0000000..732a104 --- /dev/null +++ b/psonic/synth_server.py @@ -0,0 +1,163 @@ +"""SonicPi synth server""" + +import time +from pythonosc import osc_message_builder # osc support +from pythonosc import udp_client +from .synthesizers import BEEP + +## Module attributes ## +_current_synth = BEEP + +## Module methodes ## +def use_synth(synth): + global _current_synth + _current_synth = synth + + +## Compound classes ## +class SonicPiCommon: + + UDP_IP = "127.0.0.1" + + def __init__(self): + self.udp_ip = self.UDP_IP + + def set_parameter(self, udp_ip=""): + if udp_ip == "": + self.udp_ip = self.UDP_IP + else: + self.udp_ip = udp_ip + + def send(self, command): + pass + + def sleep(self, duration): + time.sleep(duration) + + def sample(self, command): + self.send(command) + +## Sonic Pi version 4.X: Ports can be found in +# Windows: C:/Users//.sonic-pi/log/spider.log +# Linux: ~/.sonic-pi/log/spider.log +# Both the server_port and Token are required +# Default OSC Cues Port is still 4560 + +## Connection classes ## +class SonicPi(SonicPiCommon): + """Communiction to Sonic Pi""" + + #UDP_PORT = 4557 + UDP_PORT = 51235 + + #UDP_PORT_OSC_MESSAGE = 4559 + UDP_PORT_OSC_MESSAGE = 4560 + + RUN_COMMAND = "/run-code" + STOP_COMMAND = "/stop-all-jobs" + START_RECORDING_COMMAND = "/start-recording" + STOP_RECORDING_COMMAND = "/stop-recording" + SAVE_RECORDING_COMMAND = "/save-recording" + + def __init__(self): + super().__init__() + self.udp_port = self.UDP_PORT + self.udp_port_osc_message = self.UDP_PORT_OSC_MESSAGE + self.token=None + + self._init_client() + + def _init_client(self): + self.client = udp_client.UDPClient( + self.udp_ip, + self.udp_port + ) + self.client_for_messages = udp_client.UDPClient( + self.udp_ip, + self.udp_port_osc_message + ) + + def set_parameter(self, udp_ip = "", token = "", udp_port=-1, udp_port_osc_message=-1): + super().set_parameter(udp_ip) + if udp_port == -1: udp_port = self.UDP_PORT + self.udp_port = udp_port + self.token=token + if udp_port_osc_message == -1: udp_port_osc_message = self.UDP_PORT_OSC_MESSAGE + self.udp_port_osc_message = udp_port_osc_message + + self._init_client() + + def play(self, command): + command = f'use_synth :{_current_synth.name}\n{command}' + self.send(command) + + def synth(self, command): + self.send(command) + + def run(self, command): + self.send_command(SonicPi.RUN_COMMAND, command) + + def start_recording(self): + self.send_command(SonicPi.START_RECORDING_COMMAND) + + def stop_recording(self): + self.send_command(SonicPi.START_RECORDING_COMMAND) + + def save_recording(self, name): + self.send_command(SonicPi.SAVE_RECORDING_COMMAND,name) + + + def send(self,command): + self.run(command) + + def stop(self): + self.send_command(SonicPi.STOP_COMMAND) + + def test_connection(self): + # OSC::Server.new(PORT) + # abort("ERROR: Sonic Pi is not listening on #{PORT} - is it running?") + pass + + def send_command(self, address, argument=''): + msg = osc_message_builder.OscMessageBuilder(address=address) + if self.token is None: + raise RuntimeError("No token specified, please set token from file or manually before sending a command") + msg.add_arg(self.token) + if argument != "": + msg.add_arg(argument) + msg = msg.build() + self.client.send(msg) + + def send_message(self, message, *parameters): + msg = osc_message_builder.OscMessageBuilder(message) + for p in parameters: + msg.add_arg(p) + msg = msg.build() + self.client_for_messages.send(msg) + +class SonicPiNew(SonicPiCommon): + """Communiction to Sonic Pi""" + + UDP_PORT = 4559 + + def __init__(self): + self.client = udp_client.UDPClient( + self.UDP_IP, + self.UDP_PORT + ) + self.commandServer = SonicPi() + # x= 'live_loop :py do\n nv=sync "/SENDOSC"\n puts nv\n eval(nv[0])\nend' + # self.commandServer.run(x) + + def set_OSC_receiver(self, source): + self.commandServer.run(source) + + def send(self, address, *message): + msg = osc_message_builder.OscMessageBuilder(address) + for m in message: + msg.add_arg(m) + msg = msg.build() + self.client.send(msg) + + def play(self, command): + self.send(command) diff --git a/psonic/synthesizers.py b/psonic/synthesizers.py new file mode 100644 index 0000000..47be2cf --- /dev/null +++ b/psonic/synthesizers.py @@ -0,0 +1,51 @@ +"""Synthesizer""" + +class Synth: + """Synthesizer""" + + def __init__(self, name): + self.name = name + + +DULL_BELL = Synth('dull_bell') +PRETTY_BELL = Synth('pretty_bell') +SINE = Synth('sine') +SQUARE = Synth('square') +PULSE = Synth('pulse') +SUBPULSE = Synth('subpulse') +DTRI = Synth('dtri') +DPULSE = Synth('dpulse') +FM = Synth('fm') +MOD_FM = Synth('mod_fm') +MOD_SAW = Synth('mod_saw') +MOD_DSAW = Synth('mod_dsaw') +MOD_SINE = Synth('mod_sine') +MOD_TRI = Synth('mod_tri') +MOD_PULSE = Synth('mod_pulse') +SUPERSAW = Synth('supersaw') +HOOVER = Synth('hoover') +SYNTH_VIOLIN = Synth('synth_violin') +PLUCK = Synth('pluck') +PIANO = Synth('piano') +GROWL = Synth('growl') +DARK_AMBIENCE = Synth('dark_ambience') +DARK_SEA_HORN = Synth('dark_sea_horn') +HOLLOW = Synth('hollow') +ZAWA = Synth('zawa') +NOISE = Synth('noise') +GNOISE = Synth('gnoise') +BNOISE = Synth('bnoise') +CNOISE = Synth('cnoise') +DSAW = Synth('dsaw') +TB303 = Synth('tb303') +BLADE = Synth('blade') +PROPHET = Synth('prophet') +SAW = Synth('saw') +BEEP = Synth('beep') +TRI = Synth('tri') +CHIPLEAD = Synth('chiplead') # Sonic Pi 2.10 +CHIPBASS = Synth('chipbass') +CHIPNOISE = Synth('chipnoise') +TECHSAWS = Synth('tech_saws') # Sonic Pi 2.11 +SOUND_IN = Synth('sound_in') +SOUND_IN_STEREO = Synth('sound_in_stereo') diff --git a/psonic_example.py b/psonic_example.py new file mode 100644 index 0000000..c702942 --- /dev/null +++ b/psonic_example.py @@ -0,0 +1,6 @@ +from psonic import * + +set_server_parameter_from_log("127.0.0.1") +run("play 60") + + diff --git a/python-sonic.ipynb b/python-sonic.ipynb old mode 100644 new mode 100755 index 63f1c33..f5bd8d0 --- a/python-sonic.ipynb +++ b/python-sonic.ipynb @@ -29,7 +29,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "* First you need Python 3 (https://www.python.org, ) - Python 3.4 should work, because it's the development environment\n", + "* First you need Python 3 (https://www.python.org, ) - Python 3.5 should work, because it's the development environment\n", "* Then Sonic Pi (https://sonic-pi.net) - That makes the sound\n", "* Modul python-osc (https://pypi.python.org/pypi/python-osc) - Connection between Python and Sonic Pi Server\n", "* And this modul python-sonic - simply copy the source\n", @@ -38,12 +38,8 @@ ] }, { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], + "cell_type": "raw", + "metadata": {}, "source": [ "$ pip install python-sonic" ] @@ -70,6 +66,49 @@ "* Only the notes from C2 to C6" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Changelog" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "|Version | |\n", + "|--------------|------------------------------------------------------------------------------------------|\n", + "| 0.2.0 | Some changes for Sonic Pi 2.11. Simpler multi-threading with decorator *@in_thread*. Messaging with *cue* and *sync*. |\n", + "| 0.3.0 | OSC Communication | \n", + "| 0.3.1. | Update, sort and duration of samples |\n", + "| 0.3.2. | Restructured |\n", + "| 0.4.0 | Changes communication ports and recording |" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Communication" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The API *python-sonic* communications with *Sonic Pi* over UDP and two ports. One port is an internal *Sonic Pi* port and could be changed. \n", + "For older *Sonic Pi* Version you have to set the ports explicitly" + ] + }, + { + "cell_type": "raw", + "metadata": {}, + "source": [ + "from psonic import *\n", + "set_server_parameters('127.0.0.1',4557,4559)" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -87,9 +126,7 @@ { "cell_type": "code", "execution_count": 1, - "metadata": { - "collapsed": false - }, + "metadata": {}, "outputs": [], "source": [ "from psonic import *" @@ -104,10 +141,8 @@ }, { "cell_type": "code", - "execution_count": 3, - "metadata": { - "collapsed": false - }, + "execution_count": 2, + "metadata": {}, "outputs": [], "source": [ "play(70) #play MIDI note 70" @@ -122,10 +157,8 @@ }, { "cell_type": "code", - "execution_count": 4, - "metadata": { - "collapsed": false - }, + "execution_count": 3, + "metadata": {}, "outputs": [], "source": [ "play(72)\n", @@ -145,9 +178,7 @@ { "cell_type": "code", "execution_count": 4, - "metadata": { - "collapsed": false - }, + "metadata": {}, "outputs": [], "source": [ "play(C5)\n", @@ -167,9 +198,7 @@ { "cell_type": "code", "execution_count": 5, - "metadata": { - "collapsed": true - }, + "metadata": {}, "outputs": [], "source": [ "play(Fs5)\n", @@ -187,9 +216,7 @@ { "cell_type": "code", "execution_count": 6, - "metadata": { - "collapsed": false - }, + "metadata": {}, "outputs": [], "source": [ "play(72,amp=2)\n", @@ -207,9 +234,7 @@ { "cell_type": "code", "execution_count": 7, - "metadata": { - "collapsed": false - }, + "metadata": {}, "outputs": [], "source": [ "use_synth(SAW)\n", @@ -232,9 +257,7 @@ { "cell_type": "code", "execution_count": 8, - "metadata": { - "collapsed": false - }, + "metadata": {}, "outputs": [], "source": [ "play (60, attack=0.5, decay=1, sustain_level=0.4, sustain=2, release=0.5) \n", @@ -251,9 +274,7 @@ { "cell_type": "code", "execution_count": 9, - "metadata": { - "collapsed": true - }, + "metadata": {}, "outputs": [], "source": [ "sample(AMBI_LUNAR_LAND, amp=0.5)" @@ -262,9 +283,7 @@ { "cell_type": "code", "execution_count": 10, - "metadata": { - "collapsed": true - }, + "metadata": {}, "outputs": [], "source": [ "sample(LOOP_AMEN,pan=-1)\n", @@ -275,9 +294,7 @@ { "cell_type": "code", "execution_count": 11, - "metadata": { - "collapsed": true - }, + "metadata": {}, "outputs": [], "source": [ "sample(LOOP_AMEN,rate=0.5)" @@ -286,9 +303,7 @@ { "cell_type": "code", "execution_count": 12, - "metadata": { - "collapsed": true - }, + "metadata": {}, "outputs": [], "source": [ "sample(LOOP_AMEN,rate=1.5)" @@ -297,9 +312,7 @@ { "cell_type": "code", "execution_count": 13, - "metadata": { - "collapsed": true - }, + "metadata": {}, "outputs": [], "source": [ "sample(LOOP_AMEN,rate=-1)#back" @@ -308,9 +321,7 @@ { "cell_type": "code", "execution_count": 14, - "metadata": { - "collapsed": true - }, + "metadata": {}, "outputs": [], "source": [ "sample(DRUM_CYMBAL_OPEN,attack=0.01,sustain=0.3,release=0.1)" @@ -319,9 +330,7 @@ { "cell_type": "code", "execution_count": 15, - "metadata": { - "collapsed": true - }, + "metadata": {}, "outputs": [], "source": [ "sample(LOOP_AMEN,start=0.5,finish=0.8,rate=-0.2,attack=0.3,release=1)" @@ -337,9 +346,7 @@ { "cell_type": "code", "execution_count": 16, - "metadata": { - "collapsed": true - }, + "metadata": {}, "outputs": [], "source": [ "import random\n", @@ -352,9 +359,7 @@ { "cell_type": "code", "execution_count": 17, - "metadata": { - "collapsed": true - }, + "metadata": {}, "outputs": [], "source": [ "for i in range(3):\n", @@ -362,6 +367,30 @@ " sleep(1)" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Sample slicing" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [], + "source": [ + "from psonic import *\n", + "\n", + "number_of_pieces = 8\n", + "\n", + "for i in range(16):\n", + " s = random.randrange(0,number_of_pieces)/number_of_pieces #sample starts at 0.0 and finishes at 1.0\n", + " f = s + (1.0/number_of_pieces)\n", + " sample(LOOP_AMEN,beat_stretch=2,start=s,finish=f)\n", + " sleep(2.0/number_of_pieces)" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -372,9 +401,7 @@ { "cell_type": "code", "execution_count": 18, - "metadata": { - "collapsed": false - }, + "metadata": {}, "outputs": [ { "ename": "KeyboardInterrupt", @@ -408,11 +435,21 @@ }, { "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": false - }, - "outputs": [], + "execution_count": 19, + "metadata": {}, + "outputs": [ + { + "ename": "KeyboardInterrupt", + "evalue": "", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mKeyboardInterrupt\u001b[0m Traceback (most recent call last)", + "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[1;32m 22\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 23\u001b[0m \u001b[0;32mwhile\u001b[0m \u001b[0;32mTrue\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 24\u001b[0;31m \u001b[0;32mpass\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", + "\u001b[0;31mKeyboardInterrupt\u001b[0m: " + ] + } + ], "source": [ "import random\n", "from psonic import *\n", @@ -450,15 +487,13 @@ { "cell_type": "code", "execution_count": 1, - "metadata": { - "collapsed": false - }, + "metadata": {}, "outputs": [ { - "name": "stdout", + "name": "stdin", "output_type": "stream", "text": [ - "Press Enter to continue...\n" + "Press Enter to continue... \n" ] }, { @@ -517,6 +552,90 @@ "To synchronize the thread, so that they play a note at the same time, you can use *Condition*. One function sends a message with *condition.notifyAll* the other waits until the message comes *condition.wait*." ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "More simple with decorator __@in_thread__" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "name": "stdin", + "output_type": "stream", + "text": [ + "Press Enter to continue... \n" + ] + } + ], + "source": [ + "from psonic import *\n", + "from random import choice\n", + "\n", + "tick = Message()\n", + "\n", + "@in_thread\n", + "def random_riff():\n", + " use_synth(PROPHET)\n", + " sc = scale(E3, MINOR)\n", + " while True:\n", + " s = random.choice([0.125,0.25,0.5])\n", + " tick.sync()\n", + " for i in range(8):\n", + " r = random.choice([0.125, 0.25, 1, 2])\n", + " n = random.choice(sc)\n", + " co = random.randint(30,100)\n", + " play(n, release = r, cutoff = co)\n", + " sleep(s)\n", + " \n", + "@in_thread\n", + "def drums():\n", + " while True:\n", + " tick.cue()\n", + " for i in range(16):\n", + " r = random.randrange(1,10)\n", + " sample(DRUM_BASS_HARD, rate=r)\n", + " sleep(0.125)\n", + "\n", + "random_riff()\n", + "drums()\n", + "\n", + "input(\"Press Enter to continue...\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from psonic import *\n", + "\n", + "tick = Message()\n", + "\n", + "@in_thread\n", + "def metronom():\n", + " while True:\n", + " tick.cue()\n", + " sleep(1)\n", + " \n", + "@in_thread\n", + "def instrument():\n", + " while True:\n", + " tick.sync()\n", + " sample(DRUM_HEAVY_KICK)\n", + "\n", + "metronom()\n", + "instrument()\n", + "\n", + "while True:\n", + " pass" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -526,10 +645,8 @@ }, { "cell_type": "code", - "execution_count": 1, - "metadata": { - "collapsed": false - }, + "execution_count": 2, + "metadata": {}, "outputs": [], "source": [ "from psonic import *\n", @@ -550,9 +667,7 @@ { "cell_type": "code", "execution_count": 2, - "metadata": { - "collapsed": false - }, + "metadata": {}, "outputs": [], "source": [ "play(chord(E4, MINOR)) \n", @@ -575,9 +690,7 @@ { "cell_type": "code", "execution_count": 3, - "metadata": { - "collapsed": false - }, + "metadata": {}, "outputs": [], "source": [ "play_pattern( chord(E4, 'm7')) \n", @@ -595,9 +708,7 @@ { "cell_type": "code", "execution_count": 4, - "metadata": { - "collapsed": false - }, + "metadata": {}, "outputs": [], "source": [ "play_pattern_timed(scale(C3, MAJOR), 0.125, release = 0.1) \n", @@ -605,6 +716,58 @@ "play_pattern_timed(scale(C3, MAJOR_PENTATONIC, num_octaves = 2), 0.125, release = 0.1)" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The function *scale* returns a list with all notes of a scale. So you can use list methodes or functions. For example to play arpeggios descending or shuffeld." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[48, 50, 52, 53, 55, 57, 59, 60]" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import random\n", + "from psonic import *\n", + "\n", + "s = scale(C3, MAJOR)\n", + "s" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "s.reverse()" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "play_pattern_timed(s, 0.125, release = 0.1)\n", + "random.shuffle(s)\n", + "play_pattern_timed(s, 0.125, release = 0.1)" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -622,9 +785,7 @@ { "cell_type": "code", "execution_count": 5, - "metadata": { - "collapsed": false - }, + "metadata": {}, "outputs": [ { "name": "stdout", @@ -674,7 +835,10 @@ "cell_type": "code", "execution_count": 6, "metadata": { - "collapsed": true + "collapsed": true, + "jupyter": { + "outputs_hidden": true + } }, "outputs": [], "source": [ @@ -688,7 +852,10 @@ "cell_type": "code", "execution_count": 7, "metadata": { - "collapsed": true + "collapsed": true, + "jupyter": { + "outputs_hidden": true + } }, "outputs": [], "source": [ @@ -702,7 +869,10 @@ "cell_type": "code", "execution_count": 8, "metadata": { - "collapsed": false + "collapsed": true, + "jupyter": { + "outputs_hidden": true + } }, "outputs": [], "source": [ @@ -730,9 +900,7 @@ { "cell_type": "code", "execution_count": 10, - "metadata": { - "collapsed": false - }, + "metadata": {}, "outputs": [ { "name": "stdout", @@ -793,7 +961,10 @@ "cell_type": "code", "execution_count": 11, "metadata": { - "collapsed": true + "collapsed": true, + "jupyter": { + "outputs_hidden": true + } }, "outputs": [], "source": [ @@ -806,7 +977,10 @@ "cell_type": "code", "execution_count": 14, "metadata": { - "collapsed": true + "collapsed": true, + "jupyter": { + "outputs_hidden": true + } }, "outputs": [], "source": [ @@ -825,9 +999,7 @@ { "cell_type": "code", "execution_count": 1, - "metadata": { - "collapsed": false - }, + "metadata": {}, "outputs": [ { "name": "stdout", @@ -891,7 +1063,10 @@ "cell_type": "code", "execution_count": 3, "metadata": { - "collapsed": true + "collapsed": true, + "jupyter": { + "outputs_hidden": true + } }, "outputs": [], "source": [ @@ -900,9 +1075,7 @@ }, { "cell_type": "markdown", - "metadata": { - "collapsed": true - }, + "metadata": {}, "source": [ "More complex live loops" ] @@ -911,7 +1084,10 @@ "cell_type": "code", "execution_count": 4, "metadata": { - "collapsed": false + "collapsed": true, + "jupyter": { + "outputs_hidden": true + } }, "outputs": [], "source": [ @@ -939,9 +1115,7 @@ { "cell_type": "code", "execution_count": 11, - "metadata": { - "collapsed": false - }, + "metadata": {}, "outputs": [ { "name": "stdout", @@ -1028,7 +1202,10 @@ "cell_type": "code", "execution_count": 12, "metadata": { - "collapsed": true + "collapsed": true, + "jupyter": { + "outputs_hidden": true + } }, "outputs": [], "source": [ @@ -1042,7 +1219,10 @@ "cell_type": "code", "execution_count": 7, "metadata": { - "collapsed": true + "collapsed": true, + "jupyter": { + "outputs_hidden": true + } }, "outputs": [], "source": [ @@ -1056,7 +1236,10 @@ "cell_type": "code", "execution_count": 14, "metadata": { - "collapsed": false + "collapsed": true, + "jupyter": { + "outputs_hidden": true + } }, "outputs": [], "source": [ @@ -1070,7 +1253,10 @@ "cell_type": "code", "execution_count": 13, "metadata": { - "collapsed": false + "collapsed": true, + "jupyter": { + "outputs_hidden": true + } }, "outputs": [], "source": [ @@ -1092,7 +1278,10 @@ "cell_type": "code", "execution_count": 15, "metadata": { - "collapsed": true + "collapsed": true, + "jupyter": { + "outputs_hidden": true + } }, "outputs": [], "source": [ @@ -1103,94 +1292,314 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## More Examples" + "### Creating Sound" ] }, { "cell_type": "code", - "execution_count": 1, + "execution_count": 4, "metadata": { - "collapsed": true + "collapsed": true, + "jupyter": { + "outputs_hidden": true + }, + "scrolled": true }, "outputs": [], "source": [ - "from psonic import *" + "from psonic import *\n", + "\n", + "synth(SINE, note=D4)\n", + "synth(SQUARE, note=D4)\n", + "synth(TRI, note=D4, amp=0.4)" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 5, "metadata": { - "collapsed": false + "collapsed": true, + "jupyter": { + "outputs_hidden": true + } }, "outputs": [], "source": [ - "#Inspired by Steve Reich Clapping Music\n", - "\n", - "clapping = [1, 1, 1, 0, 1, 1, 0, 1, 0, 1, 1, 0]\n", - "\n", - "for i in range(13):\n", - " for j in range(4):\n", - " for k in range(12): \n", - " if clapping[k] ==1 : sample(DRUM_SNARE_SOFT,pan=-0.5)\n", - " if clapping[(i+k)%12] == 1: sample(DRUM_HEAVY_KICK,pan=0.5)\n", - " sleep (0.25)" + "detune = 0.7\n", + "synth(SQUARE, note = E4)\n", + "synth(SQUARE, note = E4+detune)" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": { + "collapsed": true, + "jupyter": { + "outputs_hidden": true + } + }, + "outputs": [], + "source": [ + "detune=0.1 # Amplitude shaping\n", + "synth(SQUARE, note = E2, release = 2)\n", + "synth(SQUARE, note = E2+detune, amp = 2, release = 2)\n", + "synth(GNOISE, release = 2, amp = 1, cutoff = 60)\n", + "synth(GNOISE, release = 0.5, amp = 1, cutoff = 100)\n", + "synth(NOISE, release = 0.2, amp = 1, cutoff = 90)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "## More Informations" + "### Next Step" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "### Sonic Pi" + "Using FX *Not implemented yet*" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "collapsed": true, + "jupyter": { + "outputs_hidden": true + } + }, + "outputs": [], + "source": [ + "from psonic import *\n", + "\n", + "with Fx(SLICER):\n", + " synth(PROPHET,note=E2,release=8,cutoff=80)\n", + " synth(PROPHET,note=E2+4,release=8,cutoff=80)" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "collapsed": true, + "jupyter": { + "outputs_hidden": true + } + }, + "outputs": [], + "source": [ + "with Fx(SLICER, phase=0.125, probability=0.6,prob_pos=1):\n", + " synth(TB303, note=E2, cutoff_attack=8, release=8)\n", + " synth(TB303, note=E3, cutoff_attack=4, release=8)\n", + " synth(TB303, note=E4, cutoff_attack=2, release=8)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - ".." + "## OSC Communication (Sonic Pi Ver. 3.x or better)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "### OSC" + "In Sonic Pi version 3 or better you can work with messages." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "from psonic import *" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - ".." + "First you need a programm in the Sonic Pi server that receives messages. You can write it in th GUI or send one with Python." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "run(\"\"\"live_loop :foo do\n", + " use_real_time\n", + " a, b, c = sync \"/osc*/trigger/prophet\"\n", + " synth :prophet, note: a, cutoff: b, sustain: c\n", + "end \"\"\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "### MIDI" + "Now send a message to Sonic Pi." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "send_message('/trigger/prophet', 70, 100, 8)" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "stop()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - ".." + "## Recording" ] }, { "cell_type": "markdown", + "metadata": {}, + "source": [ + "With python-sonic you can record wave files." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "from psonic import *" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "# start recording\n", + "start_recording()\n", + "\n", + "play(chord(E4, MINOR)) \n", + "sleep(1)\n", + "play(chord(E4, MAJOR))\n", + "sleep(1)\n", + "play(chord(E4, MINOR7))\n", + "sleep(1)\n", + "play(chord(E4, DOM7))\n", + "sleep(1)" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# stop recording\n", + "stop_recording" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "# save file\n", + "save_recording('/Volumes/jupyter/python-sonic/test.wav')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## More Examples" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "collapsed": true, + "jupyter": { + "outputs_hidden": true + } + }, + "outputs": [], + "source": [ + "from psonic import *" + ] + }, + { + "cell_type": "code", + "execution_count": null, "metadata": { - "collapsed": true + "collapsed": true, + "jupyter": { + "outputs_hidden": true + } }, + "outputs": [], + "source": [ + "#Inspired by Steve Reich Clapping Music\n", + "\n", + "clapping = [1, 1, 1, 0, 1, 1, 0, 1, 0, 1, 1, 0]\n", + "\n", + "for i in range(13):\n", + " for j in range(4):\n", + " for k in range(12): \n", + " if clapping[k] ==1 : sample(DRUM_SNARE_SOFT,pan=-0.5)\n", + " if clapping[(i+k)%12] == 1: sample(DRUM_HEAVY_KICK,pan=0.5)\n", + " sleep (0.25)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Projects that use Python-Sonic" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Raspberry Pi sonic-track.py a Sonic-pi Motion Track Demo https://github.com/pageauc/sonic-track" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, "source": [ "## Sources" ] @@ -1218,6 +1627,7 @@ } ], "metadata": { + "anaconda-cloud": {}, "kernelspec": { "display_name": "Python 3", "language": "python", @@ -1233,9 +1643,9 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.5.1" + "version": "3.7.9" } }, "nbformat": 4, - "nbformat_minor": 0 + "nbformat_minor": 4 } diff --git a/setup.py b/setup.py old mode 100644 new mode 100755 index cc3d74a..597a890 --- a/setup.py +++ b/setup.py @@ -1,5 +1,7 @@ -from setuptools import setup, find_packages +from setuptools import setup # To use a consistent encoding +# python setup.py sdist bdist_wheel +# python -m twine upload dist/* from codecs import open from os import path @@ -9,17 +11,37 @@ with open(path.join(here, 'README.rst'), encoding='utf-8') as f: long_description = f.read() +requirements = [ + 'python-osc', +] + +setup_requirements = [ + 'pytest-runner', +] + +test_requirements = [ + 'pytest', +] + + setup( name='python-sonic', - version='0.1.4', + version='0.4.4', description='Programming Music with Sonic Pi or Supercollider', long_description=long_description, url='https://github.com/gkvoelkl/python-sonic', author='gkvoelkl', author_email='gkvoelkl@nelson-games.de', - + packages=[ + 'psonic', + 'psonic.samples', + 'psonic.samples.loops', + 'psonic.internals', + ], license='MIT', - + zip_safe=False, + include_package_data=True, + install_requires=requirements, classifiers=[ 'Development Status :: 4 - Beta', 'Intended Audience :: Developers', @@ -29,8 +51,11 @@ 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9' ], - keywords= [ 'music', 'sonic pi', @@ -41,18 +66,7 @@ 'supercollider', 'synthesis' ], - - #packages=find_packages(), - py_modules=['psonic'], - install_requires=['python-osc'], - - # To provide executable scripts, use entry points in preference to the - # "scripts" keyword. Entry points provide cross-platform support and allow - # pip to create the appropriate form of executable for the target platform. - #entry_points={ - # 'console_scripts': [ - # 'sample=sample:main', - # ], - #}, + test_suite='tests', + tests_require=test_requirements, + setup_requires=setup_requirements, ) - diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_psonic.py b/tests/test_psonic.py new file mode 100755 index 0000000..0cccef6 --- /dev/null +++ b/tests/test_psonic.py @@ -0,0 +1,78 @@ +import pytest +from psonic import * + +def test_originl_behaviour(): + notes_to_play = [72, G5, Fs5, Eb5] + (play(note) for note in notes_to_play) + sleep(0.5) + play(72, amp=2) + play(74, pan=-1) + use_synth(SAW) + use_synth(PROPHET) + play( + 60, attack=0.5, decay=1, sustain_level=0.4, + sustain=2, release=0.5 + ) + sample( + LOOP_AMEN, start=0.5, finish=0.8, + rate=-0.2, attack=0.3, release=1 + ) + sample( + DRUM_CYMBAL_OPEN, attack=0.01, + sustain=0.3, release=0.1 + ) + play([64, 67, 71], amp = 0.3) + play(chord(E4, MINOR7)) + play_pattern_timed( + scale( + C3, MAJOR_PENTATONIC, num_octaves = 2 + ), + 0.125, release = 0.1, + ) + synth(TRI, note=D4, amp=0.4) + detune = 0.7 + synth(SQUARE, note=E4+detune) + synth(GNOISE, release=0.5, amp=1, cutoff=100) + sc = Ring(scale(E3, MINOR_PENTATONIC)) + play(next(sc), release=0.1) + with Fx(SLICER): + synth(PROPHET, note=E2, release=8, cutoff=80) + synth(PROPHET, note=E2+4, release=8, cutoff=80) + run("""live_loop :foo do + use_real_time + a, b, c = sync "/osc/trigger/prophet" + synth :prophet, note: a, cutoff: b, sustain: c + end """) + send_message('/trigger/prophet', 70, 100, 8) + stop() + + +@pytest.mark.parametrize("root,quality,inversion,result", ( + (C4, MAJOR, None, [C4, E4, G4]), + (C4, MAJOR, 0, [C4, E4, G4]), + (C4, MAJOR, 1, [G3, C4, E4]), + (C4, MAJOR, 2, [E3, G3, C4]), + (C4, MAJOR, 3, [C4, E4, G4]), + (C4, MAJOR, 4, [G3, C4, E4]), + (C4, MAJOR7, 0, [C4, E4, G4, B4]), + (C4, MAJOR7, 1, [B3, C4, E4, G4]), + (C4, MAJOR7, 2, [G3, B3, C4, E4]), + (C4, MAJOR7, 3, [E3, G3, B3, C4]), + (C4, MAJOR7, 4, [C4, E4, G4, B4]), + (C4, MAJOR7, 5, [B3, C4, E4, G4]), +)) +def test_chord_inversions(root, quality, inversion, result): + assert chord(root, quality, inversion) == result + + +def test_imports(): + from psonic import( + SonicPi, + SonicPiNew, + ChordQuality, + Message, + Ring, + Fx, + Synth, + Sample, + )