Jump to content

benbravo

Member
  • Posts

    49
  • Joined

  • Last visited

benbravo's Achievements

Newbie

Newbie (1/14)

0

Reputation

  1. ok good! Yes I noticed and was going to ask if sendAfter can be cancel anyhow, which you answered in a previous post (even by bypassing scripter?). That doesn't really matter to me, I actually like the musical idea of having the loop playing to the end before it stops, but I also thought of the case of mistakenly sending out a huge loop which would play for minutes (or hours, or days...) without any way of stopping it (shiver). I guess I should programm some kind of limit to the size of the loop, or the allowed recording time, just in case...
  2. Ok, I see... Let's check if I understand correctly. Sorry if you feel like going around in circles with me.... What happens during my handling of ProcessMIDI is that once "loopLength" is over, the whole array of notes is scanned with a for loop and notes are passed to sendAfterMilliesconds with their corresponding timings to make it play back correctly. From my understanding so far, this all happens "at once", so I don't feel that (paraphrasing you) "I am having the function wait for time to go by in order to do so". You mean, I'd be getting better results sending one note after the other? Only one send call per process block is advised? Here's my (simplified) code for that bit : function ProcessMIDI() { var info = GetTimingInfo(); if(ScheduledBeat != 0 && loop.length != 0 && info.blockStartBeat > ScheduledBeat){ for ( let i = 0; i < loop.length; i++){ let tempoRatio = recordedAtBpm/timingInfo.tempo; loop[i].sendAfterMilliseconds(timings[i]*tempoRatio); } ScheduledBeat = info.blockStartBeat + LoopLength; } }
  3. Thanks. I am glad to see your example code does actually look pretty similar, as far as I can tell, to what I did in the first version of my script (using date time). In my second version, I switched to calculating milliseconds from beatPos using simple math. If you think the getTime method does not require too much CPU, I will probably revert to it, as long as the MainStag beatPos values behave as they do (I submitted this to Apple), which allows me to record without the transport running. I think my biggest problem was then indeed all those GetParameter calls, because your method inside ProcessMIDI is also similar to mine (now > (last loop start + loop length)? play loop : wait till next process block). Next step for me is getting rid of all new Note call inside ProcessMIDI. I know about the momentary type, which I used for "reset" and "one shot". I prefer the checkbox for "rec/stop" because it can give me a visual feedback of the state of the recording on my controllers. Another two quick coding questions: you seem to be using var for declaring variables at the global scope and let for variables inside functions. Is this the best/most efficient way to do? I tend to use one or the other more depending on my mood than anything else (although I think I know let has a more restricted scope)... I also have a lot of global variables, is this something to avoid? Thanks for all your time, if I happened to be in Salt Lake City (which I don't, at all ), I'd invite you for a beer!
  4. yes. the funny thing is it DOES run when you start Mainstage, although the play button is not lit... that's how I started using getTime for keeping track of the milliseconds. but what I actually need, is a plugin that works and is not crashing the session, so having the transport running in exchange, is something I can deal with.
  5. oh no... it does work! I promise hit rec, play a couple notes, hit stop and then you can play around with the different other options... No success?? I tested it thoroughly today with my full setup on MainStage (3 external synth + sampled Electric piano and effects plugins) and it felt a lot better than my first attempt, without the CPU hitting the ceiling. No crash, audio dropouts, lost notes or anything whatsoever in about 30 minutes of playing around with it. Needs to be tested some more before I gain enough confidence, but it's a promising progress, thanks to you! My next step will then be to create all new stuff outside of ProcessMIDI, which I do sometimes quite intensively with the "sequencer" (for creating notes off that weren't actually recorded) and "octave" (for creating the parallel octaves) functions. I kind of started working on that earlier today but noticed that it implies quite some rethinking of the architecture, so I left it for now... But that thing about memory usage does encourage me to get my head around it. Well, if I didn't insist on trying to use sendAfterBeat and sendAtBeat, it's partly due to the fact that the MainStage timeline doesn't work the same as Logic's. For some reason, when you hit stop, blockStartBeat stops counting too, although it seems to be counting on startup, before you hit start for the first time... This means that the beatPos values of notes recorded without the timeline running are all (about) the same and unusable. This lead me to using milliseconds because I wanted (for clock sync reasons) to be able also to record and playback loops without the timeline running. But my new version of the code does use the beatPos for calculating times, so I guess I will HAVE to have the timeline running and better start studying how to use sendAtBeat properly to get rid of the extra Math. Can you understand this behaviour of the MainStage timeline? Maybe a bug? I tested and it behaves the same in MS 3.5.1 (latest) and 3.4.4.. (Glad if you give me a feedback on your next attempt of actually using the script )
  6. Thanks, i am coming forward. I tested your new version of GuiParameters, and it works. Super. I also went through my entire code and tried to simplify it as much as possible with my growing understanding of processMIDI and scripter coding. I got rid of all getTime() occurrences and used beatPos for calculating timings. ProcessMIDI now waits until the loop length has elapsed (looking at blockStartBeat) before sending out all notes of the next iteration. When the loop is sent out, there might still be some processing needed for excuting the Transpose or AddOctaves functions, depending on the chosen parameters. Do you think this is too much for being inside processMIDI? Somehow I was not able to use the sendAfterBeats or sendAtBeat but only sendAfterMilliseconds gave me the expected result. Is there any performance reason for choosing one over the other? I still get (smaller) CPU spikes (up to 40%-50%) while the plugin just sits and waits. I still have to go through the GUI and decide whether I can simplify it. Here is the complete code if you want to give it a try: /* MIDI looper Records midi "regions" and plays them back. Rec/Stop - starts recording on the first note played, ends on "stop". starting a new record erases the previous one (no overdub possible) One shot - plays the sequence just once, as recorded Loop as played - plays the sequence in loop as recorded Loop on grid - quantizes the end of the sequence relative to "End of cycle quantization" choice End of cycle quantization - the length of the region is adjusted to the next grid value Sequencer - the recorded notes are transformed into "steps" (only works with notes) Sequencer grid - the duration of one step Sequencer note length - the duration of each note Play order - the recorded sequence can be reversed or played back and forth Reset - Clears the region and sets back all parameters to default Transpose - Transposes the region up to two octaves up or down Add octave below, add octave above - */ var NeedsTimingInfo = true; //GetParameter alternative var GuiParameters = { data: [], set: function(id, val) { if(typeof id != "string") id = PluginParameters[id].name; this.data[id] = val; }, get: function(id) { if(typeof id != "string") id = PluginParameters[id].name; if(this.data[id] == undefined) { this.data[id] = GetParameter(id); } return this.data[id]; } }; //arrays for storing midi notes for different types of playbacks var recordedLoop = []; var recordedTimes = []; var reversedTimes = []; var reversedLoop = []; var pingpongLoop = []; var pingpongTimes = []; var sequencerLoop = []; var sequencerTimes = []; //the arrays for the actual playback var loop = []; var times = []; //other variables var firstBP = 0; var recordedAtBpm = 120; var elapsed = 0; var notesCount = 0; var recIsStopped = 1; var ScheduledBeat = 0; var sequencerLoopLength = 0; var LoopLength = 0; var EndOfCycleQuantize = ["Next 1/8 note","Next beat","Bars of 2 beats","Bars of 3 beats", "Bars of 4 beats", "Bars of 5 beats", "Bars of 6 beats", "Bars of 7 beats"] var ListenMidiChannels = ["All"]; var sequencerGrid = ["Whole note", "1/2.", "1/2 note", "1/4.", "1/2T", "1/4 note", "1/8.", "1/4T", "1/8 note", "1/16.", "1/8T", "1/16 note", "1/32.", "1/16T", "1/32", "1/32T",]; var sequencerGridRatios = [4,3,2,1.5,1.3333,1,0.75,0.6666,0.5,0.375,0.3333,0.25,0.1875,0.16666,0.125,0.083333]; var playOrder = ["As played","Reverse","Back&Forth"]; var rate = 0; var noteLength = 0; var rateTiming = 0; var noteLengthTiming = 0; var MidiChannels = ["As recorded"]; for(let i = 1; i<=16;i++){ MidiChannels.push("Channel " + i); ListenMidiChannels.push("Channel " + i); } var PluginParameters = [ {name: "Listen to channel", type:"menu", valueStrings:ListenMidiChannels, defaultValue:0}, {name: "Rec/Stop", type:"checkbox", defaultValue:0}, {name:"One shot", type:"momentary", defaultValue:127}, {name:"Loop as played", type:"checkbox", defaultValue:0}, {name:"Loop on grid", type:"checkbox", defaultValue:0}, {name: "End of cycle quantization", type:"menu", valueStrings:EndOfCycleQuantize, defaultValue:1}, {name:"Sequencer", type:"checkbox", defaultValue:0}, {name: "Sequencer grid", type:"menu", valueStrings:sequencerGrid, defaultValue:0}, {name: "Sequencer note length", type:"menu", valueStrings:sequencerGrid, defaultValue:0}, {name: "Play order", type:"menu", valueStrings:playOrder, defaultValue:0}, {name:"Reset", type:"momentary", defaultValue:0}, {name:"Transpose", type:"slider", numberOfSteps: 48, minValue: -24, maxValue:24, defaultValue:0}, {name: "Add octave below", type:"checkbox", defaultValue:0}, {name: "Add octave above", type:"checkbox", defaultValue:0}, {name:"Output", type:"menu", valueStrings:MidiChannels, defaultValue:0}, {name:"Thru", type:"checkbox", defaultValue:1} ]; var OutChannel = 1; function HandleMIDI(e){ if(GuiParameters.get("Output") == 0){ if(e.channel){ OutChannel = e.channel; } else{ OutChannel + 1; } } else { OutChannel = GuiParameters.get("Output"); e.channel = OutChannel; } if(GuiParameters.get("Rec/Stop") == 1 && (GuiParameters.get("Listen to channel") == 0 || e.channel == GuiParameters.get("Listen to channel"))){ RecordNotes(e); } if(GuiParameters.get("Thru") == 1){ e.send(); } } function ProcessMIDI() { var info = GetTimingInfo(); if(ScheduledBeat != 0 && info.blockStartBeat > ScheduledBeat){ OneShot(info,loop,times); if(GuiParameters.get("Loop as played") == 1){ ScheduledBeat = info.blockStartBeat + LoopLength; } else if(GuiParameters.get("Loop on grid") == 1){ //rounds to the half beat if needed GuiParameters.get("End of cycle quantization") == 0 ? ScheduledBeat = info.blockStartBeat+(0.5-(info.blockStartBeat%0.5)) + LoopLength : ScheduledBeat = Math.ceil(info.blockStartBeat) + LoopLength; } else if(GuiParameters.get("Sequencer") == 1){ ScheduledBeat = (info.blockStartBeat-(info.blockStartBeat%sequencerGridRatios[rate])) + LoopLength; } } } //Recording incoming events function RecordNotes(e) { var info = GetTimingInfo(); if(e instanceof Note || (e instanceof ControlChange && e.number == 64 ) || (e instanceof ControlChange && e.number == 01) || (e instanceof PolyPressure) || (e instanceof PitchBend)){ //recording the timings in ms for using in sendAfterMilliseconds if(notesCount == 0) { recordedTimes.push(0); firstBP = e.beatPos; } else if(notesCount != 0){ elapsed = (60000/(info.tempo))*(e.beatPos-firstBP); recordedTimes.push(elapsed); } //recording the notes recordedLoop.push(e); } notesCount++; } function OneShot(timingInfo,notes,timings){ for ( let i = 0; i < notes.length; i++){ //Transpose if needed if(GuiParameters.get("Transpose") != 0 && notes[i] instanceof Note){ let tempoRatio = recordedAtBpm/timingInfo.tempo; Transpose(notes[i], timings[i]*tempoRatio); } //Send according to recorded timings else{ let tempoRatio = recordedAtBpm/timingInfo.tempo; notes[i].sendAfterMilliseconds(timings[i]*tempoRatio); if(GuiParameters.get("Sequencer") == 1){ var off = new NoteOff(notes[i]); off.sendAfterMilliseconds(timings[i]+noteLengthTiming); } AddOctave(notes[i], timings[i]*tempoRatio); } } } function Transpose(note, time){ if(note instanceof NoteOn){ var transposedNote = new NoteOn(note); } else { var transposedNote = new NoteOff(note); } transposedNote.pitch = transposedNote.pitch + GuiParameters.get("Transpose"); transposedNote.sendAfterMilliseconds(time); if(GuiParameters.get("Sequencer") == 1){ let off = new NoteOff(transposedNote); off.sendAfterMilliseconds(time + noteLengthTiming); } AddOctave(transposedNote,time); } function AddOctave(note, time){ if((GuiParameters.get("Add octave below") == 1 || GuiParameters.get("Add octave above") == 1) && (note instanceof Note)){ if(note instanceof NoteOn){ if(GuiParameters.get("Add octave below") == 1){ var octaveBelow = new NoteOn(note); octaveBelow.pitch -=12; } if(GuiParameters.get("Add octave above") == 1){ var octaveAbove = new NoteOn(note); octaveAbove.pitch +=12; } if(GuiParameters.get("Sequencer") == 1){ if(GuiParameters.get("Add octave below") == 1){ var octaveBelowOff = new NoteOff(note); octaveBelowOff.pitch -=12; } if(GuiParameters.get("Add octave above") == 1){ var octaveAboveOff = new NoteOff(note); octaveAboveOff.pitch +=12; } } } else if(note instanceof NoteOff){ if(GuiParameters.get("Add octave below") == 1){ var octaveBelow = new NoteOff(note); octaveBelow.pitch -=12; } if(GuiParameters.get("Add octave above") == 1){ var octaveAbove = new NoteOff(note); octaveAbove.pitch +=12; } } if(GuiParameters.get("Add octave below") == 1){ octaveBelow.sendAfterMilliseconds(time); if(GuiParameters.get("Sequencer") == 1){ octaveBelowOff.sendAfterMilliseconds(time+noteLengthTiming); } } if(GuiParameters.get("Add octave above") == 1){ octaveAbove.sendAfterMilliseconds(time); if(GuiParameters.get("Sequencer") == 1){ octaveAboveOff.sendAfterMilliseconds(time+noteLengthTiming); } } } } function PrepareSequencerData(timingInfos){ //prepares arrays for sequencer var timings = []; sequencerLoop = []; sequencerTimes = []; rate = GuiParameters.get("Sequencer grid"); noteLength = GuiParameters.get("Sequencer note length"); rateTiming = (60000/timingInfos.tempo)*sequencerGridRatios[rate]; noteLengthTiming = (60000/timingInfos.tempo)*sequencerGridRatios[noteLength]; for(let i=0 ; i<loop.length; i++){ if(loop[i] instanceof NoteOn){ sequencerLoop.push(loop[i]); timings.push(times[i]); } } let steps = 0; for(let j=0 ; j < timings.length; j++){ if(j == 0){ sequencerTimes[j] = 0; steps++; } //same timing for notes played less than 15ms apart else if(j != 0 && timings[j]-timings[j-1] < 15){ sequencerTimes[j] = sequencerTimes[j-1]; } else{ sequencerTimes[j] = rateTiming * steps; steps++; } } sequencerLoopLength = steps * sequencerGridRatios[rate]; } function makeReversedLoop(forwardLoop, forwardTimes){ var reversedLoopRaw = []; var reversedTimesRaw = []; //make new reversed array from loop and times for(let i=0 ; i<forwardLoop.length;i++){ reversedLoopRaw[i] = forwardLoop[i]; } for(let i=0 ; i<forwardTimes.length;i++){ reversedTimesRaw[i] = forwardTimes[i]; } reversedLoopRaw = reversedLoopRaw.reverse(); reversedTimesRaw = reversedTimesRaw.reverse(); //reverse the timings and put the cc64 closing event timing at the end again for(let i=0 ; i<reversedTimesRaw.length;i++){ if(i == 0){ reversedTimes[i] = 0; } else{ reversedTimes[i] = reversedTimesRaw[i-1]-reversedTimesRaw[i]+reversedTimes[i-1]; } } toSubstract = reversedTimes[1]; reversedTimes.push(reversedTimes[reversedTimes.length-1]+reversedTimes[1]); reversedTimes.splice(0,1); for(let i=0 ; i<reversedTimesRaw.length;i++){ reversedTimes[i] -= toSubstract; } //reverse notes (noteOn <-> noteOff) //starting cc64 goes at the end reversedLoopRaw.push(reversedLoopRaw[0]); reversedLoopRaw.splice(0,1); for(let i=0 ; i<reversedLoopRaw.length;i++){ if(reversedLoopRaw[i] instanceof NoteOn){ reversedLoop[i] = new NoteOff(reversedLoopRaw[i]); } else if(reversedLoopRaw[i] instanceof NoteOff){ reversedLoop[i] = new NoteOn(reversedLoopRaw[i]); } else{ reversedLoop[i] = reversedLoopRaw[i]; } } //swapping noteOn and noteOff velocities for(let i=0 ; i<reversedLoop.length;i++){ if(reversedLoop[i] instanceof NoteOn){ var onVelocity = reversedLoop[i].velocity; for(let j=i; j<reversedLoop.length;j++){ if(reversedLoop[j] instanceof NoteOff && reversedLoop[j].pitch == reversedLoop[i].pitch){ reversedLoop[i].velocity = reversedLoop[j].velocity; reversedLoop[j].velocity = onVelocity; break; } } } } } function makePingPongLoop(forwardLoop, forwardTimes){ //putting forward and backward loops together let looplength = forwardLoop.length; if(reversedLoop.length == 0){ makeReversedLoop(forwardLoop, forwardTimes) } pingpongLoop = [...forwardLoop]; pingpongLoop = pingpongLoop.concat(reversedLoop); pingpongTimes = [...forwardTimes]; //correcting the timings for the second half of the loop var timeToAdd = pingpongTimes[pingpongTimes.length-1]; pingpongTimes = pingpongTimes.concat(reversedTimes); for(let i = looplength;i<pingpongTimes.length;i++){ pingpongTimes[i] = pingpongTimes[i] + timeToAdd; } } //getting ready for playback function prepareLoopAndTimes(timingInfos){ if(GuiParameters.get("Play order") == 0){ loop = recordedLoop; times = recordedTimes; } else if(GuiParameters.get("Play order") == 1){ loop = reversedLoop; times = reversedTimes; } else if(GuiParameters.get("Play order") == 2){ loop = pingpongLoop; times = pingpongTimes; } LoopLength = CalculateLoopLength(times[times.length-1],timingInfos); PrepareSequencerData(timingInfos); } //Calculate loop length function CalculateLoopLength(loopTiming,timingInfo){ var msPerBeat = (60000/(timingInfo.tempo)); if(GuiParameters.get("Loop as played") == 0){ LoopLength = (loopTiming/msPerBeat) - ((loopTiming/msPerBeat)%0.5); var endOfCycle = GuiParameters.get("End of cycle quantization"); if(endOfCycle == 0){ return LoopLength; } var remainder = (LoopLength + 1) % (endOfCycle); if(remainder != 0){ var toAdd = (endOfCycle) - remainder; LoopLength = LoopLength + toAdd; return LoopLength; } else return LoopLength; } else if(GuiParameters.get("Loop as played") == 1){ LoopLength = loopTiming/msPerBeat; return LoopLength; } } //Send all notes off of OutChannels function SendAllNotesOff(channel){ var aNo = new ControlChange; aNo.number = 123; aNo.value = 0; aNo.channel = channel; aNo.send(); } //Check for possible missing NotesOff in the loop -> push them at the end function MissingNotesOffCheck(notes, endOfLoopTime){ let notesToCheck = []; let missingOffs = []; notesToCheck = [...notes]; for(var i = 0; i < notes.length ; i++){ if(notes[i] instanceof NoteOn){ var found = false; for(var j = 0; j < notesToCheck.length ; j++){ if(notesToCheck[j] instanceof NoteOff && notesToCheck[j].pitch == notes[i].pitch){ notesToCheck.splice(j,1); var found = true; break; } } if(found == false){ missing = new NoteOff(notes[i]); missingOffs.push(missing); } } } //adds the timing of the last recorded event in the loop //to match the added missing offs for(var k = 0; k < missingOffs.length ; k++){ recordedTimes.push(endOfLoopTime); } recordedLoop = recordedLoop.concat(missingOffs); } function ParameterChanged(param, value){ GuiParameters.set(param, value); var info = GetTimingInfo(); //Rec if(param == 1 && value == 1){ if(GuiParameters.get("Sequencer") == 1){ SetParameter("Sequencer",0); } if(GuiParameters.get("Loop on grid") == 1){ SetParameter("Loop on grid",0); } if(GuiParameters.get("Loop as played") == 1){ SetParameter("Loop as played",0); } //clearing data recordedLoop = []; recordedTimes = []; sequencerLoop = []; sequencerTimes = []; reversedLoop = []; reversedTimes = []; pingpongLoop = []; pingpongTimes = []; LoopLength = 0; ScheduledBeat = 0; elapsed = 0; notesCount = 0; recIsStopped = 0; firstBP = 0; } //Stop else if(param == 1 && value == 0 && recordedLoop.length > 0){ //gathering missing closing infos //possible missing noteoff... elapsed = (60000/(info.tempo))*(info.blockStartBeat - firstBP); MissingNotesOffCheck(recordedLoop, elapsed); //...closing the loop (for timing + safe pedal info) with sustain off... var closeSustain = new ControlChange(recordedLoop[notesCount-1]); closeSustain.number = 64; closeSustain.value = 0; closeSustain.beatPos = info.blockStartBeat; recordedLoop.push(closeSustain); recordedTimes.push(elapsed); //store original tempo of the loop recordedAtBpm = info.tempo; //prepares data makeReversedLoop(recordedLoop,recordedTimes); makePingPongLoop(recordedLoop,recordedTimes); prepareLoopAndTimes(info); //validate end of rec -> looping can start recIsStopped = 1; SendAllNotesOff(OutChannel); } //One Shot else if(param == 2 && value == 1 && recordedLoop.length > 0){ OneShot(info,loop,times); } //(grid)Loop on else if((param == 3 && value == 1 || (param == 4 && value == 1)) && recordedLoop.length > 0){ if(GuiParameters.get("Rec/Stop") == 1){ SetParameter("Rec/Stop",0); } if(GuiParameters.get("Sequencer") == 1){ SetParameter("Sequencer",0); } //loop as played if(param == 3 && value == 1){ if(GuiParameters.get("Loop on grid") == 1){ SetParameter("Loop on grid",0); } CalculateLoopLength(times[times.length-1],info); //for starting right away: ScheduledBeat = 1; } //loop on grid else if(param == 4 && value == 1){ if(GuiParameters.get("Loop as played") == 1){ SetParameter("Loop as played",0); } if(GuiParameters.get("End of cycle quantization") != 0){ //for starting on the next beat ScheduledBeat = Math.ceil(info.blockStartBeat); } if(param == 4 && GuiParameters.get("End of cycle quantization") == 0){ //for starting on the next half beat ScheduledBeat = info.blockStartBeat+((info.blockStartBeat%0.5)+0.5); } CalculateLoopLength(times[times.length-1],info); } } //(grid)Loop off else if(param == 3 && value == 0 || (param == 4 && value == 0)){ ScheduledBeat = 0; if(OutChannel){ SendAllNotesOff(OutChannel); } } //End of cycle quantize else if(param == 5 && recordedLoop.length > 0){ CalculateLoopLength(times[times.length-1],info); } //Sequencer else if(param == 6 && recordedLoop.length > 0){ if(value == 1){ if(GuiParameters.get("Rec/Stop") == 1){ SetParameter("Rec/Stop",0); } if(GuiParameters.get("Loop on grid") == 1){ SetParameter("Loop on grid",0); } if(GuiParameters.get("Loop as played") == 1){ SetParameter("Loop as played",0); } //arrays ready for playback LoopLength = sequencerLoopLength; times = sequencerTimes; loop = sequencerLoop; ScheduledBeat = Math.ceil(info.blockStartBeat); } if(value == 0){ prepareLoopAndTimes(info); ScheduledBeat = 0; } } //Sequencer grid else if(param == 7 && recordedLoop.length > 0){ PrepareSequencerData(info); LoopLength = sequencerLoopLength; times = sequencerTimes; loop = sequencerLoop; ScheduledBeat = Math.ceil(info.blockStartBeat); } //Sequencer note length else if(param == 8 && recordedLoop.length > 0){ noteLength = GuiParameters.get("Sequencer note length"); noteLengthTiming = (60000/info.tempo)*sequencerGridRatios[noteLength]; } //Play order else if(param == 9 && recordedLoop.length > 0){ prepareLoopAndTimes(info); if(GuiParameters.get("Sequencer") == 1){ LoopLength = sequencerLoopLength; times = sequencerTimes; loop = sequencerLoop; ScheduledBeat = Math.ceil(info.blockStartBeat); } } //Reset else if(param == 10 && value == 1){ recordedLoop = []; recordedTimes = []; reversedLoop = []; reversedTimes = []; pingpongLoop = []; pingpongTimes = []; sequencerLoop = []; sequencerTimes = []; LoopLength = 0; ScheduledBeat = 0; firstBP = 0; SetParameter("Loop as played",0); SetParameter("Loop on grid",0); SetParameter("Rec/Stop",0); SetParameter("Sequencer",0); SetParameter("Add octave below",0); SetParameter("Add octave above",0); SetParameter("Play order",0); SetParameter("Transpose",0); if(OutChannel){ SendAllNotesOff(OutChannel); } } //Transpose else if(param == 11){ if(OutChannel){ SendAllNotesOff(OutChannel); } } //Output channel change else if(param == 14 && value != 0){ OutChannel = value; for(let i=0; i<recordedLoop.length;i++){ recordedLoop[i].channel = OutChannel; } if(OutChannel){ SendAllNotesOff(OutChannel); } } }
  7. Alright. i think i begin to understand where my problem is and in which direction I should head. You shouldn't waste too much time understanding my code, I will try to do something different based on scheduling rather than calculating the current timing. I think I can see a way to go. I will challenge you with the new version once I get there. One thing I am not sure to understand 100% is when you say: I can see how to "get out" of HandleMIDI, but how do you mean that for ProcessMIDI? As I understand, it DOES sit around and wait once it's called, doesn't it? That is how it will know that the time for sending the next bunch of notes has come, so it will have to do some conditions checking every process block. I tried, but it unfortunately it doesn't work. I solved it with my code, getting the strings out of the PluginParameters object, and it works now. I have no idea if my way to do is legit but it works.
  8. ok. so I replaced all occurences of GetParameter with your function, and as you predicted that makes quite a difference already, I seem to have more than halved the CPU spikes, beautiful! Since I was calling the parameters by their names rather than ids for practical purposes with GetParameters, I modified your function accordingly : var GuiParameters = { set: function(id, val) { this[PluginParameters[id]["name"]] = val; }, get: function(paramName) { if(this[paramName] == undefined) { this[PluginParameters["name"]] = GetParameter(paramName); } return this[paramName]; } } Is this a correct way to do it? Would I be gaining significant efficiency by calling the parameters by their ids? I might change it back when the script is finished and parameters won't be moving around anymore. I also checked the SetParameter calls inside ParameterChanged. It seems not to generate any loop or anything wild whatsoever. But I will be looking if there is a way of reducing the GUI updates. One problem I still see is I keep on getting CPU spikes (now smaller but still about 25% of one thread in Logic's Performance Meter) while the plugin is just waiting. Seems to come from ProcessMIDI going through the 3 conditions. Can you think of a way to optimizing this also? function ProcessMIDI() { var info = GetTimingInfo(); //Loop starts only if loop is recorded and prepared... if(GuiParameters.get("Loop as played") == 1 && recIsStopped == 1 && loop.length > 0){ Loop(info,loop,times); } //Grid loop starts only if loop is recorded, transport playing, and beatPos coherent else if(GuiParameters.get("Loop on grid") == 1 && recIsStopped == 1 && info.playing && loop.length > 0 && (Math.floor(loop[0].beatPos * 100) / 100 != Math.floor(loop[1].beatPos * 100) / 100)){ LoopOnGrid(info,loop,times); } //Sequencer starts only if loop is recorded and prepared else if(GuiParameters.get("Sequencer") == 1 && recIsStopped == 1 && sequencerLoop.length > 0){ LoopOnGrid(info,sequencerLoop,sequencerTimes); } }
  9. Thanks Dewdman, super valuable information, as always. I'll try improving those aspects and get back here when it's done. I'm hopeful that avoiding the many GetParameter calls inside ProcessMIDI will already make a difference. As for GUI Parameters they are there because I need the plugin functions to be accessible to my midi controllers. I guess I could also keep everything inside the hood with normal variables, and send CC values to change the various parameters in HandleMIDI. It's less attractive though as I won't be able to get a visual feedback of where the plugin is at. I'll start with the GetParameter and checking for loops at ParamaterChanged calls. We'll see where I get with that.
  10. Hey Logic scripter community, Need help optimizing my code (or understanding the limits of Logic Scripter). First off I am not at all developer, just a musician... I love to use the Scripter plugin for different little things needed in my creative workflow as a keyboard player. I have no education in software programming, just patience and curiosity, so I do code "empirically" by try and error, grabbing stuff here and there, and it's usually good enough for making little simple tasks work. I got into this more complex project after searching the web for some kind of "MIDI looper" plugin and finding nothing that fits my needs. (I work in MainStage and want to record MIDI "regions/sequences" on the fly, of any length, and looping them back as played (not quantizing the notes), if possible also without the need of the timeline running). I decided to give it a try in Scripter. I succeeded making something that pretty much works the way I want and even added a few functionality just for fun. It works in Logic just the same, of course. The problem is I am getting big CPU spikes as soon as the plugin is loaded (even idle), it uses one core up to 100%, I start getting pops and clicks, and made MainStage crash repeatedly when used in a busy session with intense MIDI processing, other virtual instruments and external synths (I work on MacBook Pro 15'', 2015, 2.8 GHz Quad-Core Intel Core i7, 16Gb). This is a no go for live use... Since I don't know anything about efficient coding, I am thinking that most probably my script has flaws. Is someone willing to take a look and tell me what's going on? - Are the CPU spikes due to bad coding? Or is it inherent to the ProcessMIDI function? - What should I be working on to get better performance? I expect no one to rewrite it or course, just a few ideas to make it better!! Here's the code. Thanks! /* MIDI looper Records midi "regions" and plays them back. Rec/Stop - starts recording on the first note played, ends on "stop". Starting a new record erases the previous one (no overdub possible) * One shot - plays the sequence just once, as recorded Loop as played - plays the sequence in loop as recorded Loop on grid - (needs timeline running) quantizes the end of the sequence relative to "End of cycle quantization" choice * End of cycle quantization - adjusts the end of the region * Sequencer - the recorded notesOn are transformed into "steps" Sequencer grid - the duration of one step Sequencer note length - the duration of each note * Play order - order of the notes: as recorded, reverse, back and forth Reset - clears the region and sets back all parameters to default Transpose, Add octave below, Add octave above - does just that */ var recordedLoop = []; // array for recorded MIDI events var recordedTimes = []; // array for timing attached to each MIDI event var sequencerLoop = []; var sequencerTimes = []; var reversedTimes = []; var reversedLoop = []; var pingpongLoop = []; var pingpongTimes = []; var loop = []; var times = []; var recordedAtBpm = 120; // recorded tempo of the loop var start = 0; var startBeat = 0; var elapsed = 0; var notesCount = 0; var recIsStopped = 1; var NextBeat = 0; var NeedsTimingInfo = true; var gridLoopLength = 0; var sequencerLoopLength = 0; var EndOfCycleQuantize = ["Next 1/8 note","Next beat","Bars of 2 beats","Bars of 3 beats", "Bars of 4 beats", "Bars of 5 beats", "Bars of 6 beats", "Bars of 7 beats"] var ListenMidiChannels = ["All"]; var sequencerGrid = ["Whole note", "1/2.", "1/2 note", "1/4.", "1/2T", "1/4 note", "1/8.", "1/4T", "1/8 note", "1/16.", "1/8T", "1/16 note", "1/32.", "1/16T", "1/32", "1/32T",]; var sequencerGridRatios = [4,3,2,1.5,1.3333,1,0.75,0.6666,0.5,0.375,0.3333,0.25,0.1875,0.16666,0.125,0.083333]; var playOrder = ["As played","Reverse","Back&Forth"]; var rate = 0; var noteLength = 0; var rateTiming = 0; var noteLengthTiming = 0; var MidiChannels = ["As recorded"]; for(let i = 1; i<=16;i++){ MidiChannels.push("Channel " + i); ListenMidiChannels.push("Channel " + i); } var PluginParameters = [ {name: "Listen to channel", type:"menu", valueStrings:ListenMidiChannels, defaultValue:0}, {name: "Rec/Stop", type:"checkbox", defaultValue:0}, {name:"One shot", type:"momentary", defaultValue:127}, {name:"Loop as played", type:"checkbox", defaultValue:0}, {name:"Loop on grid", type:"checkbox", defaultValue:0}, {name: "End of cycle quantization", type:"menu", valueStrings:EndOfCycleQuantize, defaultValue:1}, {name:"Sequencer", type:"checkbox", defaultValue:0}, {name: "Sequencer grid", type:"menu", valueStrings:sequencerGrid, defaultValue:0}, {name: "Sequencer note length", type:"menu", valueStrings:sequencerGrid, defaultValue:0}, {name: "Play order", type:"menu", valueStrings:playOrder, defaultValue:0}, {name:"Reset", type:"momentary", defaultValue:0}, {name:"Transpose", type:"slider", numberOfSteps: 48, minValue: -24, maxValue:24, defaultValue:0}, {name: "Add octave below", type:"checkbox", defaultValue:0}, {name: "Add octave above", type:"checkbox", defaultValue:0}, {name:"Output", type:"menu", valueStrings:MidiChannels, defaultValue:0}, {name:"Thru", type:"checkbox", defaultValue:1} ]; var OutChannel = 1; function Reset(){ startBeat = 0; } function HandleMIDI(e){ if(GetParameter("Output") == 0){ if(e.channel){ OutChannel = e.channel; } else{ OutChannel + 1; } } else { OutChannel = GetParameter("Output"); e.channel = OutChannel; } if(GetParameter("Rec/Stop") == 1 && (GetParameter("Listen to channel") == 0 || e.channel == GetParameter("Listen to channel"))){ RecordNotes(e); } if(GetParameter("Thru") == 1){ e.send(); } } function ProcessMIDI() { var info = GetTimingInfo(); //Loop starts only if loop is recorded and prepared... if(GetParameter("Loop as played") == 1 && recIsStopped == 1 && loop.length > 0){ Loop(info,loop,times); } //Grid loop starts only if loop is recorded, transport playing, and beatPos coherent else if(GetParameter("Loop on grid") == 1 && recIsStopped == 1 && info.playing && loop.length > 0 && (Math.floor(loop[0].beatPos * 100) / 100 != Math.floor(loop[1].beatPos * 100) / 100)){ LoopOnGrid(info,loop,times); } else if(GetParameter("Sequencer") == 1 && recIsStopped == 1 && sequencerLoop.length > 0){ LoopOnGrid(info,sequencerLoop,sequencerTimes); } } //Recording incoming events function RecordNotes(e) { if(e instanceof Note || (e instanceof ControlChange && e.number == 64 ) || (e instanceof ControlChange && e.number == 01) || (e instanceof PolyPressure) || (e instanceof PitchBend)){ if(notesCount == 0) { start = new Date().getTime(); } else if(notesCount != 0){ elapsed = new Date().getTime() - start; } recordedTimes.push(elapsed); recordedLoop.push(e); } notesCount++; } function Loop(timingInfo,notes,timings){ let now = new Date().getTime(); if(now > start + timings[notes.length-1]) { OneShot(timingInfo,notes,timings); start = new Date().getTime(); } } function LoopOnGrid(timingInfo,notes,timings){ //plays loop on the grid if(GetParameter("Sequencer") == 1){ var now = timingInfo.blockStartBeat; gridLoopLength = sequencerLoopLength; } else{ if(GetParameter("End of cycle quantization") == 0){ var now = timingInfo.blockStartBeat-(timingInfo.blockStartBeat % 0.5); } else{ var now = Math.floor(timingInfo.blockStartBeat); } } if(NextBeat == 0 ){ if(now > startBeat + gridLoopLength) { OneShot(timingInfo,notes,timings); if(GetParameter("Sequencer") == 1){ //rounding to multiple of grid value for sequencer startBeat = timingInfo.blockStartBeat-(timingInfo.blockStartBeat%sequencerGridRatios[rate]); } else{ if(GetParameter("End of cycle quantization") == 0){ startBeat = timingInfo.blockStartBeat-(timingInfo.blockStartBeat % 0.5); } else{ startBeat = Math.floor(timingInfo.blockStartBeat); } } } } else { //gridloop only stars on next beat if(now > NextBeat) { OneShot(timingInfo,notes,timings); if(GetParameter("Sequencer") == 1){ startBeat = timingInfo.blockStartBeat-(timingInfo.blockStartBeat%sequencerGridRatios[rate]); } else{ if(GetParameter("End of cycle quantization") == 0){ startBeat = timingInfo.blockStartBeat-(timingInfo.blockStartBeat % 0.5); } else{ startBeat = Math.floor(timingInfo.blockStartBeat); } } NextBeat = 0; } } } function OneShot(timingInfo,notes,timings){ for ( let i = 0; i < notes.length; i++){ //Transpose if needed if(GetParameter("Transpose") != 0 && notes[i] instanceof Note){ let tempoRatio = recordedAtBpm/timingInfo.tempo; Transpose(notes[i], timings[i]*tempoRatio); } //Send according to recorded timings else{ let tempoRatio = recordedAtBpm/timingInfo.tempo; notes[i].sendAfterMilliseconds(timings[i]*tempoRatio); if(GetParameter("Sequencer") == 1){ var off = new NoteOff(notes[i]); off.sendAfterMilliseconds(timings[i]+noteLengthTiming); } AddOctave(notes[i], timings[i]*tempoRatio); } } } function Transpose(note, time){ if(note instanceof NoteOn){ var transposedNote = new NoteOn(note); } else { var transposedNote = new NoteOff(note); } transposedNote.pitch = transposedNote.pitch + GetParameter("Transpose"); transposedNote.sendAfterMilliseconds(time); if(GetParameter("Sequencer") == 1){ let off = new NoteOff(transposedNote); off.sendAfterMilliseconds(time + noteLengthTiming); } AddOctave(transposedNote,time); } function AddOctave(note, time){ if((GetParameter("Add octave below") == 1 || GetParameter("Add octave above") == 1) && (note instanceof Note)){ if(note instanceof NoteOn){ if(GetParameter("Add octave below") == 1){ var octaveBelow = new NoteOn(note); octaveBelow.pitch -=12; } if(GetParameter("Add octave above") == 1){ var octaveAbove = new NoteOn(note); octaveAbove.pitch +=12; } if(GetParameter("Sequencer") == 1){ if(GetParameter("Add octave below") == 1){ var octaveBelowOff = new NoteOff(note); octaveBelowOff.pitch -=12; } if(GetParameter("Add octave above") == 1){ var octaveAboveOff = new NoteOff(note); octaveAboveOff.pitch +=12; } } } else if(note instanceof NoteOff){ if(GetParameter("Add octave below") == 1){ var octaveBelow = new NoteOff(note); octaveBelow.pitch -=12; } if(GetParameter("Add octave above") == 1){ var octaveAbove = new NoteOff(note); octaveAbove.pitch +=12; } } if(GetParameter("Add octave below") == 1){ octaveBelow.sendAfterMilliseconds(time); if(GetParameter("Sequencer") == 1){ octaveBelowOff.sendAfterMilliseconds(time+noteLengthTiming); } } if(GetParameter("Add octave above") == 1){ octaveAbove.sendAfterMilliseconds(time); if(GetParameter("Sequencer") == 1){ octaveAboveOff.sendAfterMilliseconds(time+noteLengthTiming); } } } } function PrepareSequencerData(timingInfos){ //prepares arrays for sequencer var timings = []; sequencerLoop = []; sequencerTimes = []; rate = GetParameter("Sequencer grid"); noteLength = GetParameter("Sequencer note length"); rateTiming = (60000/timingInfos.tempo)*sequencerGridRatios[rate]; noteLengthTiming = (60000/timingInfos.tempo)*sequencerGridRatios[noteLength]; for(let i=0 ; i<loop.length; i++){ if(loop[i] instanceof NoteOn){ sequencerLoop.push(loop[i]); timings.push(times[i]); } } let steps = 0; for(let j=0 ; j < timings.length; j++){ if(j == 0){ sequencerTimes[j] = 0; steps++; } else if(j != 0 && timings[j]-timings[j-1] < 15){ sequencerTimes[j] = sequencerTimes[j-1]; } else{ sequencerTimes[j] = rateTiming * steps; steps++; } } sequencerLoopLength = steps * sequencerGridRatios[rate]; } function makeReversedLoop(forwardLoop, forwardTimes){ var reversedLoopRaw = []; var reversedTimesRaw = []; //make new reversed array from loop and times for(let i=0 ; i<forwardLoop.length;i++){ reversedLoopRaw[i] = forwardLoop[i]; } for(let i=0 ; i<forwardTimes.length;i++){ reversedTimesRaw[i] = forwardTimes[i]; } reversedLoopRaw = reversedLoopRaw.reverse(); reversedTimesRaw = reversedTimesRaw.reverse(); //reverse timings and put cc64 end timing at the end again for(let i=0 ; i<reversedTimesRaw.length;i++){ if(i == 0){ reversedTimes[i] = 0; } else{ reversedTimes[i] = reversedTimesRaw[i-1]-reversedTimesRaw[i]+reversedTimes[i-1]; } } toSubstract = reversedTimes[1]; reversedTimes.push(reversedTimes[reversedTimes.length-1]+reversedTimes[1]); reversedTimes.splice(0,1); for(let i=0 ; i<reversedTimesRaw.length;i++){ reversedTimes[i] -= toSubstract; } //reverse notes (noteOn <-> noteOff) //starting cc64 goes at the end reversedLoopRaw.push(reversedLoopRaw[0]); reversedLoopRaw.splice(0,1); for(let i=0 ; i<reversedLoopRaw.length;i++){ if(reversedLoopRaw[i] instanceof NoteOn){ reversedLoop[i] = new NoteOff(reversedLoopRaw[i]); } else if(reversedLoopRaw[i] instanceof NoteOff){ reversedLoop[i] = new NoteOn(reversedLoopRaw[i]); } else{ reversedLoop[i] = reversedLoopRaw[i]; } } //swapping noteOn, Off velocities for(let i=0 ; i<reversedLoop.length;i++){ if(reversedLoop[i] instanceof NoteOn){ var onVelocity = reversedLoop[i].velocity; for(let j=i; j<reversedLoop.length;j++){ if(reversedLoop[j] instanceof NoteOff && reversedLoop[j].pitch == reversedLoop[i].pitch){ reversedLoop[i].velocity = reversedLoop[j].velocity; reversedLoop[j].velocity = onVelocity; break; } } } } } function makePingPongLoop(forwardLoop, forwardTimes){ //putting forward and backward loops together let looplength = forwardLoop.length; if(reversedLoop.length == 0){ makeReversedLoop(forwardLoop, forwardTimes) } pingpongLoop = [...forwardLoop]; pingpongLoop = pingpongLoop.concat(reversedLoop); pingpongTimes = [...forwardTimes]; //correcting the timings for the second half of the loop var timeToAdd = pingpongTimes[pingpongTimes.length-1]; pingpongTimes = pingpongTimes.concat(reversedTimes); for(let i = looplength;i<pingpongTimes.length;i++){ pingpongTimes[i] = pingpongTimes[i] + timeToAdd; } } //getting ready for playback function prepareLoopAndTimes(timingInfos){ if(GetParameter("Play order") == 0){ loop = recordedLoop; times = recordedTimes; } else if(GetParameter("Play order") == 1){ loop = reversedLoop; times = reversedTimes; } else if(GetParameter("Play order") == 2){ loop = pingpongLoop; times = pingpongTimes; } CalculateQuantizedLoopLength(times[times.length-1],timingInfos); NextBeat = Math.ceil(timingInfos.blockStartBeat); PrepareSequencerData(timingInfos); } function ParameterChanged(param, value){ var info = GetTimingInfo(); //One Shot if(param == 2 && value == 1 && recordedLoop.length > 0){ prepareLoopAndTimes(info); OneShot(info,loop,times); } //Rec else if(param == 1 && value == 1){ if(GetParameter("Sequencer") == 1){ SetParameter("Sequencer",0); } if(GetParameter("Loop on grid") == 1){ SetParameter("Loop on grid",0); } if(GetParameter("Loop as played") == 1){ SetParameter("Loop as played",0); } //clearing data recordedLoop = []; recordedTimes = []; sequencerLoop = []; sequencerTimes = []; reversedLoop = []; reversedTimes = []; pingpongLoop = []; pingpongTimes = []; gridLoopLength = 0; start = new Date().getTime(); elapsed = 0; notesCount = 0; recIsStopped = 0; } //Stop else if(param == 1 && value == 0 && recordedLoop.length > 0){ //gathering missing closing infos //possible missing noteoff... elapsed = new Date().getTime() - start; MissingNotesOffCheck(recordedLoop, elapsed); //...closing the loop (for timing + safe pedal info) with sustain off... var closeSustain = new ControlChange(recordedLoop[notesCount-1]); closeSustain.number = 64; closeSustain.value = 0; closeSustain.beatPos = info.blockStartBeat; recordedLoop.push(closeSustain); recordedTimes.push(elapsed); //store original tempo of the loop recordedAtBpm = info.tempo; //prepares data makeReversedLoop(recordedLoop,recordedTimes); makePingPongLoop(recordedLoop,recordedTimes); prepareLoopAndTimes(info); //validate end of rec -> looping can start recIsStopped = 1; SendAllNotesOff(OutChannel); } //Clear Loop else if(param == 10 && value == 1){ recordedLoop = []; recordedTimes = []; reversedLoop = []; reversedTimes = []; pingpongLoop = []; pingpongTimes = []; sequencerLoop = []; sequencerTimes = []; gridLoopLength = 0; SetParameter("Loop as played",0); SetParameter("Loop on grid",0); SetParameter("Rec/Stop",0); SetParameter("Sequencer",0); SetParameter("Add octave below",0); SetParameter("Add octave above",0); SetParameter("Play order",0); SetParameter("Transpose",0); } //(grid)Loop on else if((param == 3 && value == 1 || (param == 4 && value == 1)) && recordedLoop.length > 0){ if(GetParameter("Rec/Stop") == 1){ SetParameter("Rec/Stop",0); } if(GetParameter("Sequencer") == 1){ SetParameter("Sequencer",0); } if(param == 3 && value == 1 && GetParameter("Loop on grid") == 1){ SetParameter("Loop on grid",0); } else if(param == 4 && value == 1 && GetParameter("Loop as played") == 1){ SetParameter("Loop as played",0); CalculateQuantizedLoopLength(times[times.length-1],info); } if(param == 4 && GetParameter("End of cycle quantization") != 0){ //for starting on the beat NextBeat = Math.ceil(info.blockStartBeat); CalculateQuantizedLoopLength(times[times.length-1],info); } else if(param == 4 && GetParameter("End of cycle quantization") == 0){ //for starting on the half beat NextBeat = info.blockStartBeat+((info.blockStartBeat%0.5)+0.5); CalculateQuantizedLoopLength(times[times.length-1],info); } } //(grid)Loop off else if(param == 3 && value == 0 || (param == 4 && value == 0)){ if(OutChannel){ SendAllNotesOff(OutChannel); } } //End of cycle quantize else if(param == 5 && recordedLoop.length > 0){ CalculateQuantizedLoopLength(times[times.length-1],info); } //Sequencer else if(param == 6 && recordedLoop.length > 0){ if(value == 1){ if(GetParameter("Rec/Stop") == 1){ SetParameter("Rec/Stop",0); } if(GetParameter("Loop on grid") == 1){ SetParameter("Loop on grid",0); } if(GetParameter("Loop as played") == 1){ SetParameter("Loop as played",0); } } NextBeat = Math.ceil(info.blockStartBeat); } //Sequencer grid else if(param == 7 && recordedLoop.length > 0){ PrepareSequencerData(info); NextBeat = Math.ceil(info.blockStartBeat); } //Squencer note length else if(param == 8 && recordedLoop.length > 0){ noteLength = GetParameter("Sequencer note length"); noteLengthTiming = (60000/info.tempo)*sequencerGridRatios[noteLength]; } //Play order else if(param == 9 && recordedLoop.length > 0){ prepareLoopAndTimes(info); } //Transpose else if(param == 11){ if(OutChannel){ SendAllNotesOff(OutChannel); } } //Output channel change else if(param == 14 && value != 0){ OutChannel = value; for(let i=0; i<recordedLoop.length;i++){ recordedLoop[i].channel = OutChannel; } if(OutChannel){ SendAllNotesOff(OutChannel); } } } //Calculate grid loop length function CalculateQuantizedLoopLength(loopTiming,timingInfo){ var msPerBeat = (60000/(timingInfo.tempo)); gridLoopLength = (loopTiming/msPerBeat) - ((loopTiming/msPerBeat)%0.5); var endOfCycle = GetParameter("End of cycle quantization"); if(endOfCycle == 0){ return gridLoopLength; } var remainder = (gridLoopLength + 1) % (endOfCycle); if(remainder != 0){ var toAdd = (endOfCycle) - remainder; gridLoopLength = gridLoopLength + toAdd; return gridLoopLength; } else return gridLoopLength; } //Send all notes off of OutChannels function SendAllNotesOff(channel){ var aNo = new ControlChange; aNo.number = 123; aNo.value = 0; aNo.channel = channel; aNo.send(); } //Check for possible missing NotesOff -> push them at the end of loop function MissingNotesOffCheck(notes, endOfLoopTime){ let notesToCheck = []; let missingOffs = []; notesToCheck = [...notes]; for(var i = 0; i < notes.length ; i++){ if(notes[i] instanceof NoteOn){ var found = false; for(var j = 0; j < notesToCheck.length ; j++){ if(notesToCheck[j] instanceof NoteOff && notesToCheck[j].pitch == notes[i].pitch){ notesToCheck.splice(j,1); var found = true; break; } } if(found == false){ missing = new NoteOff(notes[i]); missingOffs.push(missing); } } } //adds the timing of the last recorded event in the loop //to match the added missing offs for(var k = 0; k < missingOffs.length ; k++){ recordedTimes.push(endOfLoopTime); } recordedLoop = recordedLoop.concat(missingOffs); }
  11. I use both. As I said earlier, for that project in particular I’m on Mainstage, but I might use my script with Logic too in the future.
  12. 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.
  13. 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!
  14. 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...
×
×
  • Create New...