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:
from boopak.package import * from boodle import agent water = bimport('org.boodler.old.water') class Example(agent.Agent): def run(self): chan = self.new_channel() self.sched_note(water.droplet_plink, chan=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 a named argument `chan`, passing in the channel we created. (The argument name and the variable name are the same -- sorry, you get used to it.)
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.Agent): def run(self): ag = Example2() loudchan = self.new_channel(1) self.sched_agent(ag, 0, chan=loudchan) ag = Example2() softchan = self.new_channel(0.25) self.sched_agent(ag, 0.5, chan=softchan) class Example2(agent.Agent): # plink forever example def run(self): self.sched_note(water.droplet_plink) 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 (as a named argument) 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.Agent): # broken! schedules an agent instance twice! def run(self): ag = Example2() loudchan = self.new_channel(1) self.sched_agent(ag, 0, chan=loudchan) softchan = self.new_channel(0.25) self.sched_agent(ag, 0.5, chan=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.)
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.Agent): # fade out def run(self): ag = Example2() chan = self.new_channel(1) self.sched_agent(ag, 0, chan=chan) chan.set_volume(0, 5) class Example2(agent.Agent): # plink forever def run(self): self.sched_note(water.droplet_plink) 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.Agent): # fade-in-out def run(self): ag = Example2() chan = self.new_channel(0) self.sched_agent(ag, 0, chan=chan) chan.set_volume(1, 3) ag = ExampleFadeOut() self.sched_agent(ag, 6, chan=chan) class ExampleFadeOut(agent.Agent): # fade-out def run(self): chan = self.channel chan.set_volume(0, 3) class Example2(agent.Agent): # plink forever def run(self): self.sched_note(water.droplet_plink) 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.Agent): # fade-out def run(self): chan = self.channel chan.set_volume(0, 3) ag = ExampleStop() self.sched_agent(ag, 3) class ExampleStop(agent.Agent): # 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 built-in agents to handle it. The example above could be rewritten:
from boopak.package import * from boodle import agent from boodle import builtin water = bimport('org.boodler.old.water') class Example(agent.Agent): # fade-in-out def run(self): ag = Example2() chan = self.new_channel(0) self.sched_agent(ag, 0, chan) chan.set_volume(1, 3) ag = builtin.FadeOutAgent(3) self.sched_agent(ag, 6, chan) class Example2(agent.Agent): # plink forever def run(self): self.sched_note(water.droplet_plink) self.resched(0.5)
The FadeOutAgent
class is defined in Boodler's builtin
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.
Here's an even shorter version:
class Example(agent.Agent): # fade-in-out def run(self): ag = Example2() inoutag = builtin.FadeInOutAgent(ag, 3, 3) self.sched_agent(inoutag) class Example2(agent.Agent): # plink forever def run(self): self.sched_note(water.droplet_plink) self.resched(0.5)
The FadeInOutAgent
does it all for you: creates a channel at zero volume, schedules the agent you pass in, fades the channel in, fades it out, and stops it. The arguments are the time of maximum volume (3 seconds) and the fade intervals (3 seconds each). If you wanted the sound to fade in over two seconds, live for five seconds, and then fade out over ten, you would say
inoutag = builtin.FadeInOutAgent(ag, 5, 2, 10)
Channels have a set_pan()
method, which is exactly analogous to set_volume()
. As described earlier, 0 means center, -1 means left, and 1 means right. For more clever tricks, such as compressing stereo soundscapes to a point or swapping channels, see the stereo
module reference.
Here's a sound that starts on the left, and then moves to the right over a five-second interval:
class Example(agent.Agent): def run(self): ag = Example2() chan = self.new_channel_pan(-1) self.sched_agent(ag, chan=chan) chan.set_pan(1, 5) class Example2(agent.Agent): # bloink forever def run(self): self.sched_note(water.droplet_bloink) self.resched(0.33)
new_channel_pan
is a variation of new_channel
which lets you define the starting pan position as well as (optionally) the starting volume.
(We use droplet_bloink
because it's a one-channel sound, so it's easier to hear where it's coming from. If we used droplet_plink
, which is stereo, you'd hear two very similar channels moving around you, and it would be hard to tell what was happening.)
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
and FadeInOutAgent
do both these things, it's wise just to use one of them.)
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.
In particular, a soundscape should never change the volume of -- or stop -- the root channel. Leave that to Boodler, and agents specifically in charge of controlling Boodler.
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. You can think of ExampleFadeOut
(and FadeOutAgent
and FadeInOutAgent
) as tools, which a soundscape uses to control its child channels.
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.
Soundscapes that accept arguments.