Boodler: Soundscapes With Arguments

You've passed arguments to soundscapes on the command line, and even run soundscapes (such as OneSound) that required an argument. Now let's write a soundscape that accepts arguments.

To do this, add an init() method to your Agent class. This is called during construction, with any arguments (named or unnamed) that were provided.

(Python devotees should note that this is an init() method, not __init__(). The Agent base class has an __init__() method, which you are unlikely to need to override or call. It passes along all its arguments to the init() that you write.)

from boopak.package import *
from boodle import agent

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

class Example2(agent.Agent):
    def init(self, pitch, reptime):
        self.pitch = pitch
        self.reptime = reptime
    def run(self):
        self.sched_note(water.droplet_plink, self.pitch)
        self.resched(self.reptime)

class Example(agent.Agent):
    def run(self):
        ag = Example2(1.5, 0.5)
        self.sched_agent(ag)
        ag = Example2(1, 1.21)
        self.sched_agent(ag)

The Example2 soundscape plinks forever, and takes two arguments: the plinking pitch and the repeat time. Example creates two instances of Example2. The first has pitch 1.5 (high) and repeats every half-second, and the other has pitch 1 (normal) and repeats every 1.21 seconds. Both are scheduled to start immediately, so when you run Example...

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

...you hear a quick high plinking interspersed with a low, slower sound.

Your init() method can do anything you want. But remember that it is called when the agent instance is created. This is before the agent is scheduled, placed in a channel, or set running. Therefore, the init() method cannot schedule notes or agents, or create channels, or do anything else that affects the stream of sound. (If you think you want to do these things, you probably really want to create a separate agent class that does them in its run() method.)

Most often, init() will just take its arguments and store them away for run() time. In the example, we take the pitch argument and attach it to the agent instance, as self.pitch. We do the same with reptime.

Example2 requires two arguments, so you won't be surprised that this doesn't work:

boodler --extern bootest com.example.bootest/Example2

If you try it you'll see a "2 arguments required". Okay, that's easy to fix:

boodler --extern bootest com.example.bootest/Example2 1.2 0.5

But that doesn't work either; the error says "TypeError: a float is required". What's going on?

The problem is that Boodler doesn't know how to interpret 1.2 or 0.5. Strings? Floats? Integers (in which case it should be showing a different error)? Lacking any information about what pitch and reptime should be, it passes these values in as strings. Example2 winds up calling sched_note() with a string for a pitch value, and that causes a Python error.

This is a sad state of affairs, but you can resolve it by letting Boodler know what types those arguments should be. The easiest way to do this is to provide default values:

class Example2(agent.Agent):
    def init(self, pitch=1.0, reptime=1.0):
        self.pitch = pitch
        self.reptime = reptime
    def run(self):
        self.sched_note(water.droplet_plink, self.pitch)
        self.resched(self.reptime)

This actually solves two problems. First, you can now say

boodler --extern bootest com.example.bootest/Example2

...and get a sensible result; the agent will run with a pitch of 1 and a repeat time of 1 second. And secondly, Boodler will see that those default values are floats, and deduce that Example2 takes two float arguments. So when you say:

boodler --extern bootest com.example.bootest/Example2 1.2 0.5

...the values will be passed in correctly.

Boodler can recognize string, int, float, and bool argument types. It can also recognize lists, tuples, sounds, and soundscapes -- but those take more thought, so we will get to them in a moment.

By the way, ints and floats are different types. You wouldn't want to write Example2 with the line

def init(self, pitch=1, reptime=1):

Even though 1 is the same numeric value as 1.0, Boodler would infer that these are integer arguments, and then it would reject values like 1.2 or 0.5 on the command line.

On the other hand, you would want to use an integer for an argument like this:

class Example3(agent.Agent):
    def init(self, pitch=1.0, reptime=1.0, count=1):
        self.pitch = pitch
        self.reptime = reptime
        self.count = count
    def run(self):
        for ix in range(self.count):
            self.sched_note(water.droplet_plink, self.pitch,
                delay=ix*self.reptime)

In this case, count is a number of repetitions, so it should be integral.

Specifying argument types explicitly

Boodler can infer a lot about your Agent's arguments, but not always everything. Perhaps you don't want to specify default values, but you want the correct types anyway.

You can declare additional information about your arguments, or even override Boodler's assumptions, by creating an _args field in your Agent class.

from boopak.package import *
from boopak.argdef import *
from boodle import agent

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

class Example(agent.Agent):
    _args = ArgList(Arg(type=float), Arg(type=float))

    def init(self, pitch, reptime):
        self.pitch = pitch
        self.reptime = reptime
    def run(self):
        self.sched_note(water.droplet_plink, self.pitch)
        self.resched(self.reptime)

The argument data structures are defined in the argdef module, so we must import its contents. That lets us create an ArgList object, which we store as _args.

The ArgList is set up with a sequence of Arg objects, each one representing one of the init() arguments. For each one, as you see, we can specify a type. Boodler consults both the _args and the init() function when deriving argument metadata.

You can actually specify all sorts of information in an Arg. For example, you could do this:

_args = ArgList(Arg(type=float, default=1), Arg(type=float, default=1))

That defines the type and the default value for each argument. With that line in place, you can run

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

...and the defaults will be picked up, even though they're not defined in the init() code.

The _args can also specify information about a particular argument. If you had this:

_args = ArgList(reptime=Arg(type=float))

...it would provide information about reptime, but leave pitch alone.

List and tuple arguments

Here's an Agent which takes a list of pitches, and schedules one note for each:

class Example(agent.Agent):
    def init(self, pitches=[]):
        self.pitches = pitches
    def run(self):
        pos = 0.0
        for val in self.pitches:
            self.sched_note(water.droplet_plink, val, delay=pos)
            pos = pos+0.2

This has one argument, and we even provide a default value (the empty list). Boodler can infer that the argument type is list, but it can't figure out what kind of list we mean. We can tell it, though:

_args = ArgList(Arg(type=ListOf(float)))

ListOf is another magic structure from the argdef module. ListOf(float) means just what it sounds like -- the argument will expect a list of floats. So you can say:

boodler --extern bootest com.example.bootest/Example "(0.9 1.0 1.1 1.2)"

(Lists are in parentheses, because there's a little Lisp in all of us. As usual, we have to quote parentheses to keep the Unix shell from eating them.)

ListOf offers a great deal of power. You can, of course, define types like ListOf(int) and ListOf(str), or even ListOf(ListOf(int)). You could also say ListOf(int, str) -- this defines a list of alternating integers and strings. If you want the first entry to be an integer, but all the rest to be strings, you'd say ListOf(int, str, repeat=1) -- i.e., repeat the last (one) type to extend the sequence. ListOf(int, str, float, repeat=2) would come out as [int, str, float, str, float, ...]

All of the examples so far permit any number of values in the list; we've only been specifying what types they should be, if they show up. To require a list of a particular length, you can say something like ListOf(int, min=1) (a list of at least one integer), or ListOf(int, min=1, max=5) (at least one integer, but at most five).

There's also a TupleOf type structure. It works almost the same way. The only difference is, a declaration like TupleOf(int, str, float) is interpreted as a tuple of exactly three elements (an integer, a string, and a float). This is a nod to the way tuples are commonly used. You can stick in min, max, and repeat values -- just as you can with ListOf -- for any length requirement that suits you. TupleOf(int, min=0, max=None) would be a tuple of integers of any length.

Just for completeness, you can say Arg(type=list) or Arg(type=tuple). (Equivalently, Arg(type=ListOf()) or Arg(type=TupleOf()).) These assume lists/tuples of any length, and indeterminate type -- meaning strings, by default. But parenthesized elements will be themselves interpreted as lists of indeterminate values. It is almost completely unlikely that you will ever care about this.

Sound arguments

Boodler will recognize the type of a sound sample argument.

class Example(agent.Agent):
    def init(self, sound=water.droplet_plink):
        self.sound = sound
    def run(self):
        self.sched_note(self.sound)

This plays one sound, with org.boodler.old.water/droplet_plink as the default. So:

boodler --extern bootest com.example.bootest/Example
boodler --extern bootest com.example.bootest/Example
    org.boodler.old.water/droplet_bloink

And of course you can declare a list of sounds with an _args declaration, ListOf, and the Sample class. (We haven't come across this before, because Boodler sets up sound objects before, but it lives in the boodle.sample module.)

from boodle import sample

class Example(agent.Agent):
    _args = ArgList(Arg(type=ListOf(sample.Sample)))
    def init(self, pitches):
        self.pitches = pitches

Soundscape arguments

Arguments which take soundscapes are not any harder -- on the surface.

class Example2(agent.Agent):
    def run(self):
        self.sched_note(water.droplet_plink)

class Example(agent.Agent):
    def init(self, ag=Example2()):
        self.ag = ag
    def run(self):
        self.sched_agent(self.ag)

Example is a simple Agent which accepts another Agent object as an argument, and runs it. (Not useful, but good enough for an example.) That's fine, and we can run it:

boodler --extern bootest com.example.bootest/Example
boodler --extern bootest com.example.bootest/Example com.eblong.zarf.clock/Tick

But say we want to accept an Agent and play it more than once. Here's an attempt to play a soundscape twice, to the left and right, with a slight delay on one side:

from boodle import stereo

class Example(agent.Agent):
    def init(self, ag=Example2()):
        self.ag = ag
    def run(self):
        chan = self.new_channel_pan(stereo.fixed(-1))
        self.sched_agent(self.ag, chan=chan)
        chan = self.new_channel_pan(stereo.fixed(1))
        self.sched_agent(self.ag, chan=chan, delay=0.25)

One channel starts up, but then we see an "instance is already scheduled" error. That's because an Agent object can't run twice at the same time.

We need two Agent objects. To do that, we'll pass the Agent class as the argument, and then instantiate it twice:

from boodle import stereo

class Example(agent.Agent):
    def init(self, clas=Example2):
        self.clas = clas
    def run(self):
        chan = self.new_channel_pan(stereo.fixed(-1))
        ag = self.clas()
        self.sched_agent(ag, chan=chan)
        chan = self.new_channel_pan(stereo.fixed(1))
        ag = self.clas()
        self.sched_agent(ag, chan=chan, delay=0.25)

Pleasantly, you don't have to worry about this change on the command line. You can use the same commands as before:

boodler --extern bootest com.example.bootest/Example
boodler --extern bootest com.example.bootest/Example
    com.eblong.zarf.clock/Tick

Boodler recognizes the difference between an Agent instance argument and an Agent class argument, and interprets com.eblong.zarf.clock/Tick appropriately.

In fact, this is true even if you throw in complex soundscape arguments:

boodler --extern bootest com.example.bootest/Example
    "(com.eblong.zarf.clock/Tick 1.25)"

In this case, the clas argument of Example isn't literally the com.eblong.zarf.clock/Tick class. It's a curried or "wrapped class", with the argument 1.25 already built in. You can instantiate it twice, and you'll get two separate instances, each with a pitch of 1.25.

By the way, this way of multi-running soundscapes can lead to surprises, if you run a soundscape that includes random parameters. Try this:

boodler --extern bootest com.example.bootest/Example
    com.eblong.zarf.clock/SteadyRandomChime

You'll hear two chime streams, left and right. But the SteadyRandomChime class chooses a random pitch in its init() method. So you won't hear two identical chime soundscapes. The left and right channels will be different.

To specify an Agent instance or class in an _args declaration, use Agent or Wrapped(Agent). The Wrapped structure creates the wrapped form of the type.

So, this is a list of Agent instances:

_args = ArgList(Arg(type=ListOf(agent.Agent)))

And this is a list of Agent classes:

_args = ArgList(Arg(type=ListOf(Wrapped(agent.Agent))))

Really, you can apply Wrapped to any type. But there's no point in wrapping immutable types (like int, bool, float, or str). Getting a new integer every time is indistinguishable from getting the same integer over and over.

On the other hand, you may find it useful to apply Wrapped to mutable types -- such as lists -- if you plan to mutate them as your Agent runs.

Extra arguments

Python lets you write a function where the arguments just get handed to you all in a list. Here's an attempt to accept several soundscape arguments, and start each one in its own channel:

class Example(agent.Agent):
    def init(self, *agents):
        self.agentlist = agents
    def run(self):
        for ag in self.agentlist:
            chan = self.new_channel()
            self.sched_agent(ag, chan=chan)

For this to work, we have to declare _args, using the special form ArgExtra:

_args = ArgList(ArgExtra(ListOf(agent.Agent)))

You include the ArgExtra instead of (or in addition to) the Arg entries of the ArgList. You can only specify the type (not default or anything else), and the type must be a list or tuple.

(This example, once you throw in the _args, is essentially identical to boodler.org.manage/Simultaneous. As an exercise, implement boodler.org.manage/Sequential. Note that you'll have to use Wrapped(agent.Agent), because Sequential runs each argument many times.)

(There is currently no _args notation for Python's func(**dic) syntax.)

Next

Soundscapes that you can control from the outside, and also that can control each other.


Designing Soundscapes

Return to Boodler docs index