Boodler: Communicating with Soundscapes

You are now well familiar with the act of starting soundscapes. Any time your code creates an Agent, you can pass in arguments. And if you read the previous chapter, you know more than you probably wish about the way command-line arguments are handled.

At the other end of the spindle, you can stop a soundscape, by shutting down the channel it runs in. (Most often with builtin.StopAgent or builtin.FadeOutAgent.)

Is there anything in between? Is life a thread, spun out of straw at one end and brutally sheared at the other, hanging whither it will in between? Am I slightly giddy on too much holiday shortbread? Yes, no, and probably. But let us return to Boodler.

Python approaches

The simplest way to tweak a soundscape in flight is to call one of its methods. (Or set one of its fields.) A soundscape is a Python object, after all, and you can put in any code you want. Consider this pair of agents:

from boopak.package import *
from boodle import agent

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

class Example(agent.Agent):
    def init(self):
        self.agent = None
    def run(self):
        if (self.firsttime):
            self.agent = Example2()
            self.sched_agent(self.agent)
            self.resched(2)
        else:
            self.agent.change_sound(water.droplet_bloink)

class Example2(agent.Agent):
    def init(self):
        self.sound = water.droplet_plink
    def run(self):
        self.sched_note(self.sound)
        self.resched(0.33)
    def change_sound(self, sound):
        self.sound = sound

The first time it runs, Example fires up an Example2 soundscape. It stores a reference to the Example2 agent for later use, and then it reschedules itself, with a two-second delay.

(The self.firsttime flag is a convenience that Boodler offers to all Agents. It's True the first time the agent runs, and False every time after that.)

The second time Example runs, it calls the change_sound() method on the agent it started. What is this? It's not part of the Boodler API. It's a method invented purely for this example, and you can see it defined in Example2. That agent repeats a sound forever (three times a second), and its change_sound() method changes which sound it's repeating.

So you hear repeated plinking, but after two seconds, it changes to bloinking. After that nothing changes; Example only runs twice, and even if it did run again, it would just keep Example2 on the droplet_bloink sound.

This is a straightforward means of soundscape-to-soundscape communication. But it isn't very flexible. You can only call methods on an Agent if you have a reference to it, which basically means "if you started it up". If you want to broadcast a message to every Agent, you will need another plan.

(Calling methods can also lead to tricky scheduling issues. For example, it's legal to call a method that contains sched_agent() or sched_note() calls -- those aren't limited to the run() method. But then you have to be careful about whether the Agent is actually alive. If it hasn't been started up yet, or if its channel has been shut down, you'll see Python errors.)

Boodler events

To broadcast a message for other Agents to hear, you can use Boodler events.

An event is just a string (with maybe some additional data tacked on) which is dropped into a channel. Any Agent which is listening for that event, in that channel, will receive it. Agents which are not listening will ignore it.

An Agent doesn't necessarily listen for every event. An event name can be a dotted domain name, like a package name: "bird.owl.hoot", for example. An Agent listening for that specific string will receive the matching event. An Agent can also listen for a group of events -- that is, a first part of the name. If an Agent is listening for "bird", or for "bird.owl", it will receive the event "bird.owl.hoot".

It also matters which channel you're listening on, and which channel the event was sent to. A channel event can be heard by Agents listening in that channel, and that channel's parent channels -- but not in channels within the given channel. In other words, events propagate upward, towards the root channel. If you listen in the root channel, you can hear every event that occurs. If you listen in a subchannel, you are effectively limiting your horizon to the events of that channel and the channels inside it.

(Therefore, to receive every single event in the system, you'd have to listen on the root channel, for events named "". This probably isn't useful, but you might do it while debugging.)

Here is an example that demonstrates sending and receiving events:

class Example(agent.Agent):
    def run(self):
        self.sched_agent(ExampleReceive())
        self.sched_agent(ExampleSend(), delay=1)

class ExampleReceive(agent.Agent):
    def run(self):
        self.listen('go')
    def receive(self, event):
        self.sched_note(water.droplet_plink)

class ExampleSend(agent.Agent):
    def run(self):
        self.send_event('go')
        self.resched(3)

Example starts up two other agents, ExampleReceive and ExampleSend. ExampleReceive runs immediately, but all it does is begin listening for the "go" event. Notice that it does not reschedule itself. It is no longer on the schedule at all; it is waiting around, listening for events.

The ExampleSend agent runs every three seconds, in the usual way, but it doesn't play any sounds. All it does is to send out a "go" event. Since the ExampleReceive is listening for this, its receive() method is called, and you hear a plink.

We're not specifying channels in this example. Example starts up the other two agents in its own channel, which is the root channel. The listen() and send_event() methods also work in the caller's channel unless you specify otherwise. So each "go" event hits the root channel, which is where ExampleReceive is listening, which is fine.

The run() method of ExampleReceive has no purpose but to start listening. There's nothing wrong with that, but you can use a shortcut, which is a bit clearer to read:

class Example(agent.Agent):
    def run(self):
        self.post_listener_agent(ExampleReceive())
        self.sched_agent(ExampleSend(), delay=1)

class ExampleReceive(agent.Agent):
    selected_event = 'go'
    def receive(self, event):
        self.sched_note(water.droplet_plink)

(ExampleSend is unchanged.) Rather than scheduling ExampleReceive, we call post_listener_agent(), which starts it listening directly. The event name it will listen for is defined in the class variable selected_event.

The post_listener_agent(), listen(), and send_event() methods have the usual trail of options to specify channels, events, and so on. We won't try to go through them here; see the Agent class reference for complete details.

External events

Boodler starts to get really interesting when it reacts to events from the outside world. This is the job of the boodle-event script.

Begin with this soundscape:

class Example(agent.Agent):
    selected_event = 'go'
    def run(self):
        self.listen(hold=True)
    def receive(self, event):
        self.sched_note(water.droplet_plink)

This is nearly the same as our first ExampleReceive. The only difference is the listen() call. It doesn't specify an event name, so that's pulled from the selected_event declaration -- we've seen that before.

Then there's the hold=True argument, which we need because of a little problem we glossed over earlier. A Boodler channel exists for as long as it has any agents or sounds scheduled (or until it's explictly stopped). But a listening Agent isn't scheduled. So if Example just called self.listen(), then as soon as that invocation of run() was finished, it would leave the root channel -- in fact, all of Boodler -- with nothing on the schedule. Boodler would shut down immediately.

This wasn't a problem before, because the ExampleSend agent kept running in the root channel, keeping it alive. But now there is no ExampleSend. So we have to set hold=True in the listen() call. The idea is that, really, a Boodler channel exists for as long as it has any agents or sounds scheduled, or any listening agents with the hold flag set.

(Yes, it is possible for an agent to stop listening. Either call its unlisten() method, or call the cancel() method of the object which listen() returns. See the Agent class reference for details.)

Now that we've covered that, let's start up this soundscape. We need an extra option on the command line:

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

The --listen option tells Boodler to listen for events. It acts as a teeny little network server, which can receive events from other machines (or the same machine, of course).

Network servers mean thinking about security, which is why this option isn't on by default -- you have to request it explicitly. Boodler uses TCP port 31863. You may have to open this port on the firewall of the machine you run Boodler on. On the other hand, you may want to block this port on the firewall of your broadband network device. Boodler event spam isn't a serious problem today, but who knows what will happen tomorrow?

(To choose a different port number, use the --port option. Another possibility, if you want to avoid the whole network-server situation, is to give an absolute filename for the port "number" -- for example, --port /tmp/boosocket. This will create a named Unix domain socket, which can be used by other processes on the same machine, but not on any other machine.)

Leave Boodler running. From a different command window, do this:

boodle-event go

(If you specified a different --port for boodler, use the same --port option for boodle-event. If you are running boodle-event on a different machine, use the --hostname option. The details are on the boodle-event reference page.)

(If you see a "Connection refused" error, you forgot the --listen argument to boodler. Or else it isn't running.)

boodle-event lets you send any message to Boodler's root channel. When you send the "go" event, the listening Example will hear it and play its plink. You can send the event repeatedly, if that amuses you.

For other examples of listening soundscapes, download the org.boodle.listen package.

A final note: You don't actually have to use boodle-event to send events. The protocol is simply a direct TCP/IP connection to port 31863. (telnet will work fine.) Each (nonempty) line sent is an event. The line is broken up into strings at whitespace; the resulting tuple becomes the event. You can keep the connection open, and send many messages in a row, if you want. Unix, Mac, or DOS linebreaks will all work.

Events with arguments

Events can come with arguments, much as soundscapes can.

class Example(agent.Agent):
    selected_event = 'sound'
    catalog = [
        water.droplet_plink, 
        water.droplet_bloink,
        water.rain_med, 
        water.water_rapids
    ]
    def run(self):
        self.listen(hold=True)
    def receive(self, event, val):
        val = int(val) - 1
        samp = self.catalog[val]
        self.sched_note(samp)

When you run this, it will listen for events of the form "sound 1" through "sound 4". That is, you can say:

boodle-event sound 1

...to play the first sound in the catalog, and so on.

Note that the extra values do not figure into the "bird.owl.hoot" naming scheme that was described above. Event name matching is only done on the first part of the event, which is this case is just "sound".

The extra values in the event turn up as extra arguments to the receive() function. They always arrive as strings, which is why the int(val) call is in there -- the string must be converted to an integer. Of course, if the value is not numeric, you'll see a Python error.

(Sadly, the event argument system is not yet tied into Boodler's argument type system. A future version of Boodler will let you declare event argument types, and the conversion will happen automatically.)

Not only does Boodler fail to check the argument types, it fails to check the number of arguments. If you run boodle-event with too few or too many arguments, Boodler will call your receive() method with too few or too many arguments, and again you'll see a Python error.

On the other hand, Boodler guarantees that an exception in a run() or receive() method doesn't kill Boodler. It will just abort that call. Future calls -- in this case, future events -- will still work correctly.

Boodler properties

Properties are a channel communication mechanism which are rather the reverse of events. Properties are completely passive; you can set a property or examine its value, but you never receive a notification of it. A property has a name and exactly one associated value. Properties propagate down the channel tree, not up it.

Let's rewrite the very first example on this page, using properties instead of a Python method call:

class Example(agent.Agent):
    def init(self):
        self.agent = None
    def run(self):
        if (self.firsttime):
            self.set_prop('sound', water.droplet_plink)
            self.agent = Example2()
            self.sched_agent(self.agent)
            self.resched(2)
        else:
            self.set_prop('sound', water.droplet_bloink)

class Example2(agent.Agent):
    def run(self):
        sound = self.get_prop('sound')
        self.sched_note(sound)
        self.resched(0.33)

The set_prop() and get_prop() methods do the work here. On the first call, Example sets a property called "sound" on its own channel (the root channel). The value of this property is the sound water.droplet_plink. It then sets Example2 running. That repeatedly plays whatever sound is the current "sound" property.

Two seconds later, Example fires up and changes the "sound" property to water.droplet_bloink. So, as before, you hear two seconds of plinking followed by a switch to bloinking.

When you call get_prop() on a channel, you'll see any property (of that name) which is set on that channel or its parent channels. This is what we mean by "properties propagate downward". A property set on the root channel will be visible to every agent in every channel. If you set a property on a subchannel, you are limiting its scope to the agents in that channel, and the channels inside it.

External properties

You can set Boodler properties when you start up Boodler. Try this:

class Example(agent.Agent):
    catalog = [
        water.droplet_plink, 
        water.droplet_bloink,
        water.rain_med, 
        water.water_rapids
    ]
    def run(self):
        val = self.get_prop('sound')
        val = int(val) - 1
        samp = self.catalog[val]
        self.sched_note(samp)
        self.resched(0.5)

If you run this straight up:

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

...you'll see an error, because the "sound" property is unset. (When no property is set, get_prop() returns None.) But you can do this:

boodler --prop sound=2 --external bootest com.example.bootest/Example

The --prop option sets a property of the root channel; in this case, the "sound" property, to the string "2". Again, there is no type-checking, so we have to convert the string to an int ourselves.

(You can also set properties with the BOODLER_PROPERTIES environment variable. Set this to `sound=2`, or a comma-separated list of properties of that form.)

Next

We've covered all the features you need to write soundscapes. Go to it.

For excruciatingly detailed explanations of all of Boodler's packages, modules, and classes, see the Boodler API reference.


Designing Soundscapes

Return to Boodler docs index