Real Time sound synthesis with Jupyter
Published on 2017-12-03 12:00
6 min read
In category
Jupyter
This notebook demonstrates using WebAudio API and Web Sockets to transfer audio generated with Python/Numpy to Jupyters notebook Web frontend. The notebook can be downloaded from my Github repository.
%%bash
#Install support for websockets
pip install autobahn
pip install ipywidgets
pip install plotly
Requirement already satisfied: autobahn in /usr/local/lib/python3.5/dist-packages
Requirement already satisfied: six>=1.10.0 in /usr/local/lib/python3.5/dist-packages (from autobahn)
....
import asyncio
from autobahn.asyncio.websocket import WebSocketServerProtocol, WebSocketServerFactory
#import websockets
import numpy
import threading
import time
import plotly
from plotly.graph_objs import Scatter, Layout
plotly.offline.init_notebook_mode(connected=True)
from ipywidgets import interact, interactive, fixed, interact_manual
import ipywidgets as widgets
server = None
loop_thread = None
Creating the signal
The signal to play back is generated by the Python backend through the use of Numpy. It's a simple signal consisting of 5 sine waves.
buffer_size = 4096
sample_rate = 44100.0
#note float32 is essential, the same format is used by javascript
#could be that when endianness is not equal between client and server
#additional processing is needed
buffer = numpy.zeros(buffer_size,dtype=numpy.float32)
#create an array of 5 frequencies
component_count = 5
sine_frequencies = numpy.linspace(start = 200, stop = 400, num = component_count)
#reshape from (5,) to (1,5)
sine_frequencies = numpy.reshape( sine_frequencies, (1, component_count))
#create a (1,5) array with the phase change per sample
sine_frequencies_angle_per_s = ( sine_frequencies * 2.0 * numpy.pi )
#store the start phases for each buffer fill
#by using a start phase you won't get glitches in the signal
#when chaning the frequency
sine_start_phases = numpy.zeros(sine_frequencies.shape)
#create buffers for each sine component -> (buffer_size,components)
sine_components = numpy.zeros(
(buffer_size,sine_frequencies.shape[0]),
dtype=numpy.float32)
#calculate the time of each sample in seconds (buffer_size,1)
sample_times = numpy.reshape(numpy.linspace(start = 0,stop = buffer_size / sample_rate, num = buffer_size),(buffer_size,1))
def setSineFrequency(component, frequency):
"""This function is called when a slider is moved"""
global sine_frequencies, sine_frequencies_angle_per_s
sine_frequencies[0,component] = frequency
sine_frequencies_angle_per_s = ( sine_frequencies * 2.0 * numpy.pi )
# Create a bunch of sliders for each frequency component
for component in range(sine_frequencies.shape[1]):
interact(setSineFrequency,
component = fixed(component),
frequency = widgets.FloatSlider(
min=20,max=20000,
step=1,
value=sine_frequencies[0,component]))
def fillBuffer():
"""Fill a single buffer with the sum of the sine components"""
global sine_start_phases, sine_components
sine_phases = sine_start_phases + sample_times * sine_frequencies_angle_per_s
sine_components = numpy.sin(sine_phases) * 1 / component_count
numpy.sum(sine_components, axis = 1, out = buffer)
sine_start_phases += (buffer_size + 1) * sine_frequencies_angle_per_s / sample_rate
# To test
#setSineFrequency(0,220)
glued = numpy.zeros(2*4096)
fillBuffer()
glued[0:4096] = buffer[0:4096]
#setSineFrequency(0,440)
fillBuffer()
glued[4096:8192] = buffer[0:4096]
In the actual notebook there will be shown some sliders with which you can change the frequencies of the components
x = plotly.offline.iplot({
"data": [Scatter(y=glued)],
"layout": Layout(title="Two buffers")
})
Python backend for websockets
Python can support websockets via the autobahn package. Autobahn needs an event loop system. The example uses asyncio.
#Create a thread to run the asyncio loop
#It calls loop_run_forever
loop = asyncio.get_event_loop()
class WebIOThread(threading.Thread):
def __init__(self):
super().__init__()
self.loop = asyncio.get_event_loop()
self.should_quit = False
def quit(self):
self.should_quit = True
if (self.loop.is_running):
self.loop.call_soon_threadsafe(self.loop.stop)
def run(self):
self.running = True
print("thread start")
while (not self.should_quit):
#logging.debug('running')
print("start loop")
self.loop.run_forever()
print("loop exited")
if (not self.should_quit):
time.sleep(1)
print("thread exit")
self.running = False
#Define a simple websocket protocol that handles requests
#for a sample buffers
class SignalGeneratorProtocol(WebSocketServerProtocol):
def onConnect(self, request):
print("Client connecting: {}".format(request.peer))
def onOpen(self):
print("WebSocket connection open.")
def onMessage(self, payload, isBinary):
# if the client sends (any) message then
# assume it wants a new buffer of samples
#print(".")
fillBuffer()
self.sendMessage(buffer.tobytes(), True)
def onClose(self, wasClean, code, reason):
print("WebSocket connection closed: {}".format(reason))
factory = WebSocketServerFactory()
factory.protocol = SignalGeneratorProtocol
#Ever time this cell is executed, cleanup
#previous servers
if server is not None:
print("Close existing server")
server.close()
server = None
if (loop_thread is not None):
print("Close existing loop thread")
loop_thread.quit()
loop_thread.join()
loop_thread = None
loop = asyncio.get_event_loop()
coro = loop.create_server(factory, '0.0.0.0', 8889)
server = loop.run_until_complete(coro)
loop_thread = WebIOThread()
loop_thread.start()
Close existing server
Close existing loop thread
loop exited
thread exit
thread start
start loop
Javascript audio client
The following javascript initializes WebAudio and defines a ScriptProcessor which is called periodically to fill the audio buffer. If there are less then 10 audio buffers queued then request more by sending a request via the websocket.
Execute this cell to start play back.
%%javascript
if (!window.audioContext) {
window.audioContext = new AudioContext();
}
// Connect a web socket to a localhost server on port 8889
var ws = new WebSocket("ws://127.0.0.1:8889");
ws.binaryType = 'arraybuffer';
window.ws = ws;
// bufferSize should be the same on client and server
var bufferSize = 4096;
var player = (function() {
var sampleNumber = 0;
var node = window.audioContext.createScriptProcessor(bufferSize, 1, 1);
node.onaudioprocess = function(e) {
var output = e.outputBuffer.getChannelData(0);
// does the websocket receive any buffers with samples
if (ws.received_buffers.length>=1) {
// playback the first received buffer
output.set(ws.received_buffers.shift());
}
if (ws.received_buffers.length<10) {
// The number of sample buffers in the queue is getting low
// request more buffers
if (ws.readyState==1) {
// Web Socket is connected, send data using send()
ws.send("Get block");
ws.send("Get block");
}
}
}
return node;
})();
// called when the websocket opened
ws.onopen = function()
{
console.log("Websocket connected");
ws.received_buffers = [] ;
ws.received_frames = 0
// request a buffer of samples
ws.send("Get block");
ws.is_connected = false;
};
ws.onmessage = function (evt)
{
// a new buffer of samples was received
ws.received_buffers.push(new Float32Array(evt.data)); //.set(evt.data);
//ws.last_received_buffer.set(evt.data);
ws.received_frames += 1;
if (!ws.is_connected) {
player.connect(window.audioContext.destination);
ws.is_connected = true;
}
};
ws.onclose = function()
{
player.disconnect();
ws.is_connected = false;
// websocket is closed.
console.log("Connection is closed...");
};
Client connecting: tcp:172.17.0.1:50694
WebSocket connection open.
Javascript audio client
The following javascript initializes WebAudio and defines a ScriptProcessor which is called periodically to fill the audio buffer. If there are less then 10 audio buffers queued then request more by sending a request via the websocket.
Execute this cell to start audio output.
%%javascript
console.log(window.ws.received_buffers.length)
window.ws.close()
WebSocket connection closed: None
Conclusion
The notebook shows that is possible to synthesize audio from within Python and add interactivity using IPyWidgets. Next step is to record audio using the Web Audio API, send it to the backend, process it and then send it back to the front end for playback.