[fr] Quelques réflexions en mode geek sur le code que j'ai écrit pour le dernier concert des Chemins de Traverse.
Note: This post is primarily intended as a basis for discussion on the pyo-discuss mailing list. However, as the techniques described there might interest some other people, I decided to make it a blog post.
From tune to gig...
To illustrate this post, let's consider this example of a simplistic reverb script in the spirit of this other post.
import pyo s = pyo.Server(audio='jack', nchnls=1).boot() s.start() mic = pyo.Input(chnl=0) rev = pyo.Freeverb(mic).out() s.gui()
If all you need for your tune is a simple reverb, then you're ready to gig!
But wait - having a tune ready doesn't mean your gig is ready: there will be other tunes, and you'll have to find a way to switch tunes. Of course, you can just write another simple script and simply quit the first one and launch the second one on demand.
But we wanted more. We wanted to be able to switch tunes as seamlessly as possible, and without the slightest sound interruption.
Starting and stopping a pyo server causes dropouts (long enough to have audible clicks) on my machine... So I had to find a way to switch tunes without touching the pyo server. Also, I wanted to be able to write the scripts for each tune independently and be able to freely arrange them into gigs after that, so a big, monolithic script with all tunes in it was out of the question.
In principle, this shouldn't be very complicated: transform the above reverb code into something like
import pyo def setup(context): global rev rev = pyo.Freeverb(context['mic']).stop() def start(): rev.out() def stop(): rev.set('mul', 0, 1, tearoff) def tearoff(): rev.stop()
... and add a master script that will start the server, build an appropriate
context (i.e. connect to the right mics, pedalboards, ...),
setup() each tune and
stop() them as requested.
The problem with this solution is that the variable
rev which originally appeared only once, is now repeated 5 times. Now, this is a simplistic script, but typical real-world tunes will involve one or two dozens of these objects; moreover, when composing the tune, we are likely to try a few dozens of combinations before finding the exact sound we want. Having to modify the code in 5 different places each time is an overload I'd really like to avoid.
So I was willing to spend some time in developer mode in order to alleviate the work to do in composer mode.
In the rest of this post, I'll expose how I tried to solve this problem for last december's gig and why I'm not sure this was the best way to do it. I hope very much that someone can come with a better, cleaner solution.
Step 1: get rid of
Defining the reverb in a function instead of the global scope forced me to use the
global keyword. The code would be simpler if I could define it in the global scope, but I need the
context to get things like my
One (strange, but working) solution is to add an empty
context.py module in my project and use it as a kind of blackboard. In my master script I can do something like
import context context.mic = pyo.Input(chnl=0)
My tune is now already a little bit simpler:
import pyo import context rev = pyo.Freeverb(context.mic).stop() def start(): rev.out() def stop(): rev.set('mul', 0, 1, tearoff) def tearoff(): rev.stop()
rev is still repeated 4 times...
Step 2: Autoplay
If I think about it, the code of my
stop() methods is likely to be very similar from tune to tune: just
stop'ping a bunch of PyoObjects... Couldn't we automate this? Wouldn't it be nice if my tune could look like
import pyo import context rev = pyo.Freeverb(context.mic).auto_out() # Does not work :-(
As it happens, this is possible if we are ready to use a little bit of black magic. Let's add to the project a
autoplay.py module with the following code:
import inspect AUTOPLAY_IGNORE = True def find_module(): ''' Finds and returns the first module in the call stack that does not have the AUTOPLAY_IGNORE attribute ''' stack = inspect.stack() i = 0 for frm in stack: mod = inspect.getmodule(frm) if mod is None: raise RuntimeError('Autoplay: Cannot find module') if not hasattr(mod, 'AUTOPLAY_IGNORE'): return mod else: raise RuntimeError('Autoplay: Cannot find module') def add_to_list(mod, name, obj): l = getattr(mod, name, ) if not l: setattr(mod, name, l) l.append(obj) def autoplay(self): self.stop() mod = find_module() add_to_list(mod, 'play_on_start', self) return self def auto_out(self, channel=0): self.stop() mod = find_module() add_to_list(mod, 'out_on_start', (channel, self)) return self import pyo try: pyo.PyoObject.autoplay except AttributeError: pyo.PyoObject.autoplay = autoplay pyo.PyoObject.auto_out = auto_out
Describing exactly how this code works is out of the scope of this (already too long) post, but the general idea is using some black magic to add
auto_play methods to all PyoObjects, with the following behaviour: calling
obj.auto_out() from module
foo will automagically create the list
foo.out_on_start and add
obj to it. It's now a piece of cake to update the master script to automatically start/stop PyoObjects and the tune can be simplified to:
from autoplay import pyo # Import the modified version import context # Creates the list `out_on_start` # and adds `rev` to it so that it can be # automatically started/stopped from the # master script: rev = pyo.Freeverb(context.mic).auto_out()
Step 3: Reverb Tail & co
Actually the above code still has a problem: when stopping the song, the reverb will stop abruptly instead of dying slowly. One possible solution is defining an object like
class FxTail: def __init__(self, fx, time=2): self.fx = fx self.input = fx.input self.time = time self.ca = pyo.CallAfter(self.fx.stop, self.time).stop() def out(self, channel=0): self.ca.stop() # cancel scheduled stop self.fx.setInput(self.input) self.fx.out(channel) def play(self): self.ca.stop() # cancel scheduled stop self.fx.setInput(self.input) self.fx.play() def stop(self): self.fx.setInput(context.denorm) self.ca.play() auto_play = autoplay.autoplay auto_out = autoplay.auto_out
I can now wrap my object before
auto_out'ing it and my final version of the tune will be:
from autoplay import pyo import context rev = FxTail(pyo.Freeverb(context.mic)).auto_out()
Note that this solutions works well for a "standalone" Fx like this, but is much less convincing with a chain of objects, because
rev isn't a PyoObject any more and cannot be fed as input to another PyoObject. Maybe I should modify the
FxTail class to make it a PyoObject, but I'm not completely sure this is the right way to go.
The final version of the tune is as simple as the standalone version of the beginning. But step #3 breaks the nice "chainability" of pyo and steps #1 and #2 feel somewhat fragile and a bit dirty to me.
So, although my code works pretty well, I feel that there must be a better solution, involving less black magic and (much too) clever python tricks.
As I told in the beginning, this post is intended to serve as a basis for discussion on the pyo-discuss mailing list (if I can get the members to read such an endless post!) so if you'd like to join the discussion, I'll be glad to meet you there!