Patterns

header figure

 
 

Patterns are a simple way to define complex logical conditions to be used in a whenever. A pattern is a sequence of atomic patterns that describe the evolution through time of logical conditions. There is three kinds of atomic patterns: Note, Event and State.

Such a sequence is defined and then used as the condition of a whenever to trigger some actions every time the pattern matches. It can represent a neume, that is a melodic schema defining a general shape but not necessarily the exact notes or rhythms involved. It can also be used in broader contexts involving not only the pitch detected by the listening machine, but also arbitrary variables.

*Warning: * The notion of pattern used here is very specific and the recognition algorithm departs from the recognition achieved by the listening machine. Patterns define an exact pattern of variation in time (variation of variable's values) while the listening machine recognizes the most probable variation (of the audio signal) from a given dictionary of musical events. The latter relies on signal processing probabilistic methods. The former relies on algorithms like those used for recognizing regular expressions in string matching. So the pattern matching available here is not relevant for the audio signal, even if it can have some applications.

Note: Patterns on Score

The basic idea is to react to the recognition of a musical phrase defined in a manner similar to event’s specification. For example, the statement:

          @pattern_def pattern::P
          {
             Note C4 0.5
             Note D4 1.0
          }

defines a pattern that can be used later as the argument of a whenever:

          whenever pattern::P
          {
             print "found pattern P"
          }

The pattern Note is an atomic pattern and the @pattern_def defines and gives a name to a sequence of atomic patterns.

In the current version, the only events recognized are Note: chords, trill, etc. cannot be used (see however the other kinds of atomic patterns below). Contrary to the notes in the score, the duration may be omitted to specify that any duration is acceptable.

Pattern Variables

To be more flexible, patterns can be specified using local variables that act as wildcards:

          @pattern_def pattern::Q
          {
              @local $h

              Note $h
              Note $h
          }

The pattern Note matches a note in the input followed by the listening machine. The pattern pattern::Q defines a repetition of two successive notes with the same pitch (their respective duration do not matter).

The wildcard, or pattern variable $h, is specified in the clause at the beginning of the pattern definition using a @local declaration. Every occurrence of a pattern variable must refer to the same value. Here, this value is the pitch of the detected note (given in midicents).

Pattern variables are really local variables and their scope extends to the body of the whenever that uses this pattern. So they can be used to parametrize the actions to be triggered. For example:

          whenever pattern::Q
          {
                print "detection of the repetition of pitch " $h
          }

Specifying Duration

A pattern variable can also be used to restrict durations in the same manner. The value of a duration is the value given in the score (and not the actual duration played by the musician).

Specifying Constraints

There are two parameters for a note: its pitch and its duration. These parameters may be specified by

  • a constant

  • a pattern variable specifying an unknown value determined at matching time

  • an (ordinary) variable specifying a specific value (the value of the variable at matching time)

For pitches, the constant is an integer or the ratio of two integers, or a symbolic note. These constants specify the expected pitch in midicents.

For duration, the constant specifies the expected duration in raltive time (in beat) or in absolute time (in second with the s unit appended or in millisecond with ms appended). Notice however that such specification is probably useless: there is few chance that the actual note duration is exactly equal to the specified one.

A pattern variable can be used as a kind of wildcard. Declared using an @local declaration, the variable takes its actual value with the matching of its first occurrence. The following occurrence of the variable constraint the corresponding matching to take the same value.

An ordinary Antescofo variable can be used to specify a pitch or a duration. In this case, only a note with the specified pitch or duration is matched by the note pattern. The specified value is the value of the variable at the time of matching and this value can be changed dynamically (with an assignment). This mechanism can be used to adapt a pattern to a given context.

Here is an example involving ordinary variables in a pattern :

          @pattern_def pattern::R 
          {
              Note $X
              Note C4 $Y
          }

specifies a sequence of two notes. The first one must have a pitch equal to the value of the variable $X (at the time where the pattern is checked). The pitch of the second one is C4, and the duration of the first is irrelevant while the duration of the second must be equal to the value of $Y. As for $X, this variable is updated elsewhere and the value considered is its value at the time where the pattern is checked. These two variables are recognized as ordinary variables and not as pattern variables, because they are not declared with a @local in the scope of the pattern.

Additional constraints on the matching can be specified through a where clause which specifies a logical expression which must be true for the matching to succeed:

          @pattern_def pattern::R 
          {
               @local $h, $dur1, $dur2

               Note $h $dur1 where $h > 6300
               Note $h $dur2 where $dur2 < $dur1
          }

specifies a sequence of two successive notes such that:

  • their pitch is equal and this value in midicents is the value of the local variable $h;

  • $h in midicents is higher than 6300;

  • and the duration $dur2 of the second note must be lower than the duration $dur1 of the first note.

The logical expression after the where is an arbitrary expression. If it involves variables, the value of these variables is the value of the variable at the time of matching.

Pattern Causality

In a clause, all pattern variables used must have been set before. For example, it is not possible to refer to $dur2 in the where clause of the first note: the pattern recognition is causal which means that the sequence of pattern is recognized “on-line” in time from the first to the last without guessing the future.

However, it is easy to postpone a antescofo::: where clause to an event where all pattern variables have been set. For example, writing:

          @pattern_def pattern::R 
          {
              @local $h, $dur1, $dur2

               Note $h $dur1 where ($h > 6300) && ($dur2 > 0.5)
               Note $h $dur2 where $dur2 < $dur1
          }

One can also write

          @pattern_def pattern::R 
          {
              @local $h, $dur1, $dur2

               Note $h $dur1 where $h > 6300
               Note $h $dur2 where ($dur2 < $dur1) && ($dur2 > 0.5)
          }

A Complete Example

The pattern

          @pattern_def pattern::M 
          {
               @local $h, $dur

               Note $X $dur
               Note $h $dur where $dur > $Y
               Note C4 
          }

defines a sequence of 3 notes. The first note has a pitch equal to $X (at the moment where the pattern is checked); the second note has an unknown pitch referred to by $h and a duration $dur which is the same as the duration of the first note. In addition, this duration must be greater than the current value of the ordinary variable $Y; and finally, the third note as a pitch equal to C4.

Event on Arbitrary Variables

From the listening machine perspective, a ::atescofo Note is a complex event to detect in the sequence of samples of the audio input. But from the pattern matching perspective, a Note is an atomic event that can be detected looking only on the system variables $PITCH and $DURATION managed by the listening machine.

It is then natural to extend the pattern-matching mechanism to look after any variable. This generalization from any variable is achieved using the pattern Event:

          @pattern_def pattern::Gong
          {
               @local $x, $y, $s, $z

               Event $S value $x
               Event $S value $y at $s where $s > 110
               Before [4]
                 Event $S value $z where [$x < $z < $y]
          }    

The keyword Event is used here to specify that the event we are looking for is an update in the value of the variable $S1. We say that $S is the watched variable of the pattern.

An Event pattern is another kind of atomic pattern. Note and Event patterns can be freely mixed in a @pattern_def definition.

Four optional clauses can be used to constrain an Event pattern:

  1. The before clause is used to specify a temporal scope for looking at the pattern.

  2. The value clause is used to give a name or to constrain the value of the variable specified in the at matching time.

  3. The at clause can be used to refer elsewhere to the time at which the pattern occurs.

  4. The where clause can be used to specify additional logical constraint.

The Before clause must be given before the Event keyword. The last three clauses can be given in any order after the specification of the watched variable.

Contrary to the Note pattern, there is no “duration” clause because an event is point wise in time: it detects the update of a variable, which is instantaneous.

The value Clause

The value clause used in an Event is more general than that used in a Note pattern: it accepts a pattern variable or an arbitrary expression. An arbitrary expression constrains the value of the watched variable to be equal to the value of this expression2. A pattern variable is bound to the value of the watched variable. This pattern variable can be used elsewhere in the pattern.

The at Clause.

An at clause is used to bind a local variable to the value of the $NOW variable when the match occurs. This variable can then be used in another clause, e.g. to assert some properties about the time elapsed between two events or in the body of the whenever.

Unlike in a value clause, it is not possible to directly specify a value for the clause but this value can be tested in the clause:

          @pattern_def pattern::S
          {
              @local $s, $x, $y

              Event $S at $s where $s==5  ; cannot write directly: Event $S at 5
              Event $S at $x 
              Event $S at $y where ($y - $x) < 2
          }    

Note that it is very unluckily that the matching time of a pattern is exactly “5”. Notice also that the date is expressed in absolute time.

The where Clause

As for patterns, a clause is used to constraint the parameters of an event (value and occurrence time). It can also be used to check any property that must hold at the time of matching. For example: in the clause:

          @pattern_def pattern::S
          {
               Event $S where $ok
          }    

will match an update of $S only when $ok is true.

The before Clause

For a pattern q that follows a pattern p, the before clause can be used to relax the temporal scope on which q is looked for.

When Antescofo is waiting to match the pattern q =Event $X, it starts to watch the variable right after the match of the previous pattern p. Then, at the first value change of $X, Antescofo checks the various constraints on q. If the constraints are not met, the matching fails.

The before clause can be used to shrink or to extend the temporal interval on which the pattern is matched beyond the first value change. For instance, the pattern

          @pattern_def pattern::twice
          {
                @local $x
                Event $V value $x
                Before [3s] Event $V value $x
          }

is looking for two updates of variable $:::antescofo $V for the same value $x in less than 3 seconds. Nota bene that other updates may occur but $V must be updated for the same value before 3 seconds have elapsed for the pattern to match.

If we replace the temporal scope [3s] by a logical count [3#], we are looking for an update for the same value that occurs in the next 3 updates of the watched variable. The temporal scope can also be specified in relative time [3].

Notice that a before clause cannot be achieved using an at clause with a where clause; pattern twice

          @pattern_def pattern::twice2[$x]
          {
               @local $x, $s1 $s2
               Event $V value $x at $s1
               Event $V value $x at $s2 where ($s2 - $s1) <= 3
          }

pattern::twice2 does not match the same thing as pattern::twice because for pattern::twice2 the two matched events are two successive updates of $V.

When the temporal scope of a pattern is extended beyond the first value change, it is possible that several updates occurring within the temporal scope satisfy the various patterns’ constraints3. However, the pattern matching stops looking for further occurrences in the same temporal scope after having found the first one. This behavior is called the single match property.

For instance, if the variable $V takes the same value three times within 3 seconds, say at the dates t_1 < t_2 < t_3, then pattern::twice occurs three times as (t_1, t_2), (t_1, t_3), and (t_2, t_3). Because Antescofo stops to look for further occurrences when a match starting at a given date is found, only the two matches (t_1, t_2) and (t_2, t_3) are reported.

Finally, notice that the temporal scope defined in an event starts with the preceding event. So a before clause on the first of a pattern sequence is meaningless and actually forbidden by the syntax.

Watching Multiple Variables Simultaneously

It is possible to watch several variables simultaneously: the event occurs when one of the watched variable is updated (and if the constraints are fulfilled). For instance:

          @pattern_def pattern::T 
          {
               @local $s1, $s2

               Event $X, $Y at $s1
               Event $X, $Y at $s2 where ($s2 - $s1) < 1
          }

is a pattern looking for two successive updates of either $X or $Y in less than one second.

Notice that when watching multiple variables, it is not possible to use a value clause.

A Complex Example

As mentioned, it is possible to freely mix and patterns, for example to watch some variables after the occurrence of a musical event:

          @pattern_def pattern::T 
          {
               @local $d, $s1, $s2, $z

               Note D4 $d
               Before [2.5] Event $X, $Y at $s1
               Event $Z value $z at $s2 where ($z > $d) && $d > ($s2 - $s1)
          }

Note that different variables are watched after the occurrence of a note D4 (6400 midicents). This pattern is waiting for an assignment to variable $X or $Y in an interval of2.5 beats after a note D4, followed by a change in variable $Z for a value such that the duration of D4 is greater than the interval between the changes in $X or $Y, and such that the value of $Z is also greater than this interval.

State Patterns

The Event pattern corresponds to a logic of signal: each variable update is meaningful and a property is checked instantaneously on a given point in time. This contrasts with a logic of state where a property is looked on an interval of time. The State pattern can be used to face such case.

A Motivating Example

Suppose we want to trigger an action when a variable takes the value 0 for at least 2 beats. The following pattern:

          Event $X value 0

does not work because the constraint “at least 2 beats” is not taken into account. The pattern matches every time $X takes the value 0.

The pattern sequence

          @local $start, $stop
          Event $X value 0 at $start
          Event $X value 0 at $stop where ($stop - $start) >= 2      

is no better: it matches two successive updates of $X that span over 2 seconds. It would not match three consecutive updates of for the same value 0, one at each beat, a configuration that should be recognized. In addition, converting the absolute duration into relative time is difficult because it would require tracking every tempo change in the interval.

This example shows that is not an easy task to translate the specification of a state that lasts over an interval into a sequence of instantaneous events. This is why, a new kind of atomic pattern has been introduced to match states. Using a state pattern, the specification of the previous problem is easy:

          State $X where $X == 0 during 2

matches an interval of 2 beats where the variable constantly has the value 0 (irrespectively of the variable updates).

Five optional clauses can be used to constrain a state pattern:

  1. The before clause is used to specify a temporal scope for looking the pattern.

  2. The start clause can be used to refer elsewhere to the time at which the matching of the pattern has started.

  3. The stop clause can be used to refer elsewhere to the time at which the matching of the pattern stops.

  4. The where clause can be used to specify additional logical constraints.

  5. The during clause can be used to specify the duration of the state.

The before clause must be given before the event keyword. The others can be given in any order after the specification of the watched variable. There is no value clause because the value of the watched variable may change during the matching of the pattern, for instance when the state is defined as “being above some threshold”.

The first three clauses are similar to those described for an event pattern, except that the at is split into the start and the stop clauses because here the pattern is not instantaneous; it spans over an interval of time.

The initiation of a state Pattern

Contrary to note and event, the pattern is not driven solely by the updates of the watched variables. So the matching of a state is initiated immediately after the end of the previous matching.

The during Clause

The optional during clause is used to specify the time interval on which the various constraints of the pattern must hold. If this clause is not provided, the state finishes to match as soon as the constraint becomes false.

The figure below illustrates the behavior of the pattern

          @Refractory r

          State $X during d where $X > a
          Before [s]
            State $X where $X > b

The schema assumes that variable $X is sampling a continuous variation.

state pattern with during, before and @refactory clauses

The first pattern is looking for an interval of length d where $X is constantly greater than a.

The second pattern must start to match before s beats have elapsed since the end of the previous pattern (the allowed time zone is in green). The match starts as soon as it is greater than b.

The second pattern finishes its matching as soon as it becomes smaller than b because there is no specification of a duration.

With the sketched curve, there are many other possible matches corresponding to postponing the start of the first state while still maintaining $X > b. Because the start time of these matches are all different, they are not ruled out by the single match property. A refractory period is used to restrict the number of successful (reported) matches.

Limiting the Number of Matches of a Pattern

The refractory period is defined for a pattern sequence, not for an atomic pattern. The clause must be specified at the beginning of the pattern sequence just before or after an eventual @local clause.

This clause specifies the period after a successful match (of the whole pattern) during which no other matches may occur. This period is given in absolute time and counted starting from the end of the successful match. The refractory period is represented in red in the above figure. The effect of a refractory period is to restrict the number of matching per time interval.

Pattern Compilation

Patterns are not a core feature of the language: internally they are compiled in a nest of whenever, conditionals and local variables. If verbosity is greater than zero, the [printfwd] command reveals the result of the pattern compilation in the printed score.

Two properties of the generated code must be kept in mind:

  1. Causality: The pattern compiler assumes that the various constraints expressed in a pattern are free of side-effects and the pattern matching is achieved on-line, that is, sequentially in time and without assumptions about the future.

  2. Single match property: When a pattern sequence occurs several times starting at the same time t, only one pattern occurrence is reported4.

Pattern semantics and pattern compilation are detailed in Real-Time Matching of Antescofo Temporal Patterns.



  1. A variable may be updated while keeping the same value, as for instance when evaluating let $S := $S. Why $S is updated or what it represents does not matter here. For example, $S can be the result of some computation in Antescofo to record a rhythmic structure. Or $S is computed in the environment using a pitch detector or a gesture follower and its value is notified to Antescofo using a setvar message. 

  2. To achieve the same effect in a Note pattern, you need to use the where clause: a pattern variable is used to bind the pitchor the duration value and then the logical expression is checked in the where clause. 

  3. If there is no before clause, the temporal scope is “the first value change” which implies that there is at most one match. 

  4. Alternative behaviors may be considered in the future.