Curve (continuous action)

Many computer music controls are by nature continuous. Curves in Antescofo allow users to define such actions and to delegate the rest of the hard work to Antescofo, which takes care of correct arrival and interpolations between parameters. The construction allows for the definition of continuously sampled actions on break points and detailed control of the interpolation between them. Curves are defined by a sequence of break points and their interpolation methods along with specific attributes. As time passes, the curve is traversed and the corresponding action fired at the sampling point. Curves can be scalar (one-dimensional) or vectorial (multi-dimensional).

We introduce Curves1 starting with a simplified and familiar syntax of linear interpolation and move on to the complete syntax and showcase details of Curve construction.

Simplified Curve Syntax

The simplest continuous action to imagine is the linear interpolation of a scalar value between a starting and ending point with a duration, similar to line objects in Max or Pd. The time-step for interpolation in the simplified curve is 30 milli-seconds and hard-coded. This can be achieved using the simplified syntax as shown in

          Curve level 0.0,  1.0   2.0 s

In this example, the action constructs a line starting at 0.0, going to 1.0 in 2.0 seconds and sending the results to the receiver object level. The initial point 0.0 is separated by a comma from the destination point. The destination point consists of a destination value (here 1.0) and the time to achieve it (2.0s in this case). At each sampling point x, a message level x is sent to the environment.

Simplified Curve syntax and its realisation in Ascograph

Another facility of Simplified Curves is their ability to be chained. The score excerpt below shows the score where a second call to curve level is added on the third note. This new call does not have a starting point and only has a destination value:

          Curve level 0.5 1.0

Both Curves also act on the same receiver level. This means that during performance, the second curve will take on from whatever value of the prior curve and arrives to its destination (here 0.5) at the given time (here 1.0 beat).

Chaining Simplified Curve

Note that the second curve in the figure above cannot be visualised by Ascograph. This is because its starting point is a variable whose value is unknown and depends on where and when the prior curve arrives during performance. Moreover, by calling simplified curves as above you can make sure that the first curve does not continue while the second is running. This is because of the way Simplified Curves are hard-coded. A new call on the same receiver/action will cancel the previous one before taking over.

The reason for the malleability of Simplified Curves is because they store their value as a variable. A new call on the same receiver aborts prior calls and takes the latest stored value as departing point if no initial point is given. You can program this yourself using the complete curve syntax.

The simplified curve is thus very similar to line object in Max or PD. That said, it is important (and vital) that the first call to Simplified Curve have an initial value. Otherwise, the departing point is unknown and you risk receiving NaN values (not-anumber) until the first destination point!

To summarize, starting a simplified curve is written:

        curve max_receiver starting_point , final_point  duration

and chaining a simplified curve is written:

        curve max_receiver final_point  duration

where cexp are closed expressions.

The action fired at the sampling point of a simplified curve is restricted to be a message with only one argument: the sampled value. Replacing the receiver with a bracketed @command construct, it is possible to have more general messages and even to compute the receiver of the message:

        curve @command { receiver arg₀ arg₁ }  starting_point , final_point  duration

where receiver and the argᵢ are closed expressions, will send the message:

        @command(receiver)  arg₀ arg₁ x

for each sampling point x of the curve (see the @command keyword for computing the receiver of a message). The chained version is similar:

        curve @command { receiver arg₀ arg₁ }  final_point  duration


The simplified command hides several important properties of Curves from users and are there to simplify calls for simple linear and scalar interpolation. For example, the time-step for interpolation in the above curve is 30 milli-seconds and hard-coded. A complete curve allows for adjustment of such parameters, several kinds of multi-dimensional interpolations, complex actions, and more. From here we will detail the complete curve syntax.

Full Curve Syntax

A curve iterates a sequence of actions (specified with the @action attribute) on each sampling point (defined by the @grain attribute) of a piecewise function. The piecewise function is defined by multiple sub-functions, each sub-function applying to a certain interval defined by breakpoints. Between two breakpoints, the function is defined by an interpolation type, the delay between the breakpoint and the value of the general function at the breakpoints.

Piecewise functions defined this way are also called breakpoint functions or BPF. They are implemented in Antescofo as nim. Nim offers a powerful data structure to compute Piecewise functions and the curve construction acts as a nim player by sampling the nim in time.

In this section, we mainly discuss the curve construction that directly embeds the specification of the underlying BPF.

Scalar Curve

The example below shows a simple curve defined in two pieces. The Curve has the name C and starts at a value of 0. Two beats later, the curve reaches 2 and ends on 4 after 8 additional beats. Between the breakpoints, the interpolation is linear, as indicated by the string "linear" after the keyword @type. Linear interpolation is the default behaviour of a curve (hence it can be dismissed).

     curve C 
     @action := { level $a },
     @grain := 0.1
     {
            $a
            {
                   { 0 } @type "linear"
                 2 { 2 } @type "linear"
                 8 { 4 }
            }
     }


Curve 1

 

In the above example, variable $a, called the curve parameter, ranges over the curve. Its value is updated at a time-rate defined by attribute @grain which can be specified in absolute time or in relative time. Each time $a is updated, the @action sequence, which can refer to $a, is triggered.

Vectorial Curve

It is easy to apply curves on multi-dimensional vectors as shown in the following example:

       curve C 
       {
         $x, $y, $z
         {
                {  0, 1, -1 } 
              4 {  2, 1,  0 } 
              4 { -1, 2,  1 }
         }
       }


image


In the above example, all values in the three-dimensional vector share the same breakpoints and the same interpolation type. It is also possible to split the curve to multiple parameter clauses as below to allow different breakpoints between the curve elements:

       curve C 
       {
            $x
            {
                    { 0  } 
                 2  { 0 } 
                 0  { 1  } 
                 3  { -1 }
             }
             $y
             {
                   { 1 } 
                 3 { 2 } 
             }
       }

 
 
 

image

 
 
 

In the above example, curve parameters $x and $y have different breakpoints. The breakpoint definition on $x shows how to define a sudden change on a step-function with a zero-delay value. Incidentally, note that the result is not a continuous function on [0, 5]. The parameter is defined by only one pair of breakpoints. The last breakpoint has its time coordinate equal to 3, which ends the function before the end of $x.

The figure [1] below shows a simple 2-dimensional vector curve on Ascograph. Here, two variables $x and $y are used to sample the curve and are referenced in the action. They share the same breakpoints but can be split within the same curve. The curve is also aborted at event2 when the abort command is called with the curve's name.

image

Editing Curve with Ascograph

Using Ascograph, you can graphically interact with curves, as long as the curve parameters are constant. If this is the case, it is possible to move breakpoints vertically (changing their values) and horizontally (time position) by mouse, assigning new interpolation schemes graphically (control-click on breakpoint), splitting multi-dimensional curves and more.

For many of these operations on multi-dimensional curves, each coordinate should be represented separately. This can be done by pressing the button on the Curve box in which will automatically generate the corresponding text in the score. Each time you make graphical modifications on a curve in Ascograph, you need to press APPLY to regenerate the corresponding text.

The figure below shows the curve of the previous example [1] embedded on the event score, split and in the process of being modified by a user.

image


In the following sections we will get into details of Curve attributes: namely @action, timing @grain, and interpolation methods [@type]. But before that, we will describe the textual syntax. Knowing the textual syntax is important when defining curve whose parameters are defined by full expressions.

Textual Definition of a Full Curve

The body of the curve, the part between braces, takes various forms:

Even if the specification of the body takes several forms, the principe of the curve is the same: the idea is to sample in time a function defined through a way or another.

Curve Attributes

NIM player

Breakpoints

Curve Interpolation Type

Actions Fired by a Curve

Each time a parameter is assigned, the action specified by the attribute @action is also fired. The value of the attribute is a sequence of actions. Usually, it is a simple message but arbitrary actions are allowed, for instance :

          curve C 
          @action := {
                print $y 
              2 action₁ $y
              1 action₂ $y
          }
          { ... }

At each sampling point, the value of $y is immediately sent to the receiver print. Two beats later action₁ will be fired and one beat after that action₂ will be fired.

This sequence of actions is an implicit group and cannot have attributes, but a group can be nested for that end:

          curve C 
          @action := {
              Group @tempo := 120
              {
                    print $y 
                  2 action₁ $y
                  1 action₂ $y
              }
          }
          { ... }

If the @action attribute is absent, the curve simply assigns the parameters specified in its body. This can be useful in conjunction with other parts of the code if the parameters are refered in expressions or in other actions. In the next example, a curve is used to dynamically change the tempo of a loop

        Curve
        @grain := 5ms
        {
            $x { {60} 10 {120} }
        }

        Loop 1
        @tempo := $x
        {
             print loop $NOW
        }
        until ($x >= 105)


   will print:

  0.0
  0.954669
  1.83255
  2.64963
  3.41704
  4.14287
  4.83321
  5.49282
  6.12547
  6.73421
  7.32156

Grain, Duration and Breakpoints Specifications

In the previous example, the time step (the sampling rate of the curve) called grain size and specified with the @grain attribute, is expressed in absolute time while breakpoints' durations are expressed in relative time. However, durations and grains can be freely expressed in absolute or relative time.

The grain size can be as small as needed to achieve perceptual continuity. However, in the MAX/PD environments, one cannot go below 1ms (the temporal resolution of the host2).

The grain specifies only a maximal duration between two sampling points. This freedom is used by Antescofo to ensure that the actions fired by the curve will be fired for each breakpoints boundaries.

Grain size and duration, as well as the values at breakpoints, can be closed expressions too. Grain size is evaluated at each sampling point, which makes it possible to change dynamically the time step.

The values of the breakpoint are evaluated once: when the curve is fired.

Curve Playing a NIM

A single value can be used as an argument of the curve parameter. In this case, the expression is expected to evaluate to a NIM, allowing the user to dynamically build breakpoints and their values as a result of computation. The syntax has already been described:

          Curve ... { $x : e }

defines a curve where the breakpoints are taken from the value of the expression e. This expression is evaluated when the curve is triggered and must return a nim value. The NIM is used as a specification of the breakpoints of the curve. Notice that, when a NIM is “played” by a curve, the first breakpoint of the NIM coincides with the start of the curve.

For example

          $nim := NIM { ... }
          ; ...
          Curve 
          @tempo := 30,
          @grain := 0.1s,
          @action := { print $x }
          { $x : $nim }

Any expression can be used which evaluates to a NIM. So, the following code plays a random NIM taken in a vector of 10 NIMs:

          $nim1 := NIM { ...}
          $nim2 := NIM { ...}
          ; ...
          $nim10 := NIM { ...}

          $tab := [ $nim1, $nim2, ..., $nim10 ]
          ; ...
          Curve 
          @tempo := 30,
          @grain := 0.1s,
          @action := { print $x }
          { $x : $tab[@rand(11)] }

A typical situation is to play a NIM chosen from a repertoire of NIMs in a specified time interval $dur. In this case, directly playing the NIM with the curve is not appropriate, because the NIM will be played with its natural length. Fortunately, processes like NIMplayer make the desired behavior easy to code.

          $Nim1 := NIM { 0. 0.,0.05 1 "quad",
                      0.1 0.2 "quad_out",
                      0.85 0. "cubic" }

          $Nim2:= NIM { 0. 0.,0.05 1.,
                      0.9 1.,
                      0.05 0. }

          @proc_def ::NIMplayer($NIM, $dur)
          {
                curve readNIM
                @grain := 0.02s,
                @action := { print ($NIM($x)) }
                {
                       $x 
                       {          { (@min_key($nim)) }
                            $dur  { (@max_key($nim)) }
                       }
                }
          }

          NOTE 69 4
          ::NIMplayer($Nim1, 4)

The playing of the NIM is controlled by a curve. The functions @min_key and @max_key are used to get the definition interval of the nim $nim.

Interpolation Methods

The specification of the interpolation between two breakpoints is given by an optional string. The keyword @type is mandatory only when a variable is used to specify the interpolation type. Using a variable makes it possible to compute the interpolation type, e.g. when a curve is embedded in a process and there is a need to parameterize the interpolation type.

A linear interpolation is used by default. Antescofo offers a rich set of interpolation methods, mimicking the standard tweeners used in flash animation3. There are 10 different types:

With the exception of the linear type, all interpolations types come in three “flavors” traditionally called ease:

The corresponding interpolation keywords are listed below and illustrated in the next figures. Note that the interpolation can be different for each successive pair of breakpoints. These interpolation methods are also available for NIM (but NIM includes a richer set of interpolation types).

            "back"
            "back_in"
            "back_in_out"
            "back_out"

            "bounce"
            "bounce_in"
            "bounce_in_out"
            "bounce_out"

            "circ"
            "circ_in"
            "circ_in_out"
            "circ_out"

            "cubic"
            "cubic_in"
            "cubic_in_out"
            "cubic_out"

            "elastic"
            "elastic_in"
            "elastic_in_out"
            "elastic_out"


            "exp"
            "exp_in"
            "exp_in_out"
            "exp_out"

            "quad"
            "quad_in"
            "quad_in_out"
            "quad_out"

            "quart"
            "quart_in"
            "quart_in_out"
            "quart_out"

            "quint"
            "quint_in"
            "quint_in_out"
            "quint_out"

            "sine"
            "sine_in"
            "sine_in_out"
            "sine_out"

Programming an Interpolation Method.

If your preferred interpolation method is not included in the list above, it can be easily programmed. The idea is to apply a user defined function to the value returned by a simple linear interpolation, as follows:

          @fun_def @f($x) { ... }
          ...
          curve C
          @action := { print (@f($x)) },
          @grain := 0.1
          {
               $x
               {       { 0 } @linear
                    1s { 1 }
               }
          }

The curve will interpolate function @f between 0 and 1 after it starts, over the course of one second and with a sampling rate of 0.1 beats.

Examples of Interpolation Types

In the pictures below:

Open the imge on another window to enlarge the plot.

tween linear tween back tween bounce tween circ tween cubic tween elastic tween exp tween quad tween quart tween quint tween sine


Curve Synchronization

Synchronization attributes apply to curve. To understand the effect of synchronization strategies on curve, it is usefull to understand the curve as a group whose actions are the curve's action iterated at each sampling point:

        curve C
        @grain := d
        @action := { ... actionᵢ ... }
        { $x { {start} ... {end} } }

is really a shorthand for4:

        Group G
        {
                 $x := start
                 { ... actionᵢ ... }
              d  $x := ...
                 { ... actionᵢ ... }
              d  $x := ...
                 { ... actionᵢ ... }
              ...
              d  $x := end
                 { ... actionᵢ ... }
        }

The synchronization attributes simply apply to this group.


  1. Curves can be edited graphically using the Ascograph editor. 

  2. Usually, the Max timer resolution is around 1ms and the temporal precision around 0.5ms. 

  3. Inbetweening or tweening is the process of generating intermediate frames between two images to give the appearance that the first image evolves smoothly into the second image. The page Tweeners illustrates the standard tweens to control the successive positions of a point, illustrating the use of tweens to control the apparent speed and to achieve different qualities of movement. 

  4. The equivalent group given here is only an approximation because the grain d is dynamically computed and adjusted so that curve's action is executed for each breakpoint boundaries (breakpoint's duration are not necessary a multiple of the grain size).