Factoring Out Common Args To Zipped Generators


I'm playing around with some additive synthesis in Python.

I've implemented an oscillator as a generator that takes a number of parameters. It is then possible to mix multiple oscillators using zip (or better, itertools.izip) over them and doing a (weighted) sum.

However, I wanted to be able to factor out common arguments to the oscillators so I didn't have to specify the frequency of each one individually.

I knew functools.partial would be part of the solution but it took me a while to work out how to combine its use with generators and itertools.izip.

Here is a simplified progression of what I came up with.

Phase 1

Rather than use oscillators, let's just imagine with have a generator that works a lot like xrange:

def gen1(start, stop, step): n = start while n <= stop: yield n n += step

then we can combine multiple generators and, say, sum the corresponding elements like this:

for x in zip(gen1(10, 20, 2), gen1(10, 25, 3)): print sum(x),

Phase 2

Let's abstract this into a function that takes generators as arguments (and uses itertools.izip)

def mixer1(*generators): return (sum(x) for x in izip(*generators))

for x in mixer1(gen1(10, 20, 2), gen1(10, 25, 3)): print x,

mixer1 is similar to my mixer (although without weighting)

Phase 3

But now say we wanted to factor out the common start parameter. First we need a partial version of function gen1:

gen2 = lambda **kwargs: partial(gen1, **kwargs)

This allows one to say

partial_gen = gen2(stop=20, step=2)

and then later call

partial_gen(start=10)

to get the generator.

But what we now need is a new version of the mixer that takes the extra keyword args and passes them in to each partial function to turn them back into generators:

def mixer2(*generators, **kwargs): return mixer1(*[gen(**kwargs) for gen in generators])

and now we can say:

for x in mixer2(gen2(stop=20, step=2), gen2(stop=25, step=3), start=10): print x,

Phase 4

Here's the final version:

gen2 = lambda **kwargs: partial(gen1, **kwargs)

def mixer3(*generators, **kwargs): return (sum(x) for x in izip(*[gen(**kwargs) for gen in generators]))

The real thing is a little more involved because of the weighted summing, etc but the hard parts are shown.

The original post was in the category: python but I'm still in the process of migrating categories over.

The original post had 1 comment I'm in the process of migrating over.