Curve (continuous action)

Many computer music controls are by nature continuous. They are implemented by sampling in time the continuous variation of one or more parameters and performing some arbitrary action at each sampling point.

Curves in Antescofo allow users to define such continuous variations and the corresponding actions. They are two ways to specify these continuous variations:

The programmer simply specify the continuous variations of the parameters and the corresponding actions (specified as an Antescofo group). The rest of the work is handled by the system which takes care of the computation the sampling points and the correct scheduling of the associate actions.

A breakpoint functions is defined by a sequence of interval. The value of the function is explicitly given on the borders of the interval and an interpolation method is specified for the points within this interval.

Curve based on breakpoint function may come in three flavors:

  • If the breakpoint function is defined by a linear interpolation on a single interval, then there is a simplified syntax which is very similar to the line construction in Max or PD. The user has few controls on such simplified curve. For instance, it cannot change the curve sampling rate.

  • The full curve syntax allow a fine control on the sampling process. It also make possible to specify vectorial curve. This additional expressivity comes with a price: the syntax is more complex than that of a simplified curve.

  • It is also possible to split appart the specification of breakpoint function and of the sampling process. in this case, the breakpoint function is defined elsewhere as a NIM data-structure and the curve construct can be seen as a NIM player. As time passes, the NIM is traversed and the corresponding action fired at the sampling point.

We introduce Curves 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

Chaining Simplified Curve

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

This second curve takes precedence over the first, that is, it stops the first sampling process if needed and starts it sampling from the value reached by the first curve (which is not necessarily the final value if the second curve starts before the end of the first).

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.

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.

The chaining may interleave the two forms almost arbitrarily: the first form defines a starting point and the second form starts from the last sampled value (so you must initiate the chaining with the first form).

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!

Performing Actions in a Simplified Curve

The action fired at the sampling point of a simplified curve is limited to two kind of actions:

  • updating the value of a variable with the sampled point,

  • or sending a message.

For the first case, you use a variable instead of a Max receiver in the curve construction:

        curve $var  starting_point , final_point  duration
        curve $var  final_point  duration

the sampled values will be assigned to the variable $var by the curve. This variable can be used elswhere in the score. For example, you can use a whenever construction to monitor the updates.

If the construct specify a message receiver, then at each sampling point, the curve sends a message with a single argment: 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

Summary of Simplified Curve

To summarize, starting a simplified curve is written:

        curve max_receiver starting_point , final_point  duration
        curve $var starting_point , final_point  duration
        curve @command{ exp₀ exp₁ } starting_point , final_point  duration

and chaining a simplified curve is written:

        curve max_receiver final_point  duration
        curve $var final_point  duration
        curve @command{ exp₀ exp₁ } final_point  duration

where cexp are closed expressions.


Full Curve Syntax

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.


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:

  • a sequence of explicit breakpoint specifying the value of the function at the breakpoints and the interpolation type between breakpoints (as exemplified previously);

  • sequence of symbolic breakpoints where the interval bteween breakpoints, the value at each breakpoints and the intrepolation type is not precisely given (only a general pace: increasing, decreasing, etc. is specified);

  • a nim which is value that represents a function (this value can be computed elsewhere);

  • a set of differential equations (this experimental feature is described in the next chapter).

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

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 animation2. There are 10 different types:

  • linear, quad, cubic, quart, quint: which correspond to polynomial of degree respectively one to five;

  • expo: exponential, i.e. \alpha e^{\beta t + \delta} + \gamma

  • sine: sinusoidal interpolation \alpha \sin(\beta t + \delta) + \gamma

  • back: overshooting cubic easing (\alpha+1) t^3 - \alpha t^2

  • circ: circular interpolation \alpha \sqrt{(\beta t + \delta)} + \gamma

  • bounce: exponentially decaying parabolic bounce

  • elastic: exponentially decaying sine wave

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

  • in (the default) which means that the derivative of the curve is increasing with the time (usually from zero to some value),

  • out when the derivative of the curve is decreasing (usually to zero),

  • and in_out when the derivative first increases (until the midpoint of the two breakpoints) and then decreases.

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:

  • The label xxx[0] corresponds to the ease in, that is to the type "xxx_in" or equivalently "xxx".

  • The label xxx[1] corresponds to the ease out, i.e. the interpolation type "xxx_out".

  • And the label xxx[2] corresponds to the ease in_out, i.e. interpolation method "xxx_in_out".

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 for3:

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

The synchronization attributes simply apply to this group.


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

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

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