Boodler: Working With Channels

As we have said, a channel is a group of agents and notes which can be manipulated all together. In the examples above, the agents -- and therefore all the notes -- have run in the root channel, which is created automatically. Here is an example that creates a new channel:
class Example(Agent):
    name = 'channel example'
    def run(self):
        chan = self.new_channel()
        self.sched_note('environ/droplet-plink.aiff', 1, 1, 0, chan)
The new_channel() method creates a channel, which is contained inside the channel that the agent is running in -- that is, inside the root channel. We then call sched_note() with five arguments: pitch, volume, time, and channel. (As usual, to provide the fifth argument we must also give the first four.)

This plays a note inside our new channel. As you hear, this sounds exactly the same as playing it in the root channel.

So what's the point? Consider this example:

class Example(Agent):
    name = 'channel example'
    def run(self):
        ag = Example2()
        loudchan = self.new_channel(1)
        self.sched_agent(ag, 0, loudchan)
        ag = Example2()
        softchan = self.new_channel(0.25)
        self.sched_agent(ag, 0.5, softchan)

class Example2(Agent):
    name = 'plink forever example'
    def run(self):
        self.sched_note('environ/droplet-plink.aiff')
        self.resched(1.0)
The Example2 agent repeats a plink sound once per second, forever. Example creates two channels and two instances of Example2, and sets them off.

We've dropped in some new optional arguments here. The first argument of new_channel() is the channel volume. It defaults to 1.0, meaning (as usual) "full volume". We set the first channel to full volume, but for the second channel we set one-quarter volume instead. The three arguments to sched_agent() are the agent, the scheduling time, and the channel.

So the first agent runs in a full-volume channel, and starts immediately. The second agent runs in a 25% channel, and is scheduled to start one half-second in the future. Since each of the two agents repeats a plink every second, you hear one every half-second, but they alternate loud-soft-loud-soft.

(A quick footnote that doesn't really belong here: note that we create two instances of Example2. It would not be legal to create one instance and schedule it twice:

class Example(Agent):
    # broken! schedules an agent instance twice!
    name = 'channel example'
    def run(self):
        ag = Example2()
        loudchan = self.new_channel(1)
        self.sched_agent(ag, 0, loudchan)
        softchan = self.new_channel(0.25)
        self.sched_agent(ag, 0.5, softchan)
No instance of an Agent class may wait on the schedule twice at the same time. On the other hand, once an agent starts running, it's off the schedule and it's legal to put it back on; this is why the resched() method works.)

Changing volumes and stopping channels

In the previous example we created channels with particular volume levels. It's also possible to change the volume of a channel as it plays.

class Example(Agent):
    name = 'fade-out example'
    def run(self):
        ag = Example2()
        chan = self.new_channel(1)
        self.sched_agent(ag, 0, chan)
        chan.set_volume(0, 5)

class Example2(Agent):
    name = 'plink forever example'
    def run(self):
        self.sched_note('environ/droplet-plink.aiff')
        self.resched(0.5)
We create the channel with full volume, but we then call chan.set_volume(). (Note that this is a method of the channel, not of self.) The first argument is the volume to change to; the second is how long it takes to slide to that level. We are scheduling the volume to fade from 1 (full) to 0 (silent) over a five-second interval. The Example2 agent runs continuously during this time, but since it is running in the channel, its notes are affected by the volume change.

Note that Boodler does not shut down after the five seconds are over. Example2 is still running and playing notes; they're just at zero volume.

(By the way, you should never call chan.set_volume() with a zero-second fade interval. Changing the volume instantaneously produces clicking or popping in the sound stream. If you leave off the second argument -- for example,

        chan.set_volume(0)
-- then the default interval will be 0.005, or five milliseconds. This is short enough to sound instantaneously, but long enough to prevent popping. You should not use an interval shorter than this.)

We frequently want a soundscape that starts at zero volume, fades in, plays for a few seconds, and then fades out. Unfortunately, chan.set_volume() does not have an argument for starting time. It always schedules the volume change beginning immediately. To schedule a volume change starting in the future, we must create and schedule an agent.

class Example(Agent):
    name = 'fade-in-out example'
    def run(self):
        ag = Example2()
        chan = self.new_channel(0)
        self.sched_agent(ag, 0, chan)
        chan.set_volume(1, 3)
        ag = ExampleFadeOut()
        self.sched_agent(ag, 6, chan)

class ExampleFadeOut(Agent):
    name = 'fade-out example'
    def run(self):
        chan = self.channel
        chan.set_volume(0, 3)

class Example2(Agent):
    name = 'plink forever example'
    def run(self):
        self.sched_note('environ/droplet-plink.aiff')
        self.resched(0.5)
We create a zero-volume channel, start the plinker immediately, and schedule a volume change from 0 to 1 over three seconds. Then we schedule an instance of ExampleFadeOut to begin running after six seconds. (This gives us three seconds of fade-in followed by three seconds of full volume.) The ExampleFadeOut agent does nothing but schedule a three-second fade-out, from volume 1 to 0. After nine seconds, we are back to silence.

It would be nice if Boodler shut down after that nine-second sequence. The chan.stop() method will kill a channel, and all notes and agents running in it. (And also any channels inside that channel.) But this method, like chan.set_volume(), takes effect immediately. To delay it, we must add another agent.

class ExampleFadeOut(Agent):
    name = 'fade-out example'
    def run(self):
        chan = self.channel
        chan.set_volume(0, 3)
        ag = ExampleStop()
        self.sched_agent(ag, 3)

class ExampleStop(Agent):
    name = 'stop example'
    def run(self):
        chan = self.channel
        chan.stop()
(The other two agents are as before.) Note that when ExampleFadeOut schedules ExampleStop, it does not need to pass a second argument to sched_agent(); the default behavior is to schedule the new agent in the same channel as the current agent, and that's the channel that Example creates.

Also note that the sequence of events has gotten quite elaborate. Example launches two agents, one immediate and one delayed. The immediate agent runs forever, rescheduling itself every half-second. The delayed agent launches a third agent after yet another delay.

(We could have scheduled ExampleFadeOut and ExampleStop both directly from Example, but this arrangement makes the soundscape easier to modify. If we wanted to change the full-volume interlude from three seconds to five, we would just have to change the 6 in Example to an 8. ExampleFadeOut would run later, but it would still schedule the chan.set_volume() and the ExampleStop at the correct times, relative to each other.)

Actually, this sequence -- fade out and stop -- is so common that Boodler has a built-in agent to handle it. The example above could be rewritten:

class Example(Agent):
    name = 'fade-in-out example'
    def run(self):
        ag = Example2()
        chan = self.new_channel(0)
        self.sched_agent(ag, 0, chan)
        chan.set_volume(1, 3)
        ag = FadeOutAgent(3)
        self.sched_agent(ag, 6, chan)

class Example2(Agent):
    name = 'plink forever example'
    def run(self):
        self.sched_note('environ/droplet-plink.aiff')
        self.resched(0.5)
The FadeOutAgent class is defined in Boodler's agent module, which we import. We create an instance, passing the fade-out interval (three seconds) as an argument. Then we schedule it like any other agent.

Channel limitations and rules

The channel.stop() method kills a channel instantaneously; any sounds that are playing get cut off. This, like an instantaneous volume change, can cause clicks and pops. It is wise to fade the volume to zero before you stop the channel.

(In fact, since FadeOutAgent does both these things, it's just wise to use FadeOutAgent.)

The channel.set_volume() system is somewhat limited. You cannot schedule two volume changes on the same channel at the same time. Overlapping volume changes will sound wrong, as the later change pre-empts the first. In fact, if a volume change even begins too soon after the previous one ends (on a given channel), the results will not be exactly right. The lesson here is that volume changes should not be used for frequent, short-term effects on a channel. Keep them a few seconds apart.

On the other hand, there's no problem with volume changes on different channels. It is perfectly fine for two agents to be running, each creating its own channels, and fading them in and out independently. They will not interfere with each other. In fact, it may be that (unbeknownst to them) the root channel is slowly fading out.

(You may wonder what happens when a note is playing in a channel, inside another channel, inside the root channel, and each of these has its own volume level. Simply: the loudness of a note is found by multiplying its own volume (the volume passed to sched_note()) by the volume of every channel it is inside. The default volume, 1, has no effect on the final product. On the other hand, if any channel has volume zero, the product will be zero. This is what we expect: you can silence a note by silencing the root channel, or the channel that directly contains the note, or any channel in between.)

This brings up one final, somewhat abstract question: who is in charge of a channel's volume? Since two agents trying to change the volume of a single channel will interfere with each other, we need a guideline.

The general guideline is: an agent takes responsibility for controlling the volume of the channels it creates. An agent should not try to change the volume of the channel it is running in.

This is because, in general, an agent does not know what other agents might be running in the channel with it. If they all tried to manipulate that channel's volume, none of them would get what they wanted. Instead, if you want to mess with channel volumes, do what we do in the examples above: create a channel for your own use, run agents in it, and change the volume of that channel.

This does not mean that it is absolutely forbidden for an agent to change or stop its own channel. ExampleFadeOut above does just that. But it is part of a system of agents that make up a soundscape. In fact, Example is employing it for the specific purpose of changing the volume of the channel that Example created.

But it would be a bad idea for Example to call self.channel.set_volume(0). Example is a complete soundscape, and some other soundscape might want to invoke it running in a channel with several other agents.


Designing Soundscapes

Return to Boodler docs index