benbravo Posted February 24, 2021 Share Posted February 24, 2021 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! Quote Link to comment Share on other sites More sharing options...
des99 Posted February 24, 2021 Share Posted February 24, 2021 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? Quote Link to comment Share on other sites More sharing options...
benbravo Posted February 24, 2021 Author Share Posted February 24, 2021 thanks! yes you can get sync infos with NeedsTimingInfo = true. I guess then the only possibility I have is to make this script while logic is running. l'll be looking into it. thanks for the reply. Quote Link to comment Share on other sites More sharing options...
ValliSoftware Posted February 24, 2021 Share Posted February 24, 2021 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. Quote Link to comment Share on other sites More sharing options...
Dewdman42 Posted February 24, 2021 Share Posted February 24, 2021 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? Quote Link to comment Share on other sites More sharing options...
Dewdman42 Posted February 24, 2021 Share Posted February 24, 2021 Also Logic Pro is always running even when the transport is not actually moving. You can still use RELATIVE beat positions or millisecond delays to make things play back even when the transport is stopped Quote Link to comment Share on other sites More sharing options...
benbravo Posted February 25, 2021 Author Share Posted February 25, 2021 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? Quote Link to comment Share on other sites More sharing options...
Dewdman42 Posted February 25, 2021 Share Posted February 25, 2021 Why do you need scripter to do that? What you wish is achievable but not a script for beginners to do Quote Link to comment Share on other sites More sharing options...
des99 Posted February 25, 2021 Share Posted February 25, 2021 because I need to be able to do that without starting the timeline. I'm curious as to what you're doing that running the transport (even in a long cycle) is not an option? (But not so curious I won't get any sleep if you don't want to go into details... ) Quote Link to comment Share on other sites More sharing options...
ValliSoftware Posted February 25, 2021 Share Posted February 25, 2021 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. Quote Link to comment Share on other sites More sharing options...
des99 Posted February 25, 2021 Share Posted February 25, 2021 Yep, I was thinking of the live loops as a possibility as well, as I think they run independently of the transport... It's why I was asking about what the OP was trying to do... Quote Link to comment Share on other sites More sharing options...
benbravo Posted February 25, 2021 Author Share Posted February 25, 2021 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... Quote Link to comment Share on other sites More sharing options...
des99 Posted February 26, 2021 Share Posted February 26, 2021 Ah, I see. Thanks for the clarification..! Quote Link to comment Share on other sites More sharing options...
ValliSoftware Posted February 26, 2021 Share Posted February 26, 2021 recording midi notes and CC’s on the fly How are you doing this right now? Quote Link to comment Share on other sites More sharing options...
88keys Posted February 26, 2021 Share Posted February 26, 2021 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++; } Quote Link to comment Share on other sites More sharing options...
88keys Posted February 26, 2021 Share Posted February 26, 2021 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. Quote Link to comment Share on other sites More sharing options...
Dewdman42 Posted February 27, 2021 Share Posted February 27, 2021 (edited) 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 February 27, 2021 by Dewdman42 Quote Link to comment Share on other sites More sharing options...
Dewdman42 Posted February 27, 2021 Share Posted February 27, 2021 (edited) 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 February 27, 2021 by Dewdman42 Quote Link to comment Share on other sites More sharing options...
Dewdman42 Posted February 27, 2021 Share Posted February 27, 2021 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. So yes, you can definitely use Scripter to send out looping midi....while the transport is stopped.... Quote Link to comment Share on other sites More sharing options...
Dewdman42 Posted February 27, 2021 Share Posted February 27, 2021 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. Quote Link to comment Share on other sites More sharing options...
88keys Posted February 27, 2021 Share Posted February 27, 2021 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(); } } Quote Link to comment Share on other sites More sharing options...
Dewdman42 Posted February 28, 2021 Share Posted February 28, 2021 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.. Quote Link to comment Share on other sites More sharing options...
benbravo Posted February 28, 2021 Author Share Posted February 28, 2021 hey guys, thanks. wow, so much info here. much to learn from! i'm slowly getting my head around it and trying to get something working in my script. Looks promising and I should be getting somewhere. I'll keep you posted! Quote Link to comment Share on other sites More sharing options...
88keys Posted February 28, 2021 Share Posted February 28, 2021 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. Quote Link to comment Share on other sites More sharing options...
Dewdman42 Posted February 28, 2021 Share Posted February 28, 2021 Mainstage also has a transport and can schedule notes exactly on schedule!!! it works exactly the same way in Scripter as it does in LogicPro. Quote Link to comment Share on other sites More sharing options...
Dewdman42 Posted February 28, 2021 Share Posted February 28, 2021 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. Quote Link to comment Share on other sites More sharing options...
Dewdman42 Posted February 28, 2021 Share Posted February 28, 2021 another thing work pointing out is that a typical process block represents less then 1 ms of time @ 256 buffer size. Nobody will hear even ten times that much of slop. Quote Link to comment Share on other sites More sharing options...
Dewdman42 Posted February 28, 2021 Share Posted February 28, 2021 I did this MainStage Midi Player a while back..you might be able to adapt it for playing a loop.. or else just generate a really long sequence that goes so long it might as well be the same as if its looped. https://gitlab.com/dewdman42/MainstageMidiPlayer/-/wikis/home Quote Link to comment Share on other sites More sharing options...
ValliSoftware Posted February 28, 2021 Share Posted February 28, 2021 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. The record button you see only records AUDIO 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? Quote Link to comment Share on other sites More sharing options...
benbravo Posted February 28, 2021 Author Share Posted February 28, 2021 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. Quote Link to comment Share on other sites More sharing options...
Recommended Posts
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.