TimeVar Basics


Music is something that occurs over time – changes in pitches, durations, amplitudes and sounds – and a FoxDot TimeVar (Time-dependant Variable) allows you to create values that change over time. Here is how it works. You create a var object and give it two inputs; a list of values you want it to hold, and a list of durations (in beats) to hold those values for. For example, take this TimeVar;

var([0, 1, 2, 3], [8, 4, 2, 2])

This will hold the value of 0 for 8 beats, then 1 for 4 beats, then 2 and 3 for 2 beats each. After this it will go back to the start of the cycle and hold the value of 0 again for 8 beats. The basics are also explained in the TimeVar quick intro page here.

You can perform any mathematical operation on a TimeVar and it will still change its value over time. So if you double the TimeVar defined above like so:

>>> a = var([0, 1, 2, 3], [8, 4, 2, 2])
>>> b = a * 2
>>> print(a, b)
(1, 2)

Then after 8 beats pass and a has a value of 1, then b will have a value of 2. After 4 more beats elapse, a will have a value of 2 and b will have a values of 4. You can add a list or tuple to a TimeVar to get a Pattern of TimeVar values; this can be useful for transforming whole melodies or rhythms for periods of time:

p1 >> pluck([0, 1, 4] + var([0, 4]), dur=PDur(3,8) * var([1, 2], 8))

You might notice a few things about the duration inputs for the TimeVars above. First, no duration was supplied to the first TimeVar – by default the duration will be the length of one bar (usually 4 beats) per value. Secondly, a single value was supplied to the TimeVar, var([1, 2], 8), instead of a list. In this case, that duration is used for all the values in the TimeVar (i.e. 8 beats each).

Other features

1. Nested TimeVars

One useful feature with TimeVars is that you can nest one inside another to subdivide a value’s… errr, well, value. This is useful for creating more complex TimeVars and is best explained with a trivial demonstration. Let’s say you’ve created the following TimeVar during a FoxDot session:

var([0, 1, 2, 3], [8, 4, 2, 2])

Now I want to change the last value on the last beat of the TimeVar’s second cycle. Here is the manual way to do it:

var([0, 1, 2, 3, 0, 1, 2, 3, 5], [8, 4, 2, 2, 8, 4, 2, 1, 1])

This is too much typing and starting to look messy. We could also add another TimeVar to our original:

var([0, 1, 2, 3], [8, 4, 2, 2]) + var([0, 2], [15, 1])

Not bad, but it requires us knowing the length of the cycle and the number with have to add to our original value to get our desired one. Here’s how do it using nested TimeVars. First, use a nested list to alternate the last value of the cycle, then use a TimeVar with that nested list like so:

var([0, 1, 2, [3, var([3, 5], 1)]), [8, 4, 2, 2])

This is a manageable way of creating more complex TimeVars without having to keep track of durations for each value used. You don’t always need to use the nested list, but it was useful for the purpose of this example. More practical examples of this technique will be shown in other parts of the documentation.

2. Named TimeVars

Sometimes it might be useful to utilise a TimeVar in multiple locations in your code. An example of this might be sharing a chord sequence between Player Objects. Here’s how you might go about doing this:

chords = var([0, 4, 5, 3])

p1 >> blip(chords + (0, 2, 4), dur=4)
p2 >> pasha(chords + [0, 2, 3, 4, 7, 9], dur=1/4)

What happens when you want to change the chord sequence? Try running this code in FoxDot for yourself. If you change the value in chords then the music doesn’t change unti you re-run the lines of code corresponding the p1 and p2. There is a way around this issue and that is to use named TimeVars. To do this, all you need to do is start your variable name with var. like so:

var.chords = var([0, 4, 5, 3])

Then reference it using the same name such that your code looks like this:

p1 >> blip(var.chords + (0, 2, 4), dur=4, oct=5)
p2 >> pasha(var.chords + [0, 2, 3, 4, 7, 9], dur=1/4)

Now FoxDot keeps track of that variable name and will update any player objects using it when its updated. So if you run your new code and change the value of var.chords to something else:

var.chords = var([0, 2, 3, 4])

The music will change instantly. Neat!

3. Transformations

You have already seen that you can do simple transformations of a TimeVar by performing arithmetic operations on them, e.g. addition and multiplication. You can also transform values using your own pre-defined functions or inline lambda functions (if you are an advanced Python user). However, you cannot simply just use my_function(my_timevar) unless the function is only performing basic mathematical operations. If it is doing something more complex, such as using if-else statements, then you need to know about the TimeVar transform method. Let’s look at an example:

Here is my arbitrary function that returns 5 if its input is odd and 3 if its input is even:

def odd_test(num):
    return 5 if num % 2 == 1 else 3

And here is my TimeVar:

my_var = var([0, 1, 2, 3])

If I call odd_test(my_var) then it will return its output (3 or 5) based on the current value of my_var and the value will stay the same, even if my_var changes. Try it yourself to see. To make sure you always return the correct value given my_var as an input, you need to call the transform method, which takes a callable object, usually a function, as an input. Note that this callable function can only take one argument as an input.

my_output = my_var.transform(odd_test)

Now my_output will always be 5 whenever my_var is odd and 3 whenever it is even. If you are an advanced Python user you can even create a class with a __call__ method present to create a callable object and use that as an input for the transform method to hold some sort of state between transformations e.g. count the number of times it was called and change the value every 10 times.