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.)
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.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.