Let's try to put theory into practice. (Or as we like to say, "practheorytice".)
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.
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 installed. Type: boodle-mgr install http://boodler.org/lib/org.boodler.old.water.1.0.boop
--external
argument. Or, you forgot the Metadata
file.
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.
main.py
.
Metadata
file doesn't match the one in your command line.
main.py
properly.
--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.
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.)
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.
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.
Now that we've created a soundscape, it's time to build a Boodler package that contains it.