Live coding using SC3 and scikit-learn
This blog post is about machine learning techniques in live coding. I particularly focused on SuperCollider (SC3) and scikit-learn library for Python3. The main procedure was to send data over Open Sound Control (OSC) protocol, using pythonosc, and to analyze the data in Python3. The data analysis results are sending back to sclang in real-time for parameter control of UGens.
Description and order of execution
The main idea is to get input of environmental sounds using a microphone (also input from speakers works fine), and to analyze the input based on Chromagram
class from SC3. The next step is to send the acoustical features to python via OSC. The python script chroma-server.py
is responsible to receive the data and to make the data analysis. This is done by writing the raw data in the file data.txt
. The data analysis is based on IncrementalPCA algorithm. When the analysis is finished chroma-server.py
appends the results in data-pca.txt
, and calls the client-parasite.py
script which sends the data back to sclang.
The script client-parasite.py
is in the folder ./lib
. The python home directory looks as follows:
$ tree -L 2
.
├── chroma-server.py
├── data-pca.txt
├── data.txt
└── lib
├── __init.py__
├── __pycache__
└── client_parasite.py
2 directories, 5 files
-
*NOTES*:
init.py
is an empty file, and__pycache__
is generated by python.- Make sure to delete
data.txt
anddata-pca.txt
every time you restart the script.
-
*Order of execution*:
- Run the script
chroma-server.py
- Run the code chunks in SC3
IMPORTANT NOTICE: For applying Incremental PCA we have to keep records of the full data set. As such, the 'data.txt' is getting bigger and bigger. Than makes the computations CPU intensive. Pay attention to Python CPU percentage!
sclang
(
SynthDef(\chroma,{
var in, fft, chroma, array;
in = SoundIn.ar(0); // get sound from mic
fft = FFT(LocalBuf(2048), in);
chroma = Chromagram.kr(fft);
//chroma.poll(1);
//in
12 do: { |i|
SendTrig.kr(Impulse.kr(2), i, chroma[i])
};
}).add;
)
// send to python
(
b = NetAddr.new("127.0.0.1", 5005); // create the NetAddr
o = OSCFunc({ arg msg, time;
b.sendMsg("/chroma", msg[2].asString, msg[3].asString);
//[msg[2], msg[3]].postln;
},'/tr', s.addr);
)
o.free;
x = Synth(\chroma) // start sending the synth data
x.free
thisProcess.openUDPPort(7007); // open port 7007 to receive from client-parasite.py
thisProcess.openPorts; // list all open ports
// start the sound to collect some data
Ndef(\brg).fadeTime_(4)
(
Ndef('brg', { |pc1=2 pc2=2 pc3=3 pc4=4|
FreeVerb.ar(
Ringz.ar(
TGrains.ar(2, LFPulse.ar(pc1/pc3), Buffer.read(s, Platform.resourceDir ++ "/sounds/a11wlk01.wav"), Sweep.ar( x = LFPulse.ar(pc2/pc4)), x, Sweep.ar(x, pc3/pc4)
),
1390, 0.004
),
0.24, 0.11, 0.12
)
}).play
)
// get the results of the analysis and control Ndef parameters
(
t = OSCFunc( { |msg, time, addr, recvPort|
var pca_data;
pca_data = clump(msg.asString.findRegexp("[0-9]+\.[0-9]+") collect: { |i| i[1] }, 4); // 4 components
pca_data.postln;
Ndef(\brg).set(\pc1, pca_data[1][0].interpret, \pc2, pca_data[1][1].interpret, \pc3, pca_data[1][2].interpret, \pc4, pca_data[1][3].interpret);
}, '/components');
)
t.free;
Ndef(\brg).clear(2)
python 3
chroma-server.py
"""
Live coding session with machine learning.
The script receives Chromagram data from SC3 and writes the raw data from onsets analysis to 'data.txt', and then it writes a file called 'data-pca.txt' with the iPCA results
"""
import argparse
import math
import lib.client_parasite
import numpy as np
#np.set_printoptions(threshold=np.nan)
from sklearn.decomposition import IncrementalPCA
from scipy import stats
from pythonosc import dispatcher
from pythonosc import osc_server
def fw_pca(explained_variance, singular_values):
"""
Write the results of the IPCA in data-pca.txt file
"""
f = open('data-pca.txt', 'a')
data_array = explained_variance + '\n' + singular_values + '\n'
f.write(data_array)
f.close()
def pca(fname):
"""
Perform IPCA on the features extracted from Chromagram in SC3
"""
d = {}
for x in range(0,12):
d["val{0}".format(x)] = 0
data_array = []
with open(fname) as f:
content = f.readlines()
content = [x.strip() for x in content]
#print('CONTENT: ', content)
#print('CONTENT_SIZE: ', len(content))
for elem in content:
for i in range(0,12):
if elem[0] == str(i) and elem[1] == ':':
d['val'+str(i)] = float(elem.lstrip(str(i)+':').strip())
elif elem[1] == '0':
d['val10'] = float(elem.lstrip('10:').strip())
elif elem[1] == '1':
d['val11'] = float(elem.lstrip('11:').strip())
data_array.append([d['val0'], d['val1'], d['val2'], d['val3'], d['val4'], d['val5'], d['val6'], d['val7'], d['val8'], d['val9'], d['val10'], d['val11']])
#print('DATA_ARRAY_SIZE: ', len(data_array))
try:
X = np.array(data_array)
print('X = ', X)
Y = stats.zscore(X)
mypca = IncrementalPCA(n_components=4, batch_size=None) # configure batch size
mypca.fit(Y)
print(mypca.explained_variance_ratio_)
print(mypca.singular_values_)
lib.client_parasite.read_pca_data(str(mypca.explained_variance_ratio_),str(mypca.singular_values_))
except:
pass
return fw_pca(str(mypca.explained_variance_ratio_), str(mypca.singular_values_))
def write_chroma(unused_addr, args, data):
"""
Write incoming acoustical features of Chromagram to data.txt file
"""
if args[0] is not None:
chromadata = "{}: {}\n".format(args, data)
f = open('data.txt', 'a')
f.write(chromadata) # python will convert \n to os.linesep
f.close()
with open('data.txt') as fn: # readlines in every entry
for i, l in enumerate(fn):
pass
lines = i + 1
if lines % 360 == 0: # n_features=12 * 5
pca('data.txt')
return print(lines)
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("--ip",
default="127.0.0.1", help="The ip to listen on")
parser.add_argument("--port",
type=int, default=5005, help="The port to listen on")
args = parser.parse_args()
dispatcher = dispatcher.Dispatcher()
dispatcher.map("/chroma", print)
dispatcher.map("/chroma", write_chroma)
server = osc_server.ThreadingOSCUDPServer(
(args.ip, args.port), dispatcher)
print("Serving on {}".format(server.server_address))
server.serve_forever()
client-parasite.py
"""
The script reads the data-pca.txt and replies to SC3
"""
import argparse
import numpy as np
from pythonosc import osc_message_builder
from pythonosc import udp_client
def read_pca_data(first, second):
parser = argparse.ArgumentParser()
parser.add_argument("--ip", default="127.0.0.1",
help="The ip of the OSC server")
parser.add_argument("--port", type=int, default=7007,
help="The port the OSC server is listening on")
args = parser.parse_args()
client = udp_client.SimpleUDPClient(args.ip, args.port)
return client.send_message("/components", str([first, second]))
I would like to thank Ioannis Zannos for his suggestion to focus on OSC communication between sclang and python.