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

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

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

`boodle-event.py` 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.py` 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.py 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.py` 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.py --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.py --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