Jump to content

Scripter - how to run a function on a time interval


benbravo

Recommended Posts

Hi there logic scripters,

 

Getting stuck on something seemingly simple... I am trying to run a simple loop through an array of Midi Events on an interval.

 

My idea:

setInterval(function(){
for ( var i = 0; i < eventsArray.length; i++){
	eventsArray[i].send();
}
}, 5000)

 

The setInterval (and setTimeout for that matter), seems to not be recognised by Logic:

[JS Exception] ReferenceError: Can't find variable: setInterval

 

How to resolve this?

 

Thanks!

Link to comment
Share on other sites

Logic's scripter is not a "program anything you want in javascript" environment.

 

Logic runs a callback that basically runs your code on incoming events. You might be able to monitor the sync/playback position (I can't remember offhand whether you can get sync information in Scripter), and then only run your routines when a specific playback time has passed, perhaps?

Link to comment
Share on other sites

benbravo said:

Hi there logic scripters,

Getting stuck on something seemingly simple... I am trying to run a simple loop through an array of Midi Events on an interval.

My idea:

setInterval(function(){
for ( var i = 0; i < eventsArray.length; i++){
	eventsArray[i].send();
}
}, 5000)

The setInterval (and setTimeout for that matter), seems to not be recognised by Logic:

[JS Exception] ReferenceError: Can't find variable: setInterval

How to resolve this?

Thanks!

One thing I'll point out with this is, even if this was able to work this would "sound" terrible because you're just sending MIDI notes without regard to timing (sync/quantize), it's just random notes playing anywhere. I mention this because you're using just Event.send(), not Event.sendAfterMilliseconds(), Event.sendAtBeat or Event.sendAfterBeats().

I'm assuming you're using ProcessMIDI() and not HandleMIDI() as well.

I personally don't use ProcessMIDI() since it's just more CPU usage.

I use HandleMIDI() and on aother track I cabled to my main track and send a CC to let me know where I'm at.

Then in my script, once I see that CC, I know where I'm at and them I can send addtional MIDI Events.

That's how I stay in sync/quantize with the project.

Link to comment
Share on other sites

Hi there logic scripters,

 

Getting stuck on something seemingly simple... I am trying to run a simple loop through an array of Midi Events on an interval.

 

My idea:

setInterval(function(){
for ( var i = 0; i < eventsArray.length; i++){
	eventsArray[i].send();
}
}, 5000)

 

The setInterval (and setTimeout for that matter), seems to not be recognised by Logic:

[JS Exception] ReferenceError: Can't find variable: setInterval

 

How to resolve this?

 

Thanks!

 

So typical JavaScript timer functions are usually built into the browser and are not available in scripter, as Valle said already. Basically the way to handle that kind of thing in scripter is write something in ProcessMIDI that will check a time to see if it’s time to send something a d then send it, as Valle aireado said you should use Beatpos to exactly schedule the sending of events.

 

But you can also schedule them all way ahead of time and they will just playback. I will make some examples later.

 

Also remember that scripter does not run its code in real time. Scripter is always executing a little bit ahead of the time when you will actually hear the notes. That’s why you always want to base your time information on beatpos usually.

 

What exactly are you trying to accomplish?

Link to comment
Share on other sites

Thanks for the replies and infos. I am actually trying to achieve a infinite loop (thats the keyword here) of pre-stored Notes or CC messages or both. I record the messages and the time between them in two different arrays. Storing and playing back once is easy, indefinitely looping on a time interval is where I get stuck, because I need to be able to do that without starting the timeline. Is there a solution or do I HAVE to run the transport for it to work?
Link to comment
Share on other sites

Thanks for the replies and infos. I am actually trying to achieve a infinite loop (thats the keyword here) of pre-stored Notes or CC messages or both. I record the messages and the time between them in two different arrays. Storing and playing back once is easy, indefinitely looping on a time interval is where I get stuck, because I need to be able to do that without starting the timeline. Is there a solution or do I HAVE to run the transport for it to work?

Why don't you drag and drop your MIDI data into a LiveLoop Cell then. Once you hit play, that loop is playing forever until you stop it.

LiveLoopEdit.thumb.png.6335ef28c023e749409d71bdb22c6493.png

Link to comment
Share on other sites

seems like my question triggers the curiosity :) thanks for the inputs. Here’s more context: the reason for this is that I am working on a live setup in MainStage (as the MainStage forum is way less active I thought I’d be better off posting my scripter question here) involving recording midi notes and CC’s on the fly, and looping them (no audio loop). I want to have both options, timed and untimed loops, that’s why I emphasized the point of having the transport not running. But reading your posts and thinking about it, it might be possible to play untimed loops based on the beatPos info and the transport running... What do you think?

 

Unfortunately the new live loops function of Logic is not (yet) available in MS...

Link to comment
Share on other sites

So you can definitely access the Date and system time using standard Javascript in Scripter. For example, this works:

 

var d = new Date();

var s = d.getSeconds();
Trace(s);

var ms = d.getMilliseconds();
Trace(ms);

var time = d.getTime();
Trace(time);

 

so . . . something like this should work as a simple timer. Here I've set up an interval timer that does something every 2 seconds (2000 milliseconds) 3 times.

 

var repeats = 3;
var n = 0 ;
while (n < 3) {
var startTime = new Date().getTime();
var currentTime = new Date().getTime();
while (currentTime - startTime < 2000){
	currentTime = new Date().getTime();
}
//do something that you want here

n++;
}

Link to comment
Share on other sites

Actually, my idea doesn't work. It is possible to get system time info. For example:

 

function HandleMIDI(event)
{
event.trace();
event.send();
var eventTime = new Date().getTime();
Trace(eventTime);
}

But any attempt to roll my own interval timer to trigger events using a loop is foiled by the fact that the sending of those events (even a console logging via Trace) is not really synchronous despite looking that way in code. I tried some noteOn events. My attempts to write "blocking code" using the while loop only ended up causing the notes to all trigger at once after all intervals had passed or to delay the initial noteOn event used as the trigger for the whole thing if I placed my makeshift timer in the HandleMIDI() function.

I guess the whole system is asynchronous / event-based. After all, the scripts "compile" (sort of) when you run them, and then simply wait for incoming events.

Link to comment
Share on other sites

using Date.getTime() will tell you the exact time when scripter is processing the event that came into HandleMIDI, but it will not tell you anything about when that midi event is supposed to actually play in time. that can only be revealed by looking at beatPos.

 

Scripter also does not really h ave any way to wake up on a timer to call a function. All you can do is put code inside ProcessMIDI() or perhaps Idle(), which should check the beat time of the process block and then handle things if and when the window of time matches the window of time of the current process block.

Edited by Dewdman42
Link to comment
Share on other sites

On the topic of timers in Scripter:

 

here is a simple example using getTime() to wait for a timer and send midi after 1000 ms. Anything you hit a key on the keyboard, it sets a new timer for 1000ms

 

var wait = 1000;   //ms
var start = 0;
var waiting = false;

function HandleMIDI(event)
{
   start = new Date().getTime();
   waiting = true;
   event.send();
}

function ProcessMIDI() {

  let now = new Date().getTime();
  
  if(waiting && now > start + wait) {
      
      let event = new NoteOn;
      event.channel = 1;
      event.velocity = 100;
      event.pitch = 60;
      event.send();
      event.velocity = 0;
      event.sendAfterMilliseconds(500);
      waiting = false;
      
  }
}

 

 

for the curious...here is some experimentation code I did with Scripter a while back to attempt to create a MidiTimer class. It basically works. But I haven't found a use for it yet. But its an example of what you have to do in order to have Scripter call a function on a timer.

 

var NeedsTimingInfo = true;

/******************************************************************
* MidiTimer Class
******************************************************************/

MidiTimer = function() {
   this.triggers = [];
   this.mstriggers = [];
};

//-------------------------
// * process method
// *
// * should be called once per process block
// * to update various timer counters and 
// * potentially call timer triggers
//

MidiTimer.prototype.process = function(ctx) {

   // Date.now triggers, always, even when not playing

   for( let i=0; i<this.mstriggers.length; i++) {
   
       let t = this.mstriggers[i];
       let now = Date.now();
       
       if( now >= t.start + t.ms ) {
               
           let funcPtr = t.func;
           funcPtr(t.ms);
           this.mstriggers.splice(i,1); // remove the trigger
       }
   }  
   
   /*********************************
    * Only while playing?
    *********************************/
    
   if(ctx == undefined) {
       ctx = GetTimingInfo();
   }

   if (!ctx.playing) return;
   
   // BeatPos triggers
   for( let i=0; i<this.triggers.length; i++) {
       let t = this.triggers[i];
       if( t.beatPos >= ctx.blockStartBeat
               && t.beatPos <= ctx.blockEndBeat ) {
               
           let funcPtr = t.func;
           funcPtr(t.beatPos);
           this.triggers.splice(i,1); // remove the trigger
       }
   }    
};

// Set a function to be called an exact beatPos
MidiTimer.prototype.triggerAtBeat = function(func, beat) {
   this.triggers.push({
       beatPos: beat,
       func:    func
   });
};

// Set a function to be called after so many beats goes by
MidiTimer.prototype.triggerAfterBeats = function(func, beats) {

   let ctx = GetTimingInfo();

   this.triggers.push({
       beatPos: ctx.blockEndBeat + beats,
       func:    func
   });
};

// Use Date.now() as milliseconds to trigger function call, 
// Note based on BeatPos at all, just raw real time in the code
// 
MidiTimer.prototype.triggerAfterMilliseconds = function(func, ms) {
   this.mstriggers.push({
       start:  Date.now(),
       ms:     ms,
       func:   func
   });
};

/*****************************
* Example Usage
*****************************/

var myTimer = new MidiTimer;

/*******************************************
* callbacks
*******************************************/

function sendNote() {
   var event = new NoteOn;
   event.channel = 1;
   event.velocity = 100;
   event.pitch = 60;
   event.send();
   event.velocity = 0;
   event.sendAfterMilliseconds(500);
}

myTimer.triggerAtBeat(sendNote, 5);

function ProcessMIDI() {
   let ctx = GetTimingInfo();
   myTimer.process(ctx);
}
Edited by Dewdman42
Link to comment
Share on other sites

some more general comments for the OP.

 

Here is a little code bit showing that the beat position is constantly being updated, even when the transport is stopped. Load this script and watch the Tracing to see the beat position of the process block constantly being updated...

 

var NeedsTimingInfo = true;

function ProcessMIDI() {
   let ctx = GetTimingInfo();
   
   Trace(ctx.blockStartBeat);
}

 

You will notice that before you press play it will have some large beat number that is being incremented. After you hit play the beat position will drop back to the actual beats tracking the transport and when you hit stop the transport stops...but scripter keeps processing midi with incrementing beat position.

 

incrementing.thumb.jpg.b0219c0e5222b787c5864eddac794d04.jpg

 

So yes, you can definitely use Scripter to send out looping midi....while the transport is stopped....

Link to comment
Share on other sites

As I said earlier, what you are wanting to do is frankly an advanced Scripter topic, will require some sophisticated code that would be time consuming to code. What you want is possible, but not easy or simple to do.

 

Also keep in mind Mainstage also has a transport. it might make more sense for you to have MainStage playing the transport also when you want the loops playing back.

Link to comment
Share on other sites

So . . . to the OP, I think Dewdman has your answer here. Using that idea, I simply reset the start time at the end of the conditional in ProcessMidi(). I hadn't realized that we had a function here, ProcessMidi(), that gets called by the environment repeatedly. Documentation says: once per “process block,” which is determined by the host audio settings (sample rate and buffer size).. So there is your clock source.

 

The following example uses a C# (note 61) to trigger a recurring C to be played each second. Note 62 (D) stops the recurrence. Of course you can do anything you want for an event and use anything you want for the triggers.

 

var wait = 1000;   //ms
var start = 0;
var waiting = false;

function HandleMIDI(event)
{
   start = new Date().getTime();
   if (event.pitch == 61) waiting = true;
   else if (event.pitch == 62) waiting = false;
   //event.send();  //we don't have to hear these notes 
}

function ProcessMIDI() {

  let now = new Date().getTime();
  
  if(waiting && now > start + wait) {
      
      let event = new NoteOn;
      event.channel = 1;
      event.velocity = 100;
      event.pitch = 60;
      event.send();
      event.velocity = 0;
      event.sendAfterMilliseconds(500);
      //waiting = false;
      start = new Date().getTime();            
  }
}
Link to comment
Share on other sites

But also I want to reiterate...that Scripter does not run in "real time". ProcessMIDI gets called once per process block and takes an undetermined amount of time to run its code as quickly as it possibly can.. it gets its turn to do so, just like all other plugins. When all plugins have done all the DSP and crunching that they need to do, that process block worth of calculated audio is flushed to the audio buffer in your sound card and finally the sound card will stream out the audio to your speakers in real time.

 

So every midi event that you want to happen at a certain point in time of the music needs to be scheduled. By Scheduled, I mean, you assign a beat position value to the midi event.

 

When NeedsTimingInfo has been set to true, all midi events have a beatPos attribute. In the most simple case you simple assign the exact point in time as beatPos, a fractional floating point value...and when you call event.send(), that event is actually scheduled, its not sent immediately.

 

The word send, is a bit misleading to what actually happens under the covers. you call Event.send() in Scripter, what you actually do is schedule the event to be played...with the optional beatPos value assigned.

 

So what if you don't assign a beatPos or don't have NeedsTimingInfo activated? Well if you're calling Event.send() from inside HandleMIDI, then Scripter seems to know under the covers the underlying beatPos of the event argument. When you call send, it just preserves the same beatPos. From inside ProcessMIDI, I'm not sure what assumption will be made if you don't use NeedsTimingInfo, most likely it uses the start or end beat positions of the process block, one or the other. But generally if you are generating midi events over time, you should use NeedsTimingInfo=true, and you should take control of exactly the timing by setting beatPos of each midi event, then they will dutifully play exactly at the correct time, sample-accurately!

 

And by the way you can set the beatPos way into the future, way after the current process block too. Call event.send() a bunch of times to schedule many bars ahead of midi events if you wish.

 

In addition to being able to set the beatPos event attribute, you can also use Event.sendAfterMilliseconds(), Event.sendAtBeat and Event.sendAfterBeats, which are very similar as if you calculated and set the beatPos attribute explicity. Its just a way to do it relatively and without touching the beatPos attribute. They are usually a bit more useful when you are specifically NOT using NeedsTimingInfo, when the beatPos attribute is not available, you can't get it or set it, but under the covers, Scripter will make the relative calculation based on either what it knows about the event object passed to HandleMIDI or if its a new event object, then it will be relative to either the start or end of the current process block..not sure which..

Link to comment
Share on other sites

Please do. I hope you get this to work the way you want. I think Dewdman42's point is crucial. There is no real guarantee of timing outside of the grid imposed by the music timeline. If you aren't scheduling events on that timeline then the audio rendering will happen as soon as it can.

 

That being said, if you are making a simple playback looper for MainStage, ie. of the kind that guitarists often use when playing alone in coffeeshops, it might not matter. You don't need anything anywhere near sample accurate timing. Using the script above, I couldn't hear any deviation from regular intervals. But then I was simply using a single electric piano plugin on a single track with no other audio plugins.

Link to comment
Share on other sites

In addition to producing sample accurate results...the actual script programming will be considerably more simple if you just schedule the events rather then trying to use a timer. I included some Timer experiments above just for the curious minds that are interested in how to do it, but I don't consider that at all the right way to handle most situations in Scripter for scheduling midi event playback.

 

I think a Timer might be more interesting if you actually had some kind of special code that you need to run periodically and generate stuff that couldn't be generated ahead of time.

Link to comment
Share on other sites

recording midi notes and CC’s on the fly

How are you doing this right now?

Bump.

 

I'm still curious on what program you're using to record midi notes and CC's on the fly.

You can't be using MainStage since MainStage doesn't record MIDI, it only records AUDIO.

267385575_AudioOnly.png.e7c44083bc1c9ff827029be46e47e371.png

The record button you see only records AUDIO

AudioFileOnly.thumb.png.8b6550301b09d0de95c7545d411bebdf.png

 

To use Scripter the way you want to by adding arrays with the note/CC information still requires another program that will take a MIDI file to convert it into this Scripter array data but then you can only cut-and-paste that into Scripter and you need to run the Scripter first to make sure there''s no syntax errors.

 

If my hunch is right, you're looking for a loop back device like those hardware devices that can you can just record a loop, then append over that loop..., but for MIDI.

 

So again, what program are you using to record midi notes and CC's on the fly?

Link to comment
Share on other sites

Sorry Valli, I overlooked your question... I do record the MIDI purely in Scripter and it works very good. What I do is simply push the incoming midi events in an array. This function is triggered by a Rec/Stop checkbox parameter which can be assigned to whatever screen control for on-the-fly use. I haven’t found any drawbacks to this or the need of anything outside of Scripter. I simply need to record “silent” Midi messages for indicating the beginning and the end of the recording (with ParameterChanged) and the array contains all the information I need and is pretty much the equivalent of a region in Logic.
Link to comment
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.
Note: Your post will require moderator approval before it will be visible.

Guest
Reply to this topic...

×   Pasted as rich text.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

×
×
  • Create New...