Rendering 1 Second of Audio Data

Yesterday’s python script rendered 1 frame of a rudimentary additive synthesizer. Today’s script renders 1 second. You can download the complete example at textsnip or at snipt.

To render multiple frames of audio, I added two classes: Mixer and Run.

class Mixer:
    '''A simple mixer.'''
    
    def __init__(self, source1, source2):
        self.s1 = source1
        self.s2 = source2
    
    def __iter__(self):
        self.index = 0
        self.iter_1 = (i for i in self.s1)
        self.iter_2 = (i for i in self.s2)
        return self
    
    def next(self): 
        if self.index >= ksmps:
            raise StopIteration

        self.index += 1
        return self.iter_1.next() + self.iter_2.next()
    
class Run:
    '''Render frames over time.'''
    
    def __init__(self, dur=1.0):
        self.dur = dur  
    
    def __iter__(self):
        self.index = 0
        return self
    
    def next(self): 
        if self.index >= (self.dur * sr) / ksmps:
            raise StopIteration

        self.index += 1
        return self.index

Mixer is an iterator class that takes two iterator objects as its input. The values yielded by the inputs are summed together. Yesterday’s method was only good for one frame; A Mixer object does not have this restriction.

The Run iterator class is designed to loop through the iterator/audio graph over a user-defined duration of time, creating multiple frames of audio data.

The follow snippet of code resembles yesterday’s example as it creates a graph of two sine waves (a1 & a2) being fed into a mixer (amix.) The Run object is given a duration of 1 second, which produces 4410 frames (sr / ksmps) and 44100 samples (10 samples per frame.)

if __name__ == "__main__":
    a1 = Sine(0.5, 440)
    a2 = Sine(0.5, 440 * 2 ** (7 / 12.0))
    amix = Mixer(a1, a2)
    
    for frame in Run(1.0):
        print frame, ':'
        for sample in amix:
            print 't', sample

Still no output to an audio file. What it does output is a printed list of frames and samples. Here’s frame 4276:

4276 :
    0.128148365438
    0.138541849949
    0.14709747835
    0.153592896452
    0.157826911985
    0.15962183375
    0.158825589584
    0.155313602303
    0.148990404968
    0.139790979215

BTW, I’m trying a new service, textsnip.com, for storing and sharing my scripts online. If you have a better recommendation, let me know. Update: textsnip seems to add a couple of gremlins to the autodocs, so I’m trying out snipt.org as well.

Python Iterator as a Sine Oscillator

I wrote a sine oscillator as a Python iterator class. Not the fastest digital oscillator in the world. Though for now, doing prototype work in pure Python will do just fine, even if it means slow render times. In the long run, Slipmat will require a powerhouse of an engine for real-time audio synthesis and DSP, probably written in C. All in good time.

Here’s the script:

#!/usr/bin/env python
import math
import itertools

class Sine:
    '''A sine wave oscillator.'''
    
    sr = 44100
    ksmps = 10    
    
    def __init__(self, amp=1.0, freq=440, phase=0.0):
        self.amp = amp
        self.freq = float(freq)
        self.phase = phase
        
    def __iter__(self):
        self.index = 0
        return self
    
    def next(self):
        if self.index >= self.ksmps:
            raise StopIteration

        self.index += 1
        v = math.sin(self.phase * 2 * math.pi)
        self.phase += self.freq / self.sr
        return v * self.amp

if __name__ == "__main__":
    a1 = Sine(1, 4410, 0.25)
    a2 = Sine(0.5, 8820)
    amix = (i + j for i, j in itertools.izip(a1, a2))
    
    for i in amix:
        print i

Currently, it produces the same sound as a tree falling in the woods with no one around to hear it; It doesn’t write to a DAC or sound file. At least we can still view the results:

1.0
1.28454525252
0.602909620521
-0.602909620521
-1.28454525252
-1.0
-0.333488736227
-0.0151243682287
0.0151243682287
0.333488736227

Near the bottom of the code is a very simple graph, where two sine wave generators (a1 & a2) are patched into a mixer generator (amix), creating a very rudimentary additive synthesizer. The signals aren’t generated until the for-block at the very end. The script only prints one control-block’s worth of data, 10 samples, which coincides with the value of ksmps found in class Sine. The sample rate and control rate is built right into class Sine, and needs to be moved out.

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.

The KISS Principle

Keep it simple, stupid!

Wikipedia says, “The KISS principle states that simplicity should be a key goal in design, and that unnecessary complexity should be avoided.” I’m continually reminding myself of this as I move forward with Slipmat.

x Trigger Notation

In Roll You Own Syntax, I theorized how users could construct their own systems of notation using strings. I’ve constructed a working function, called trig(), to show how it’s done.

First, let’s see trig() in action. The following is complete conceptual prototype Slipmat program that generates a rock drum groove with 8th note hats:

#!/usr/bin/env slipmat

from JakeLib.Generators import trig
from EasyKit import hat, snare, kick
    
@trig('x.x. x.x. x.x. x.x.') hat()
@trig('.... x... .... x...') snare()
@trig('x... .... x... ....') kick()

This horizontal system for notating triggers, and others like it, can greatly improve the legibility of a piece, while catering to a composer’s preferred style of working. A composer quickly scans this and comprehends it without having to reconstruct it in their head from a list of individual events:

@0    hat()
@0    kick()
@0.5  hat()
@1    hat()
@1    snare()
@1.5  hat()
@2    hat()
@2    kick()
@2.5  hat()
@3    hat()
@3    snare()
@3.5  hat()

The form is lost in translation.

Building the function is pretty straight forward if you’re somewhat experienced with Python. I put together the following function definition, with docstrings, in roughly 15 minutes:

def trig(seq, res=0.25):
    '''
    Creates a numeric sequence from a string and returns a list.

    Description:
    A string trigger sequencer, where an 'x' creates a trigger, and a
    '.' creates a rest. All other glyphs are ignored. The resolution
    of triggers and rests are determined by the argument res.

    Input:
    seq -- A string containing a sequence of 'x' triggers and '.' rests
    res -- Resolution of note triggers and rests
        
    Output:
    return -- A numeric list
    '''    
    
    L = []  # The return list
    p = 0   # Position in sequence

    for c in seq:
        if c == 'x':
            L.append(p * res)
            p += 1            
        elif c == '.':
            p += 1

    return L

The trig() function accepts a string formatted in what I call ‘x trigger notation.’ The function parses the string and auto-generates a list of numbers representing trigger times. A trigger is denoted by an ‘x’, while a rest is a ‘.’. All other glyphs are ignored. I use a single space between beats for clarity. The default resolution is a 16th note, though trig() accepts an optional argument for changing the resolution, increasing its usefulness.

Import, Reuse, Remix

The best part about custom functions is that they can be reused multiple times in multiple programs by multiple people with the use of the import. No refactoring of code, no copy and paste, no reinventing of the wheel. Just import, reuse, remix.

Functions as Reusable Score Clips

I’m not a fan of the copy and paste method for composing computer music scores. I’d rather write something once, and reuse it as needed. Slipmat uses function definitions as one method for creating reusable score clips.

The following function rock_drums() stores a simple rock drum pattern with 8th note hats:

def rock_drums():
    @[0, 0.5, 1, 1.5, 2, 2.5, 3, 3.5] hat()
    @[1, 3]                           snare()
    @[0, 2]                           kick()

Here, the rock_drum() clip is played 4 times:

@0  rock_drums()
@4  rock_drums()
@8  rock_drums()
@12 rock_drums()

Here are 16 bars using the range() shorthand method:

@range(0, 64, 4) rock_drums()

Using functions as clips can save time and keystrokes while greatly improving legibility. The easier your code is to read, the more likely others will remix your work. If they can’t make heads or tails out of it, they’re more likely to move on.

The Slipcue

The Slipcue (@) is the object responsible for the fourth dimension of a Slipmat program: time.

The name is derived from the term Slip-cueing, which Wikipedia defines as, “a turntable-based DJ technique that consists of holding a record still while the platter rotates underneath the slipmat and releasing it at the right moment.”

The Slipcue is formally known as the @ scheduler. There are various reasons for the name change. I’ve only shown the Slipcue as a simple mechanism for scheduling tasks, though it controls so much more:

  • Syncing other Slipmat processes, ticks, threads and shreds
  • Starting, Stoping, Pausing, fast forwarding, rewinding
  • The Master Clock (or Mistress Clock)
  • Task Management
  • Units of Time
  • Tempo
  • etc

Plus, “the Slipcue” rolls off the tongue rather well.

Custom Sequence List Generators

Python’s built-in range() list generator is full of possibilities when used in conjunction with Slipmat’s @ scheduler. And that’s just one generator out of, dare I say, a million possibilities.

Imagine being able to conveniently import generators from a massive user library of modules. This will be a reality for Slipmat for two reasons:

I built a list generator called range_plus(), which takes Python’s range() function a few steps farther. range() is limited to integers; range_plus() removes this restriction and allows for floats. range() supports a single increment step value; range_plus() additionally allows users to pass a list of increment step values that are chosen at random.

Here’s the working Python defintion for range_plus():

def range_plus(start, stop, random_steps=1):
    '''
    Returns a list of successive incremented values
    chosen randomly from a list.

    Input:
    start -- Starting value
    stop --  End point
    random_steps -- A list of incremental values chosen at random or a
        single numeric value
        
    Output:
    return -- A numeric list
    '''
    
    if stop < start:  # Avoid infinite loop from bad input
        return []

    if not isinstance(random_steps, list):
        random_steps = [random_steps]

    L = []

    while start < stop:
        L.append(start)
        start = start + random.choice(random_steps)

    return L

I made sure to write the docstrings, to make life easier for anyone who may import my code later. Here's a Python interpreter session to test out some numbers:

>>> from MyListGenerators import range_plus
>>> range_plus(0, 4, 0.5)
[0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5]
>>> range_plus(0, 4, [0.25, 0.25, 0.5, 0.75])
[0, 0.5, 1.0, 1.5, 1.75, 2.5, 3.0, 3.25, 3.75]

As applied to music, range_plus() would be a perfect fit for randomly generating rhythmic patterns, for things such as hi-hats.

@0  @range_plus(0, 4, [0.25, 0.25, 0.5, 0.75]) hat()
@4  @range_plus(0, 4, [0.25, 0.25, 0.5, 0.75]) hat()
@12 @range_plus(0, 4, [0.25, 0.25, 0.5, 0.75]) hat()
@16 @range_plus(0, 4, [0.25, 0.25, 0.5, 0.75]) hat()

Here's is one scenario for what those patterns would look like in trigger notation:

Bar 1:  x.x. x.xx ..x. xx.x
Bar 2:  xx.. x..x ..x. .xx.
Bar 3:  x..x ..x. xx.x .x..
Bar 4:  x.x. .xxx .xxx x..x

The 'x' is a 16th note trigger, the '.' is a 16th note rest, and spaces are used to distinguish between each quarter note.

Auto-Generating Lists

In yesterday’s blog, Lists as Micro-Sequencers, we discussed writing lists instead of individual events. Today, we’ll generate lists instead of writing list values.

Python comes with the built-in range() function that generates and returns a list of integers, which can be used to schedule events. The following python interpreter session demos range() with three sets of args along with their respective outputs.

>>> range(4)
[0, 1, 2, 3]
>>> range(5, 9)
[5, 6, 7, 8]
>>> range(0, 16, 4)
[0, 4, 8, 12]

The range() function can be a huge saver of time and keystrokes. Especially if you’re composing goa trance:

@range(1024) goa_kick()

“The kicks are done, man.” Just to be absolutely clear, that generates 1024 goa_kick() events, one per beat.