diff --git a/README.rst b/README.rst index a45ccd9..1fa44ea 100755 --- a/README.rst +++ b/README.rst @@ -1,4 +1,3 @@ - python-sonic - Programming Music with Python, Sonic Pi or Supercollider ======================================================================= @@ -8,65 +7,121 @@ 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.5 - 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. -$ pip install python-sonic +For local development you might want to locally install using -That should work. +$ pip install -e . Limitations ----------- - You have to start *Sonic Pi* first before you can use it with python-sonic +- Only the notes from C2 to C6 Changelog --------- -+------------+---------------------------------------------------------------+ -| Version | | -+============+===============================================================+ -| 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 | -+------------+---------------------------------------------------------------+ ++--------+-------------------------------------------------------------+ +| 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:: ipython3 +.. code-block:: python from psonic import * The first sound -.. code:: ipython3 +.. code-block:: python play(70) #play MIDI note 70 Some more notes -.. code:: ipython3 +.. code-block:: python play(72) sleep(1) @@ -74,9 +129,9 @@ Some more notes sleep(1) play(79) -In more traditional music notation +In more tratitional music notation -.. code:: ipython3 +.. code-block:: python play(C5) sleep(0.5) @@ -86,7 +141,7 @@ In more traditional music notation Play sharp notes like *F#* or dimished ones like *Eb* -.. code:: ipython3 +.. code-block:: python play(Fs5) sleep(0.5) @@ -95,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:: ipython3 +.. code-block:: python play(72,amp=2) sleep(0.5) @@ -103,7 +158,7 @@ pan) Different synthesizer sounds -.. code:: ipython3 +.. code-block:: python use_synth(SAW) play(38) @@ -116,46 +171,46 @@ Different synthesizer sounds ADSR *(Attack, Decay, Sustain and Release)* Envelope -.. code:: ipython3 +.. 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:: ipython3 +.. code-block:: python sample(AMBI_LUNAR_LAND, amp=0.5) -.. code:: ipython3 +.. code-block:: python sample(LOOP_AMEN,pan=-1) sleep(0.877) sample(LOOP_AMEN,pan=1) -.. code:: ipython3 +.. code-block:: python sample(LOOP_AMEN,rate=0.5) -.. code:: ipython3 +.. code-block:: python sample(LOOP_AMEN,rate=1.5) -.. code:: ipython3 +.. code-block:: python sample(LOOP_AMEN,rate=-1)#back -.. code:: ipython3 +.. code-block:: python sample(DRUM_CYMBAL_OPEN,attack=0.01,sustain=0.3,release=0.1) -.. code:: ipython3 +.. 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:: ipython3 +.. code-block:: python import random @@ -163,7 +218,7 @@ Play some random notes play(random.randrange(50, 100)) sleep(0.5) -.. code:: ipython3 +.. code-block:: python for i in range(3): play(random.choice([C5,E5,G5])) @@ -171,7 +226,7 @@ Play some random notes Sample slicing -.. code:: ipython3 +.. code-block:: python from psonic import * @@ -185,7 +240,7 @@ Sample slicing An infinite loop and if -.. code:: ipython3 +.. code-block:: python while True: if one_in(2): @@ -222,7 +277,7 @@ An infinite loop and if If you want to hear more than one sound at a time, use Threads. -.. code:: ipython3 +.. code-block:: python import random from psonic import * @@ -249,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:: ipython3 +:: + + + --------------------------------------------------------------------------- + + 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 @@ -293,7 +365,7 @@ Your can hear them running. .. parsed-literal:: - Press Enter to continue... + Press Enter to continue... @@ -309,9 +381,9 @@ 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\_\_ +More simple with decorator \_\_@in_thread_\_ -.. code:: ipython3 +.. code-block:: python from psonic import * from random import choice @@ -349,18 +421,10 @@ More simple with decorator \_\_@in\_thread\_\_ .. parsed-literal:: - Press Enter to continue... - - - - -.. parsed-literal:: - - '' + Press Enter to continue... - -.. code:: ipython3 +.. code-block:: python from psonic import * @@ -386,7 +450,7 @@ More simple with decorator \_\_@in\_thread\_\_ Play a list of notes -.. code:: ipython3 +.. code-block:: python from psonic import * @@ -397,7 +461,7 @@ Play a list of notes Play chords -.. code:: ipython3 +.. code-block:: python play(chord(E4, MINOR)) sleep(1) @@ -408,22 +472,9 @@ Play chords play(chord(E4, DOM7)) sleep(1) -Play chords with inversions - -.. code:: ipython3 - - play(chord(E4, MAJOR)) - sleep(1) - play(chord(E4, MAJOR, inversion=1)) - sleep(1) - play(chord(E4, MAJOR, 2)) - sleep(1) - play(chord(E3, MAJOR)) - sleep(1) - Play arpeggios -.. code:: ipython3 +.. code-block:: python play_pattern( chord(E4, 'm7')) play_pattern_timed( chord(E4, 'm7'), 0.25) @@ -431,7 +482,7 @@ Play arpeggios Play scales -.. code:: ipython3 +.. 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) @@ -441,7 +492,7 @@ 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:: ipython3 +.. code-block:: python import random from psonic import * @@ -458,11 +509,11 @@ descending or shuffeld. -.. code:: ipython3 +.. code-block:: python s.reverse() -.. code:: ipython3 +.. code-block:: python play_pattern_timed(s, 0.125, release = 0.1) @@ -473,10 +524,10 @@ 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:: ipython3 +.. code-block:: python from psonic import * from threading import Thread @@ -509,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:: ipython3 +.. code-block:: python def my_loop(): use_synth(TB303) play (60, release= 0.3) sleep (0.25) -.. code:: ipython3 +.. code-block:: python def my_loop(): use_synth(TB303) play (chord(E3, MINOR), release= 0.3) sleep(0.5) -.. code:: ipython3 +.. code-block:: python def my_loop(): use_synth(TB303) @@ -533,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:: ipython3 +.. code-block:: python from psonic import * from threading import Thread, Condition @@ -589,13 +640,13 @@ Now with two live loops which are synch. -.. code:: ipython3 +.. code-block:: python def loop_foo(): play (A4, release = 0.5) sleep (0.5) -.. code:: ipython3 +.. code-block:: python def loop_bar(): sample (DRUM_HEAVY_KICK) @@ -604,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:: ipython3 +.. code-block:: python from psonic import * from threading import Thread, Condition, Event @@ -658,13 +709,13 @@ stop event it works. -.. code:: ipython3 +.. code-block:: python stop_event.set() More complex live loops -.. code:: ipython3 +.. code-block:: python sc = Ring(scale(E3, MINOR_PENTATONIC)) @@ -681,7 +732,7 @@ More complex live loops Now a simple structure with four live loops -.. code:: ipython3 +.. code-block:: python import random from psonic import * @@ -728,7 +779,7 @@ Now a simple structure with four live loops live_thread_1 = Thread(name='producer', target=live_loop_1, args=(condition,stop_event)) live_thread_2 = Thread(name='consumer1', target=live_loop_2, args=(condition,stop_event)) live_thread_3 = Thread(name='consumer2', target=live_loop_3, args=(condition,stop_event)) - live_thread_4 = Thread(name='consumer3', target=live_loop_4, args=(condition,stop_event)) + live_thread_4 = Thread(name='consumer3', target=live_loop_3, args=(condition,stop_event)) live_thread_1.start() live_thread_2.start() @@ -753,28 +804,28 @@ Now a simple structure with four live loops After starting the loops you can change them -.. code:: ipython3 +.. code-block:: python def live_1(): sample(BD_HAUS,amp=2) sleep(0.5) pass -.. code:: ipython3 +.. code-block:: python def live_2(): - sample(AMBI_CHOIR, rate=0.4) - sleep(1) + #sample(AMBI_CHOIR, rate=0.4) + #sleep(1) pass -.. code:: ipython3 +.. code-block:: python def live_3(): use_synth(TB303) play(E2, release=4,cutoff=120,cutoff_attack=1) sleep(4) -.. code:: ipython3 +.. code-block:: python def live_4(): notes = scale(E3, MINOR_PENTATONIC, num_octaves=2) @@ -784,14 +835,14 @@ After starting the loops you can change them And stop. -.. code:: ipython3 +.. code-block:: python stop_event.set() Creating Sound ~~~~~~~~~~~~~~ -.. code:: ipython3 +.. code-block:: python from psonic import * @@ -799,13 +850,13 @@ Creating Sound synth(SQUARE, note=D4) synth(TRI, note=D4, amp=0.4) -.. code:: ipython3 +.. code-block:: python detune = 0.7 synth(SQUARE, note = E4) synth(SQUARE, note = E4+detune) -.. code:: ipython3 +.. code-block:: python detune=0.1 # Amplitude shaping synth(SQUARE, note = E2, release = 2) @@ -819,7 +870,7 @@ Next Step Using FX *Not implemented yet* -.. code:: ipython3 +.. code-block:: python from psonic import * @@ -827,7 +878,7 @@ Using FX *Not implemented yet* synth(PROPHET,note=E2,release=8,cutoff=80) synth(PROPHET,note=E2+4,release=8,cutoff=80) -.. code:: ipython3 +.. code-block:: python with Fx(SLICER, phase=0.125, probability=0.6,prob_pos=1): synth(TB303, note=E2, cutoff_attack=8, release=8) @@ -839,39 +890,81 @@ OSC Communication (Sonic Pi Ver. 3.x or better) In Sonic Pi version 3 or better you can work with messages. -.. code:: ipython3 +.. 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:: ipython3 +.. code-block:: python run("""live_loop :foo do use_real_time - a, b, c = sync "/osc/trigger/prophet" + a, b, c = sync "/osc*/trigger/prophet" synth :prophet, note: a, cutoff: b, sustain: c end """) Now send a message to Sonic Pi. -.. code:: ipython3 +.. code-block:: python send_message('/trigger/prophet', 70, 100, 8) -.. code:: ipython3 +.. 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:: ipython3 +.. code-block:: python from psonic import * -.. code:: ipython3 +.. code-block:: python #Inspired by Steve Reich Clapping Music diff --git a/psonic/notes.py b/psonic/notes.py old mode 100644 new mode 100755 diff --git a/psonic/psonic.py b/psonic/psonic.py index 94d6057..5606525 100755 --- a/psonic/psonic.py +++ b/psonic/psonic.py @@ -1,3 +1,5 @@ +import os +import re import random from .samples import Sample from .synthesizers import SAW @@ -167,8 +169,47 @@ 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) diff --git a/psonic/synth_server.py b/psonic/synth_server.py index e52b6f5..732a104 100755 --- a/psonic/synth_server.py +++ b/psonic/synth_server.py @@ -19,6 +19,15 @@ 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 @@ -28,30 +37,58 @@ def sleep(self, 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_OSC_MESSAGE = 4559 - GUI_ID = 'SONIC_PI_PYTHON' + #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.udp_ip, + self.udp_port ) self.client_for_messages = udp_client.UDPClient( - self.UDP_IP, - self.UDP_PORT_OSC_MESSAGE + 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 = 'use_synth :{0}\n'.format(_current_synth.name) + command + command = f'use_synth :{_current_synth.name}\n{command}' self.send(command) def synth(self, command): @@ -60,6 +97,16 @@ def synth(self, 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) @@ -73,7 +120,9 @@ def test_connection(self): def send_command(self, address, argument=''): msg = osc_message_builder.OscMessageBuilder(address=address) - msg.add_arg('SONIC_PI_PYTHON') + 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() 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 index 8aa4d2d..f5bd8d0 100755 --- a/python-sonic.ipynb +++ b/python-sonic.ipynb @@ -39,9 +39,7 @@ }, { "cell_type": "raw", - "metadata": { - "collapsed": true - }, + "metadata": {}, "source": [ "$ pip install python-sonic" ] @@ -84,7 +82,31 @@ "| 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 |" + "| 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)" ] }, { @@ -103,10 +125,8 @@ }, { "cell_type": "code", - "execution_count": 4, - "metadata": { - "collapsed": true - }, + "execution_count": 1, + "metadata": {}, "outputs": [], "source": [ "from psonic import *" @@ -121,10 +141,8 @@ }, { "cell_type": "code", - "execution_count": 5, - "metadata": { - "collapsed": true - }, + "execution_count": 2, + "metadata": {}, "outputs": [], "source": [ "play(70) #play MIDI note 70" @@ -139,10 +157,8 @@ }, { "cell_type": "code", - "execution_count": 10, - "metadata": { - "collapsed": true - }, + "execution_count": 3, + "metadata": {}, "outputs": [], "source": [ "play(72)\n", @@ -161,10 +177,8 @@ }, { "cell_type": "code", - "execution_count": 11, - "metadata": { - "collapsed": true - }, + "execution_count": 4, + "metadata": {}, "outputs": [], "source": [ "play(C5)\n", @@ -184,9 +198,7 @@ { "cell_type": "code", "execution_count": 5, - "metadata": { - "collapsed": true - }, + "metadata": {}, "outputs": [], "source": [ "play(Fs5)\n", @@ -204,9 +216,7 @@ { "cell_type": "code", "execution_count": 6, - "metadata": { - "collapsed": true - }, + "metadata": {}, "outputs": [], "source": [ "play(72,amp=2)\n", @@ -223,10 +233,8 @@ }, { "cell_type": "code", - "execution_count": 12, - "metadata": { - "collapsed": true - }, + "execution_count": 7, + "metadata": {}, "outputs": [], "source": [ "use_synth(SAW)\n", @@ -248,10 +256,8 @@ }, { "cell_type": "code", - "execution_count": 13, - "metadata": { - "collapsed": true - }, + "execution_count": 8, + "metadata": {}, "outputs": [], "source": [ "play (60, attack=0.5, decay=1, sustain_level=0.4, sustain=2, release=0.5) \n", @@ -268,9 +274,7 @@ { "cell_type": "code", "execution_count": 9, - "metadata": { - "collapsed": true - }, + "metadata": {}, "outputs": [], "source": [ "sample(AMBI_LUNAR_LAND, amp=0.5)" @@ -278,10 +282,8 @@ }, { "cell_type": "code", - "execution_count": 4, - "metadata": { - "collapsed": true - }, + "execution_count": 10, + "metadata": {}, "outputs": [], "source": [ "sample(LOOP_AMEN,pan=-1)\n", @@ -292,9 +294,7 @@ { "cell_type": "code", "execution_count": 11, - "metadata": { - "collapsed": true - }, + "metadata": {}, "outputs": [], "source": [ "sample(LOOP_AMEN,rate=0.5)" @@ -303,9 +303,7 @@ { "cell_type": "code", "execution_count": 12, - "metadata": { - "collapsed": true - }, + "metadata": {}, "outputs": [], "source": [ "sample(LOOP_AMEN,rate=1.5)" @@ -314,9 +312,7 @@ { "cell_type": "code", "execution_count": 13, - "metadata": { - "collapsed": true - }, + "metadata": {}, "outputs": [], "source": [ "sample(LOOP_AMEN,rate=-1)#back" @@ -325,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)" @@ -336,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)" @@ -354,9 +346,7 @@ { "cell_type": "code", "execution_count": 16, - "metadata": { - "collapsed": true - }, + "metadata": {}, "outputs": [], "source": [ "import random\n", @@ -369,9 +359,7 @@ { "cell_type": "code", "execution_count": 17, - "metadata": { - "collapsed": true - }, + "metadata": {}, "outputs": [], "source": [ "for i in range(3):\n", @@ -388,10 +376,8 @@ }, { "cell_type": "code", - "execution_count": 5, - "metadata": { - "collapsed": true - }, + "execution_count": 18, + "metadata": {}, "outputs": [], "source": [ "from psonic import *\n", @@ -449,11 +435,21 @@ }, { "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "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", @@ -494,10 +490,10 @@ "metadata": {}, "outputs": [ { - "name": "stdout", + "name": "stdin", "output_type": "stream", "text": [ - "Press Enter to continue...\n" + "Press Enter to continue... \n" ] }, { @@ -565,25 +561,15 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": null, "metadata": {}, "outputs": [ { - "name": "stdout", + "name": "stdin", "output_type": "stream", "text": [ - "Press Enter to continue...\n" + "Press Enter to continue... \n" ] - }, - { - "data": { - "text/plain": [ - "''" - ] - }, - "execution_count": 6, - "metadata": {}, - "output_type": "execute_result" } ], "source": [ @@ -624,9 +610,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "collapsed": true - }, + "metadata": {}, "outputs": [], "source": [ "from psonic import *\n", @@ -661,10 +645,8 @@ }, { "cell_type": "code", - "execution_count": 1, - "metadata": { - "collapsed": true - }, + "execution_count": 2, + "metadata": {}, "outputs": [], "source": [ "from psonic import *\n", @@ -685,9 +667,7 @@ { "cell_type": "code", "execution_count": 2, - "metadata": { - "collapsed": true - }, + "metadata": {}, "outputs": [], "source": [ "play(chord(E4, MINOR)) \n", @@ -710,9 +690,7 @@ { "cell_type": "code", "execution_count": 3, - "metadata": { - "collapsed": true - }, + "metadata": {}, "outputs": [], "source": [ "play_pattern( chord(E4, 'm7')) \n", @@ -729,10 +707,8 @@ }, { "cell_type": "code", - "execution_count": 5, - "metadata": { - "collapsed": true - }, + "execution_count": 4, + "metadata": {}, "outputs": [], "source": [ "play_pattern_timed(scale(C3, MAJOR), 0.125, release = 0.1) \n", @@ -749,7 +725,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 5, "metadata": {}, "outputs": [ { @@ -758,7 +734,7 @@ "[48, 50, 52, 53, 55, 57, 59, 60]" ] }, - "execution_count": 9, + "execution_count": 5, "metadata": {}, "output_type": "execute_result" } @@ -773,10 +749,8 @@ }, { "cell_type": "code", - "execution_count": 10, - "metadata": { - "collapsed": true - }, + "execution_count": 6, + "metadata": {}, "outputs": [], "source": [ "s.reverse()" @@ -784,10 +758,8 @@ }, { "cell_type": "code", - "execution_count": 11, - "metadata": { - "collapsed": true - }, + "execution_count": 7, + "metadata": {}, "outputs": [], "source": [ "\n", @@ -863,7 +835,10 @@ "cell_type": "code", "execution_count": 6, "metadata": { - "collapsed": true + "collapsed": true, + "jupyter": { + "outputs_hidden": true + } }, "outputs": [], "source": [ @@ -877,7 +852,10 @@ "cell_type": "code", "execution_count": 7, "metadata": { - "collapsed": true + "collapsed": true, + "jupyter": { + "outputs_hidden": true + } }, "outputs": [], "source": [ @@ -891,7 +869,10 @@ "cell_type": "code", "execution_count": 8, "metadata": { - "collapsed": true + "collapsed": true, + "jupyter": { + "outputs_hidden": true + } }, "outputs": [], "source": [ @@ -980,7 +961,10 @@ "cell_type": "code", "execution_count": 11, "metadata": { - "collapsed": true + "collapsed": true, + "jupyter": { + "outputs_hidden": true + } }, "outputs": [], "source": [ @@ -993,7 +977,10 @@ "cell_type": "code", "execution_count": 14, "metadata": { - "collapsed": true + "collapsed": true, + "jupyter": { + "outputs_hidden": true + } }, "outputs": [], "source": [ @@ -1076,7 +1063,10 @@ "cell_type": "code", "execution_count": 3, "metadata": { - "collapsed": true + "collapsed": true, + "jupyter": { + "outputs_hidden": true + } }, "outputs": [], "source": [ @@ -1085,9 +1075,7 @@ }, { "cell_type": "markdown", - "metadata": { - "collapsed": true - }, + "metadata": {}, "source": [ "More complex live loops" ] @@ -1096,7 +1084,10 @@ "cell_type": "code", "execution_count": 4, "metadata": { - "collapsed": true + "collapsed": true, + "jupyter": { + "outputs_hidden": true + } }, "outputs": [], "source": [ @@ -1211,7 +1202,10 @@ "cell_type": "code", "execution_count": 12, "metadata": { - "collapsed": true + "collapsed": true, + "jupyter": { + "outputs_hidden": true + } }, "outputs": [], "source": [ @@ -1225,7 +1219,10 @@ "cell_type": "code", "execution_count": 7, "metadata": { - "collapsed": true + "collapsed": true, + "jupyter": { + "outputs_hidden": true + } }, "outputs": [], "source": [ @@ -1239,7 +1236,10 @@ "cell_type": "code", "execution_count": 14, "metadata": { - "collapsed": true + "collapsed": true, + "jupyter": { + "outputs_hidden": true + } }, "outputs": [], "source": [ @@ -1253,7 +1253,10 @@ "cell_type": "code", "execution_count": 13, "metadata": { - "collapsed": true + "collapsed": true, + "jupyter": { + "outputs_hidden": true + } }, "outputs": [], "source": [ @@ -1275,7 +1278,10 @@ "cell_type": "code", "execution_count": 15, "metadata": { - "collapsed": true + "collapsed": true, + "jupyter": { + "outputs_hidden": true + } }, "outputs": [], "source": [ @@ -1294,6 +1300,9 @@ "execution_count": 4, "metadata": { "collapsed": true, + "jupyter": { + "outputs_hidden": true + }, "scrolled": true }, "outputs": [], @@ -1309,7 +1318,10 @@ "cell_type": "code", "execution_count": 5, "metadata": { - "collapsed": true + "collapsed": true, + "jupyter": { + "outputs_hidden": true + } }, "outputs": [], "source": [ @@ -1322,7 +1334,10 @@ "cell_type": "code", "execution_count": 12, "metadata": { - "collapsed": true + "collapsed": true, + "jupyter": { + "outputs_hidden": true + } }, "outputs": [], "source": [ @@ -1352,7 +1367,10 @@ "cell_type": "code", "execution_count": 2, "metadata": { - "collapsed": true + "collapsed": true, + "jupyter": { + "outputs_hidden": true + } }, "outputs": [], "source": [ @@ -1367,7 +1385,10 @@ "cell_type": "code", "execution_count": 3, "metadata": { - "collapsed": true + "collapsed": true, + "jupyter": { + "outputs_hidden": true + } }, "outputs": [], "source": [ @@ -1394,9 +1415,7 @@ { "cell_type": "code", "execution_count": 1, - "metadata": { - "collapsed": true - }, + "metadata": {}, "outputs": [], "source": [ "from psonic import *" @@ -1411,15 +1430,13 @@ }, { "cell_type": "code", - "execution_count": 3, - "metadata": { - "collapsed": true - }, + "execution_count": 2, + "metadata": {}, "outputs": [], "source": [ "run(\"\"\"live_loop :foo do\n", " use_real_time\n", - " a, b, c = sync \"/osc/trigger/prophet\"\n", + " a, b, c = sync \"/osc*/trigger/prophet\"\n", " synth :prophet, note: a, cutoff: b, sustain: c\n", "end \"\"\")" ] @@ -1433,10 +1450,8 @@ }, { "cell_type": "code", - "execution_count": 4, - "metadata": { - "collapsed": true - }, + "execution_count": 3, + "metadata": {}, "outputs": [], "source": [ "send_message('/trigger/prophet', 70, 100, 8)" @@ -1444,15 +1459,86 @@ }, { "cell_type": "code", - "execution_count": 5, - "metadata": { - "collapsed": true - }, + "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": {}, @@ -1464,7 +1550,10 @@ "cell_type": "code", "execution_count": 1, "metadata": { - "collapsed": true + "collapsed": true, + "jupyter": { + "outputs_hidden": true + } }, "outputs": [], "source": [ @@ -1475,7 +1564,10 @@ "cell_type": "code", "execution_count": null, "metadata": { - "collapsed": true + "collapsed": true, + "jupyter": { + "outputs_hidden": true + } }, "outputs": [], "source": [ @@ -1507,9 +1599,7 @@ }, { "cell_type": "markdown", - "metadata": { - "collapsed": true - }, + "metadata": {}, "source": [ "## Sources" ] @@ -1553,9 +1643,9 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.6.4" + "version": "3.7.9" } }, "nbformat": 4, - "nbformat_minor": 1 + "nbformat_minor": 4 } diff --git a/setup.py b/setup.py old mode 100644 new mode 100755 index a43b2f1..597a890 --- a/setup.py +++ b/setup.py @@ -1,5 +1,7 @@ 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 @@ -24,7 +26,7 @@ setup( name='python-sonic', - version='0.3.2', + version='0.4.4', description='Programming Music with Sonic Pi or Supercollider', long_description=long_description, url='https://github.com/gkvoelkl/python-sonic', @@ -50,6 +52,9 @@ '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', diff --git a/tests/test_psonic.py b/tests/test_psonic.py old mode 100644 new mode 100755