Managing Sample Playback in Presence of Transport Commands

This how-to details a possible approach to the proper management of sample playback while using transport commands.

The problem with transport commands

During a rehearsal, it is essential to have the ability to start the execution of the score at a given label. This can be done by using a transport command, for example startfromlabel. In this case, the execution of the score is started in fastforward mode up to the label and then the normal execution is resumed.

This approach is adequate when the computation at the other end of the messages send by Antescofo are “stateless”. As a matter of fact, messages are not sent during the fastforward mode. However, when the computation has a state, we need to send the relevant messages to update this state correctly, even in fastforward mode. This is the case for instance for ‟on/off” message sent to a device.

A possible solution is to use the transport commands antescofo::scrubtobeat or antescofo::scrubtolabel, as these commands send all messages produced during the fast forward phase. However, this solution is not always possible: the accumulation of messages sent during the short duration of the fastforward mode can overload Max or PD and lead to a crash. to a crash. Antescofo limits the number of messages sent in one logical moment (i.e. simultaneously) to avoid such a crash. This limit can be increased (or decreased) by a call to the @set_max_message_sent_instantaneously function.

But even if this approach is applicable, it does not achieve the correct behavior in all cases. Consider a score that playbacks a sample by sending messages to sfplay~ (in Max) through the receiver sample, something like :

   sample open "/path/to/sample"
   sample 1

EVENT 1  Part2
; ...

If the score is run with the command antescofo::startfromlabel Part2 the message sample are not sent and there is no playback. With antescofo::scrubtolabel Part2 the sample is played but the timing is not correct. When the event corresponding to Part2 is occurs, the sample is near the begining because the time used to achieve the fastforward between the start of the score and the event labeled Part2 is negligible whilst in normal functioning, the sample is 4 beats after its begining.

We propose below a solution that can be used to achieve the correct behavior: when the computation resume at label Part2, the sample must resume its playback 4 beats after its begining.

A Solution with message and event callbacks

The idea is to use callback messages and event callbacks to ‟intercept” the message directed to sfplay and to adjust them to achieve the right behavior.

We suppose that several sfplay~ are adressed through the receiver sampleN where N is a number. The program below can be downloaded here.

This approach uses callback messages, event callbacks and the function @send_message. Refer also to transport commands. The implementation is working because during the fastforward mode, the $NOW variable is correctly updated, that is, it is updated using the virtual time of the fastforward mode that flows as fast as possible but in accordance with the progression in the score, and not by the passing of the physical clockwall time.

// ---------------------------------------------------------------------
// Example of fastforward management to drive corectly sfplay

; We record the start date of a sample in a map to implement a database
; of the runing samples.
$runing_sample := MAP{}

; We keep track of four sfplay~ commands:
;   open   : to load a sample
;   1      : to start the playback
;   0      : to stop the playback
;   seek n : to play from n to the end of the sample

@fun_def update_runing_sample($receiver, $args)
    // the open command of sfplay~ does not change the state of our database
    switch ($args[0])
          case 0:
            ; sample stoped, it must be removed from the database

          case 1:
            ; a sample is started, we record it
            $runing_sample.insert($receiver, $NOW)

          case "seek":
            ; a sample is started at some position P, this is equivalent
            ; of starting the sample at a previous date such date now it
            ; is at the right position
            $runing_sample.insert($receiver, $NOW - $args[1]/1000.)

; This function is used to trap a 'sample' message during fastforward mode
; and we replace the 'non-sending' of a sampleN message in fastforward mode
; by the update of our database. If the command is a load, we have to do the
; load. The other command do nothing: in fastforward mode, the playbacks remain
; silent.

@fun_def sample_in_ffw($receiver, $args)
    ; print FFW $receiver " and args " $args " at " $NOW
    @update_runing_sample($receivers, $args)
    if ($args[0] == "load")
    { @send_message($receivers, $args) }    

; we install the handler of sampleN message
@message_def "sample[0-9]+" [FFW] := @sample_in_ffw

; The same remark apply for the sampleN message sent during normal execution
; but this time we must also send the message. We cannot write directly the
; message to sent, because the handling of the message is redefined. So we use
; the @send_message() function to force the regular sending of the message.

@fun_def sample_in_ordinary_mode($receiver, $args)
    print send message
    @send_message($receiver, $args)
    @update_runing_sample($receiver, $args)

@message_def sample [msg] := @sample_in_ordinary_mode

; To resume the playback of the sample when we exit the fastforward mode
; we simply have to issue 'sampleN seek P' for every sample which is supposed
; to run with the right P. All information needed to compute P is in the database.
; There is however a small subtleties: the playback must resume when the expected
; event occurs, not when the fastforward phase stops. So we use two internal event
; callback:
; - the stop of the fastforward phase install an handler that is triggered with 
;   the occurence of the next event
; - and the occurence of the next event 

; When the fastforward phas is toped,
; we install a specific transient handler for the nex event
@fun_def @stop_ffw($rdate, $date)
    print "callback stop fastforward at " $rdate $date
    @callback_next_event(@exit_ffw, false)

@callback_stop_fastforward(@stop_ffw, false)

; This is the handler of the next event received after the fastforward phase.
; It simplies send the message according the databases. 
@fun_def @exit_ffw($rdate, $date)
    print "callback exit fastforward at " $rdate $date
    print "recorded: " ("" + $runing_sample)

    forall $receiver, $debut in $runing_sample
        print "launch: " $receiver seek  (($NOW - $debut)*1000.)
        @send_message($receiver, seek,  ($NOW - $debut)*1000.)

; When a stop command is received anytime, 
; we use the database to stop all the runing sample

@fun_def cbstop($rdate, $date)
     forall $recv, $d in $runing_sample
          print "stop " $recv
          @send_message($recv, 0)

@callback_stop(@cbstop, false)

; This whenever is used to trace each event
whenever($BEAT_POS == $BEAT_POS)
    print $BEAT_POS "-------------------------"

// The rest of the program is the initial score.
// We put here an arbitrary example using EVENT 
// to avoid the description of an actual score

BPM 30
sample1 open "/path/to/sample1.wav"  
sample2 open "/path/to/sample2.wav"  

Event 1
   sample1 1

Event 1
Event 1
   sample2 1
Event 1
    print sample1 seek 50
    sample1 seek 50000

Event 1
Event 1
Event 1

Event 1 Part2
  print Part2 $NOW

Event 1
Event 1
Event 1
Event 1

Event 1
  sample1 0

Event 1
   sample2 0
   print done