# 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.py --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.py 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.py --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