Skip to content

Programming Music with Python, Sonic Pi and Supercollider

License

Notifications You must be signed in to change notification settings

gkvoelkl/python-sonic

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

64 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

python-sonic - Programming Music with Python, Sonic Pi or Supercollider

Python-Sonic is a simple Python interface for Sonic Pi, which is a real 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.

If you like it, use it. If you have some suggestions, tell me (gkvoelkl@nelson-games.de).

Installation

Or try

$ pip install python-sonic

That should work.

Limitations

  • You have to start Sonic Pi first before you can use it with 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

Communication

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.
For older Sonic Pi Version you have to set the ports explicitly
from psonic import *
set_server_parameter('127.0.0.1',4557,4559)

Examples

Many of the examples are inspired from the help menu in Sonic Pi.

from psonic import *

The first sound

play(70) #play MIDI note 70

Some more notes

play(72)
sleep(1)
play(75)
sleep(1)
play(79)

In more tratitional music notation

play(C5)
sleep(0.5)
play(D5)
sleep(0.5)
play(G5)

Play sharp notes like F# or dimished ones like Eb

play(Fs5)
sleep(0.5)
play(Eb5)

Play louder (parameter amp) or from a different direction (parameter pan)

play(72,amp=2)
sleep(0.5)
play(74,pan=-1) #left

Different synthesizer sounds

use_synth(SAW)
play(38)
sleep(0.25)
play(50)
sleep(0.5)
use_synth(PROPHET)
play(57)
sleep(0.25)

ADSR (Attack, Decay, Sustain and Release) Envelope

play (60, attack=0.5, decay=1, sustain_level=0.4, sustain=2, release=0.5)
sleep(4)

Play some samples

sample(AMBI_LUNAR_LAND, amp=0.5)
sample(LOOP_AMEN,pan=-1)
sleep(0.877)
sample(LOOP_AMEN,pan=1)
sample(LOOP_AMEN,rate=0.5)
sample(LOOP_AMEN,rate=1.5)
sample(LOOP_AMEN,rate=-1)#back
sample(DRUM_CYMBAL_OPEN,attack=0.01,sustain=0.3,release=0.1)
sample(LOOP_AMEN,start=0.5,finish=0.8,rate=-0.2,attack=0.3,release=1)

Play some random notes

import random

for i in range(5):
    play(random.randrange(50, 100))
    sleep(0.5)
for i in range(3):
    play(random.choice([C5,E5,G5]))
    sleep(1)

Sample slicing

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

while True:
  if one_in(2):
    sample(DRUM_HEAVY_KICK)
    sleep(0.5)
  else:
    sample(DRUM_CYMBAL_CLOSED)
    sleep(0.25)
---------------------------------------------------------------------------

KeyboardInterrupt                         Traceback (most recent call last)

<ipython-input-18-d8759ac2d27e> in <module>()
      5   else:
      6     sample(DRUM_CYMBAL_CLOSED)
----> 7     sleep(0.25)


/mnt/jupyter/python-sonic/psonic.py in sleep(duration)
    587     :return:
    588     """
--> 589     time.sleep(duration)
    590     _debug('sleep', duration)
    591


KeyboardInterrupt:

If you want to hear more than one sound at a time, use Threads.

import random
from psonic import *
from threading import Thread

def bass_sound():
    c = chord(E3, MAJOR7)
    while True:
        use_synth(PROPHET)
        play(random.choice(c), release=0.6)
        sleep(0.5)

def snare_sound():
    while True:
        sample(ELEC_SNARE)
        sleep(1)

bass_thread = Thread(target=bass_sound)
snare_thread = Thread(target=snare_sound)

bass_thread.start()
snare_thread.start()

while True:
    pass
---------------------------------------------------------------------------

KeyboardInterrupt                         Traceback (most recent call last)

<ipython-input-19-5b8671a783d6> in <module>
     22
     23 while True:
---> 24     pass


KeyboardInterrupt:

Every function bass_sound and snare_sound have its own thread. Your can hear them running.

from psonic import *
from threading import Thread, Condition
from random import choice

def random_riff(condition):
    use_synth(PROPHET)
    sc = scale(E3, MINOR)
    while True:
        s = random.choice([0.125,0.25,0.5])
        with condition:
            condition.wait() #Wait for message
        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)

def drums(condition):
    while True:
        with condition:
            condition.notifyAll() #Message to threads
        for i in range(16):
            r = random.randrange(1,10)
            sample(DRUM_BASS_HARD, rate=r)
            sleep(0.125)

condition = Condition()
random_riff_thread = Thread(name='consumer1', target=random_riff, args=(condition,))
drums_thread = Thread(name='producer', target=drums, args=(condition,))

random_riff_thread.start()
drums_thread.start()

input("Press Enter to continue...")
Press Enter to continue...
''

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.

More simple with decorator __@in_thread__

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...")
Press Enter to continue...
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

from psonic import *

play ([64, 67, 71], amp = 0.3)
sleep(1)
play ([E4, G4, B4])
sleep(1)

Play chords

play(chord(E4, MINOR))
sleep(1)
play(chord(E4, MAJOR))
sleep(1)
play(chord(E4, MINOR7))
sleep(1)
play(chord(E4, DOM7))
sleep(1)

Play arpeggios

play_pattern( chord(E4, 'm7'))
play_pattern_timed( chord(E4, 'm7'), 0.25)
play_pattern_timed(chord(E4, 'dim'), [0.25, 0.5])

Play scales

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.

import random
from psonic import *

s = scale(C3, MAJOR)
s
[48, 50, 52, 53, 55, 57, 59, 60]
s.reverse()
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, too.

from psonic import *
from threading import Thread

def my_loop():
  play(60)
  sleep(1)

def looper():
  while True:
    my_loop()

looper_thread = Thread(name='looper', target=looper)

looper_thread.start()

input("Press Enter to continue...")
Press Enter to continue...Y
'Y'

Now change the function my_loop und you can hear it.

def my_loop():
  use_synth(TB303)
  play (60, release= 0.3)
  sleep (0.25)
def my_loop():
  use_synth(TB303)
  play (chord(E3, MINOR), release= 0.3)
  sleep(0.5)
def my_loop():
    use_synth(TB303)
    sample(DRUM_BASS_HARD, rate = random.uniform(0.5, 2))
    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 –> Restart

Now with two live loops which are synch.

from psonic import *
from threading import Thread, Condition
from random import choice

def loop_foo():
  play (E4, release = 0.5)
  sleep (0.5)


def loop_bar():
  sample (DRUM_SNARE_SOFT)
  sleep (1)


def live_loop_1(condition):
    while True:
        with condition:
            condition.notifyAll() #Message to threads
        loop_foo()

def live_loop_2(condition):
    while True:
        with condition:
            condition.wait() #Wait for message
        loop_bar()

condition = Condition()
live_thread_1 = Thread(name='producer', target=live_loop_1, args=(condition,))
live_thread_2 = Thread(name='consumer1', target=live_loop_2, args=(condition,))

live_thread_1.start()
live_thread_2.start()

input("Press Enter to continue...")
Press Enter to continue...y
'y'
def loop_foo():
  play (A4, release = 0.5)
  sleep (0.5)
def loop_bar():
  sample (DRUM_HEAVY_KICK)
  sleep (0.125)

If would be nice if we can stop the loop with a simple command. With stop event it works.

from psonic import *
from threading import Thread, Condition, Event

def loop_foo():
  play (E4, release = 0.5)
  sleep (0.5)


def loop_bar():
  sample (DRUM_SNARE_SOFT)
  sleep (1)


def live_loop_1(condition,stop_event):
    while not stop_event.is_set():
        with condition:
            condition.notifyAll() #Message to threads
        loop_foo()

def live_loop_2(condition,stop_event):
    while not stop_event.is_set():
        with condition:
            condition.wait() #Wait for message
        loop_bar()



condition = Condition()
stop_event = Event()
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_1.start()
live_thread_2.start()

input("Press Enter to continue...")
Press Enter to continue...y
'y'
stop_event.set()

More complex live loops

sc = Ring(scale(E3, MINOR_PENTATONIC))

def loop_foo():
  play (next(sc), release= 0.1)
  sleep (0.125)

sc2 = Ring(scale(E3,MINOR_PENTATONIC,num_octaves=2))

def loop_bar():
  use_synth(DSAW)
  play (next(sc2), release= 0.25)
  sleep (0.25)

Now a simple structure with four live loops

import random
from psonic import *
from threading import Thread, Condition, Event

def live_1():
    pass

def live_2():
    pass

def live_3():
    pass

def live_4():
    pass

def live_loop_1(condition,stop_event):
    while not stop_event.is_set():
        with condition:
            condition.notifyAll() #Message to threads
        live_1()

def live_loop_2(condition,stop_event):
    while not stop_event.is_set():
        with condition:
            condition.wait() #Wait for message
        live_2()

def live_loop_3(condition,stop_event):
    while not stop_event.is_set():
        with condition:
            condition.wait() #Wait for message
        live_3()

def live_loop_4(condition,stop_event):
    while not stop_event.is_set():
        with condition:
            condition.wait() #Wait for message
        live_4()

condition = Condition()
stop_event = Event()
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_3, args=(condition,stop_event))

live_thread_1.start()
live_thread_2.start()
live_thread_3.start()
live_thread_4.start()

input("Press Enter to continue...")
Press Enter to continue...y
'y'

After starting the loops you can change them

def live_1():
    sample(BD_HAUS,amp=2)
    sleep(0.5)
    pass
def live_2():
    #sample(AMBI_CHOIR, rate=0.4)
    #sleep(1)
    pass
def live_3():
    use_synth(TB303)
    play(E2, release=4,cutoff=120,cutoff_attack=1)
    sleep(4)
def live_4():
    notes = scale(E3, MINOR_PENTATONIC, num_octaves=2)
    for i in range(8):
        play(random.choice(notes),release=0.1,amp=1.5)
        sleep(0.125)

And stop.

stop_event.set()

Creating Sound

from psonic import *

synth(SINE, note=D4)
synth(SQUARE, note=D4)
synth(TRI, note=D4, amp=0.4)
detune = 0.7
synth(SQUARE, note = E4)
synth(SQUARE, note = E4+detune)
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

from psonic import *

with Fx(SLICER):
    synth(PROPHET,note=E2,release=8,cutoff=80)
    synth(PROPHET,note=E2+4,release=8,cutoff=80)
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.

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.

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.

send_message('/trigger/prophet', 70, 100, 8)
stop()

Recording

With python-sonic you can record wave files.

from psonic import *
# 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)
# stop recording
stop_recording
<function psonic.psonic.stop_recording()>
# save file
save_recording('/Volumes/jupyter/python-sonic/test.wav')

More Examples

from psonic import *
#Inspired by Steve Reich Clapping Music

clapping = [1, 1, 1, 0, 1, 1, 0, 1, 0, 1, 1, 0]

for i in range(13):
    for j in range(4):
        for k in range(12):
          if clapping[k] ==1 : sample(DRUM_SNARE_SOFT,pan=-0.5)
          if clapping[(i+k)%12] == 1: sample(DRUM_HEAVY_KICK,pan=0.5)
          sleep (0.25)

Projects that use Python-Sonic

Raspberry Pi sonic-track.py a Sonic-pi Motion Track Demo https://github.com/pageauc/sonic-track

Sources

Joe Armstrong: Connecting Erlang to the Sonic Pi http://joearms.github.io/2015/01/05/Connecting-Erlang-to-Sonic-Pi.html

Joe Armstrong: Controlling Sound with OSC Messages http://joearms.github.io/2016/01/29/Controlling-Sound-with-OSC-Messages.html

About

Programming Music with Python, Sonic Pi and Supercollider

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published