Boodler: Simple Soundscapes

Let's try to put theory into practice. (Or as we like to say, "practheorytice".)

Creating a package directory

Boodler 2 believes that all soundscapes come in packages. If you're developing a soundscape, you're developing a package. But you don't want to build a .boop file every time you change a line of code. Instead, you need to create a directory, and set it up like a Boodler package. Then you use the --external option to make Boodler look at it.

A package directory must contain a Metadata file. This defines the package name, the version number, the title, the author, and so on. We'll start simple, though. Create an empty directory called bootest. Inside it, create a text file called Metadata, with one line:

boodler.package: com.example.bootest

boodler.package indicates the package name, which will be com.example.bootest. A package name should really be unique to you, which usually means starting with your home domain spelled backwards. (I use com.eblong.zarf.) This tutorial will continue to use package names beginning with com.example, because http://example.com/ exists for the sake of documentation examples.

This one-file, one-line directory is sufficient to create a valid Boodler package. But it wouldn't be interesting, because there's nothing in it.

Creating a soundscape

Add a second line to your Metadata file, so that it looks like this:

boodler.package: com.example.bootest
boodler.main: main

This indicates that the package will contain a Python source file named main.py. Create that file, with that name, containing these lines:

from boopak.package import *
from boodle import agent

water = bimport('org.boodler.old.water')

class Example(agent.Agent):
    def run(self):
        self.sched_note(water.droplet_plink)

(Remember that indentation is important. None of these lines should be indented, except for the "def" and "self" lines. It doesn't matter how far they're indented, except that the "self" line must be indented more than "def".)

Now run it:

boodler --external bootest com.example.bootest/Example

You should hear a plink, and see the output lines:

16:20:32 (root) located external package: com.example.bootest 1.0
16:20:33 (root) Running "unnamed agent"

Things that might have gone wrong, if it isn't working:

"org.boodler.old.water: package directory does not exist"
You don't have the org.boodler.old.water package installed. Type:
boodle-mgr install http://boodler.org/lib/org.boodler.old.water.1.0.boop
"Package name must be given in Metadata or inferred from directory name"
You named the wrong directory, or a nonexistent directory, in the --external argument. Or, you forgot the Metadata file.
"unable to load Example ('module' object has no attribute 'Example')"
You forgot the boodler.main: main line in the Metadata file. Or, the agent class name in your main.py file doesn't match the one in your command line.
"ImportError: No module named main"
Your Python source file is in the wrong place, or isn't named main.py.
"com.example.bootest: package directory does not exist"
The package name in your Metadata file doesn't match the one in your command line.
"IndentationError: expected an indented block"
You didn't indent the last two lines in main.py properly.
You see no errors, but there's no sound.
Your speakers may be off, or you may be using the wrong Boodler driver. Go back to Installation, and make sure Boodler is set up correctly, and that the --testsound option plays an audible sound.

Here is the meaning of main.py, line by line:

from boopak.package import *

This loads a set of Boodler functions which allow you to import other packages. The bimport line below depends on this.

from boodle import agent

This loads the Boodler agent module, which allows you to define Agent classes.

(These two from lines are always necessary at the beginning of a soundscape file, but I will omit them in later examples.)

water = bimport('org.boodler.old.water')

This imports the Boodler package named org.boodler.old.water. The variable water will refer to it. This is a package of sound samples; we will need its droplet_plink sound.

class Example(agent.Agent):

This begins the definition of an Agent class (from the agent module). It will be called Example.

Agent is a general Python class, defined by Boodler, which contains all the code an agent needs -- methods to schedule notes, create channels, and so on. You are creating a specialized class, the Example agent, which makes use of that code.

    def run(self):

This begins the definition of the class's run() method. This is the function which Boodler calls when it is time for your agent to run.

The run() method should always have one argument, self.

        self.sched_note(water.droplet_plink)

This is the entire body of the run() method. It calls the note-scheduling function, which is called self.sched_note(). (Most Boodler functions that agents use are defined within the agent itself -- they are part of the general Agent class. This is why their invocations begin with self.)

We pass the sched_note() method one argument: the droplet_plink sound from the water module we imported. The sound is played at its default pitch and full volume, and it is played immediately.

A second sound

Say we want a quiet burble of water before the plink noise. water.water_rushing is a good burble sound. To play it more softly, we add some optional arguments to sched_note():

        self.sched_note(water.water_rushing, 1, 0.5)

sched_note() must take at least one argument, which is the name of the sound. If there is a second argument, it is taken as the pitch of the sound -- recall that 1 means the sound is played at its original pitch. If there is a third argument, it is taken as the volume; we give 0.5, meaning half volume.

(Note that if you're passing simple values as arguments, you have to provide them in order. You can't give the third argument -- volume -- unless you also give the second -- the pitch. We used a pitch value of 1, meaning "the sound's original pitch". To skip that, use a named argument: self.sched_note(water.water_rushing, volume=0.5).)

So, our new agent class:

class Example(agent.Agent):
    def run(self):
        self.sched_note(water.water_rushing, 1, 0.5)
        self.sched_note(water.droplet_plink)

Does this work? Well, no. The plink and the burble are played at the same time. (At least, they start at the same time. Since the plink is short, it finishes first.)

This points out an important rule of sched_note(): it schedules a note to play at a given time -- by default, immediately. The function does not actually start the note playing, and it does not wait until the note is finished.

The time at which a note plays is the fourth optional argument to sched_note(). We'll refer to it by name:

class Example(agent.Agent):
    def run(self):
        self.sched_note(water.water_rushing, 1, 0.5)
        self.sched_note(water.droplet_plink, delay=4.8)

The first sound plays at pitch 1, volume 0.5, and immediately. The second plays (at the default pitch and volume), but not until 4.8 seconds have passed. (That is, 4.8 seconds after the Example agent runs.) Since the burble sound, as it happens, is about 4.7 seconds long, the plink will not be heard until the burble is finished.

You can schedule any number of notes, at any time, and set each to play at any time. The order in which you put them on the schedule is unimportant. The agent would behave exactly the same if you swapped the two lines:

class Example(agent.Agent):
    def run(self):
        self.sched_note(water.droplet_plink, delay=4.8)
        self.sched_note(water.water_rushing, 1, 0.5)

Sometimes you want one sound to follow immediately on the heels of another. Conveniently, the sched_note() function returns the duration of the sound it plays (taking into account the pitch and duration that you specify). You can use this information:

class Example(agent.Agent):
    def run(self):
        dur = self.sched_note(water.water_rushing, 1, 0.5, 1.5)
        self.sched_note(water.droplet_plink, delay=1.5+dur)

This produces the burble, but not until 1.5 seconds have passed (in silence). Precisely when the burble ends, the plink sound begins. (Note that we didn't use the delay=1.5 notatation on the first line, because we supplied all four arguments as simple values. Either way works.)

Playing forever

Perhaps we wish that plink repeated several times. It is easy to schedule several notes at once:

class Example(agent.Agent):
    def run(self):
        self.sched_note(water.droplet_plink, delay=0.0)
        self.sched_note(water.droplet_plink, delay=0.5)
        self.sched_note(water.droplet_plink, delay=1.0)
        self.sched_note(water.droplet_plink, delay=1.5)
        self.sched_note(water.droplet_plink, delay=2.0)
        self.sched_note(water.droplet_plink, delay=2.5)

We could even use the magic of Python to schedule a whole lot of notes at once:

class Example(agent.Agent):
    def run(self):
        for num in range(100):
            self.sched_note(water.droplet_plink, delay=0.5*num)

But you cannot schedule an infinite number of notes at once.

class Example(agent.Agent):
    # broken! infinite loop!
    def run(self):
        num = 0
        while (True):
            self.sched_note(water.droplet_plink, delay=0.5*num)
            num = num+1

This will cause an infinite loop, as the system tries to schedule notes forever. It will never get around to playing any. (Actually, in the current version of Boodler, the system will throw an error when num gets too high -- it cannot schedule notes more than an hour in the future. It will then play the hour's worth of notes that have been scheduled. Which is sort of like infinity, but not much.)

The correct way to run a soundscape forever is to have an agent schedule another agent -- or reschedule itself.

class Example(agent.Agent):
    def run(self):
        self.sched_note(water.droplet_plink)
        self.resched(0.5)

The run() method plays the plink sound -- and note that it only gives one argument, so the default values of "original pitch", "full volume", and "start immediately" are employed. The method then uses the resched() method to schedule itself to run again, half a second in the future. When that time comes, the agent will run again, and schedule another plink note and yet another iteration of itself. And so on.

Scheduling another agent is not much harder. You create the new agent instance, and then use the sched_agent() method.

class Example(agent.Agent):
    def run(self):
        ag = Example2()
        self.sched_agent(ag)
        self.resched(1.0)

class Example2(agent.Agent):
    def run(self):
        # trill once
        self.sched_note(water.droplet_plink, 1.0, 1, 0.0)
        self.sched_note(water.droplet_plink, 1.2, 1, 0.1)
        self.sched_note(water.droplet_plink, 1.4, 1, 0.2)
        self.sched_note(water.droplet_plink, 1.6, 1, 0.3)
        self.sched_note(water.droplet_plink, 1.8, 1, 0.4)

The Example2 agent schedules just five notes; you can hear the effect by typing

boodler --external bootest com.example.bootest/Example2

But if you run Example, you will hear the full effect. The Example agent creates an instance of the Example2 class, schedules it to run immediately, and then reschedules itself (the Example agent) to run one second later. Thus, a trill repeated forever.

Be wary of accidentally unleashing an unbounded flood of agents. You could also have made a trill repeat forever with the following single agent:

class Example2(agent.Agent):
    def run(self):
        # trill forever
        self.sched_note(water.droplet_plink, 1.0, 1, 0.0)
        self.sched_note(water.droplet_plink, 1.2, 1, 0.1)
        self.sched_note(water.droplet_plink, 1.4, 1, 0.2)
        self.sched_note(water.droplet_plink, 1.6, 1, 0.3)
        self.sched_note(water.droplet_plink, 1.8, 1, 0.4)
        self.resched(0.91213)

But what happens if you run Example in combination with this version of Example2? Every second it will fire off another instance of Example2. But this Example2 doesn't stop after five notes; it runs forever. After ten seconds, there are ten Example2 instances hanging around, firing off fifty notes at a time. After thirty seconds, there are thirty of them. The sound rapidly builds up to a meaningless blare, and then starts to overload the Boodler engine, causing skips or clipping noise.

Don't do that.

Randomness

These examples have created very regular soundscapes. We often want an element of randomness, particularly in naturalistic soundscapes such as wind or bird noises.

The Python random module supports several different handy randomness functions. For example, random.uniform(min, max) returns a random real number between min and max. We can use this to provide an irregular sequence of plinks:

from boopak.package import *
from boodle import agent
import random

water = bimport('org.boodler.old.water')

class Example(agent.Agent):
    def run(self):
        self.sched_note(water.droplet_plink)
        delay = random.uniform(0.25, 0.75)
        self.resched(delay)

(Note that we have to import the random module at the beginning of the file.) Each time this agent runs, it reschedules itself to run again -- but the delay can be anywhere between a quarter-second and three-quarters of a second.

Other useful functions include random.randint(min, max) (return a random integer between min and max, inclusive) and random.choice(list) (return a randomly-chosen element of the list). The Python reference documentation has complete details.

Next...

Now that we've created a soundscape, it's time to build a Boodler package that contains it.


Designing Soundscapes

Return to Boodler docs index