Operators Made Easy

There was something that seriously annoyed me about yesterday’s instrument graph. The use of the Multiply() and the Sum() is bothersome; I’m used to expressing this functionality in a more concise manner using the * and + operators. Download today’s code here.

This doesn’t work for me:

return Multiply(Sum(a1, a2), RiseFall(dur, 0.5))

Thankfully, Python allows us to overload the operators, so we can express the same line as this:

return (a1 + a2) * RiseFall(dur, 0.5)

Less typing, easier to read. Let’s see it in the context of @Instr MyInstr:

@Instr
def MyInstr(dur=1.0, amp=1.0, freq=440, tune=12):
    a1 = Sine(amp * 0.5, freq)
    a2 = Sine(amp * 0.5, freq * 2 ** (tune / 12.0))
    return (a1 + a2) * RiseFall(dur, 0.5)

Here’s how I implemented it. I started by creating a generic iterator class called UnitGenerator:

class UnitGenerator:
    def __init__(self): pass
    def __iter__(self): pass                 
    def next(self): raise StopIteration    
    def __add__(self, i): return Add(self, i)
    def __mul__(self, i): return Mul(self, i)

The last two lines of the class redefine __add__() and __mul__(), which control the behaviors of + and *. These functions use the custom classes Add() and Mul(). These were originally called Sum() and Multiply(), though I renamed them to follow Python naming conventions. The last thing I had to do was alter some of the existing classes to derive from class UnitGenerator, so they automatically incorporate the overloaded operators.

class Instr(UnitGenerator):  ...
class IterReduce(UnitGenerator):  ...
class Mul(IterReduce):  ...
class Add(IterReduce):  ...
class RiseFall(UnitGenerator):  ...
class Sine(UnitGenerator):  ...

Classes Mul and Add are also of type UnitGenerator. They inherit for class IterReduce which inherents from UnitGenerator.

Realm of the Practical

Brain storming is easy; I can make things up without being held accountable. However, I need to spend time in the realm of the practical. I won’t stop collecting ideas, but if Slipmat is going to be a reality, I need to know what the issues are. This means lots of research and lots of prototyping and lots of little scripts that test various facets of Python.

For example, creating a graph of Python 3 generators:

#!/usr/bin/env python3

import operator

ksmp = 8
foo = (i * 2 for i in range(ksmp))
foo = (i + 1 for i in foo)
bar = (11 for i in range(ksmp))
foo = map(operator.mul, foo, bar)

print(*foo)

This is interesting to me because the names foo and bar do not receive an array/collect/list of numbers. Instead, they point to generator objects. These generators are not evaluated until print(*foo) is called.

By the way, if my understand or terminology is ever off, please correct me. I’m also here to learn.

Patching — An Early Mockup

Being able to patch together units is a fundamental principle of a modular environment. Though I’m far from figuring out what the syntax should look like in my faux music language, I have been writing some mock up code just to get a sense of it.

Just a warning, the following example is ignorant of i/k/a-rates, along with pass-by-reference vs pass-by-value:

import FX
import Master
import Mixer
from Envelope import line
from TestLibrary.Instruments import SineTone

# Create instances of objects and patch together
st = SineTone()                             # Simple sine instrument
reverb = FX.Reverb(input=st.out, time=3.1)  # Reverb unit
mix = Mixer.pan(0, st.out, reverb.out)      # Dry/wet: value, sig 1, sig 2
output = Master.DAC(mix)                    # Main output

# Score
@0 turnon(reverb, mix, output)  # Turn on selected instances

@0 st.play(20, 1, 440)        # Play for 20 seconds, amp = 1, frequency = 440
@0 st.amp *= line(0, 10, 1)   # Amplitude rise
@10 st.amp *= line(1, 10, 0)  # Amplitude fall
@20 mix.pan = line(1, 20, 0)  # Dry to wet over 20 seconds

@10 reverb.time += line(0, 5, 8.1)  # Increase reverb time starting at 10
@20:
    @reverb.time turnoff(reverb, mix, output)  # Turnoff selected instances

The import section loads classes from existing instrument/unit generator libraries.

In the orchestra, instances are created from the imported classes, and patched together into a simple instrument graph. There’s a simple sine instrument, which is plugged into a reverb unit. Next, there is the pan mixer, which has the dry sine instrument plugged into one side, and the wet reverb signal plugged into the other; It is initially set to 100% dry. The pan mixer is then patched into the output, which sends the audio to the DAC.

The first line in the score turns on three instruments: reverb, mix and output. There will be times when the duration is unknown. The ability to start and stop machines is a must.

The sine instrument starts with a duration of 20, an amplitude of 1 and a frequency of 440. The amplitude of the sine is modulated by two envelopes, creating a rise/fall shape. Line envelopes also modulate the dry/wet mixer and reverb time.

At the end, the turnoff function shuts down reverb, mix and output.

Bonus round: Why do you suppose I wrote,

@20:
    @reverb.time turnoff(reverb, mix, output)  # Turnoff selected instances

instead of this?

@(20 + reverb.time) turnoff(reverb, mix, output)  # Turnoff selected instances