Jump to content

Can I trigger patch change from Playback?


joachim_s

Recommended Posts

Well actually no you’d need to modify the script some more to handle other midi event types. Not sure what you meant by "other var entries". I had to modify the script guts a bit to handle the bank messages.

 

But in theory something very similar can be done. I have been able to sequence entire songs using scripter: viewtopic.php?f=45&t=132562&p=676821&hilit=mainstage#p676821

Link to comment
Share on other sites

Here's a new version of the script that includes ability to send MSB/LSB bank messages:

 

var NeedsTimingInfo = true;

//========= EDIT THIS ARRAY ====================

var programChanges = [
   {beatPos:1.99, channel:1, msb:0, lsb:3, number:2}
  ,{beatPos:4.99, channel:1, number:3}
  // copy the above line for more Program changes
];


//========= DO NOT EDIT BELOW HERE =============

var idx = 0;
var pc = new ProgramChange;  //reuse in a loop
var cc = new ControlChange;

function ProcessMIDI() {
  /* Get current timing info */
  var info = GetTimingInfo();
 
  /* only do work if playing */
  if ( !info.playing ) {
      idx = 0;
      return;
  }
  
  if (idx < programChanges.length) {
  
     while ( idx < programChanges.length &&
             info.blockStartBeat <= programChanges[idx].beatPos && 
             info.blockEndBeat >= programChanges[idx].beatPos ) {
        
        if(programChanges[idx].msb != undefined) {         
            cc.channel = programChanges[idx].channel;
            cc.beatPos = programChanges[idx].beatPos;
            cc.number = 0;
            cc.value = programChanges[idx].msb;
            cc.send();
            //cc.trace();
        }
        
        if(programChanges[idx].lsb != undefined) {                    
            cc.channel = programChanges[idx].channel;
            cc.beatPos = programChanges[idx].beatPos;
            cc.number = 32;
            cc.value = programChanges[idx].lsb;
            cc.send();
            //cc.trace();
        }

        if(programChanges[idx].number != undefined) {
            pc.number = programChanges[idx].number;    
            pc.beatPos = programChanges[idx].beatPos;
            pc.channel = programChanges[idx].channel;  
            pc.send();
            //pc.trace();
        }
        
        idx++;
     }              
  }
  
}

// ignore incoming midi, might not need this, not sure right now
function HandleMIDI(event) {
}

 

thx a lot!!!

Link to comment
Share on other sites

Scripter-based sequencer

 

This will explain how to use the free Scripter script to perform mainstage sequencing of program changes. Note, in this case, the previously saved midi file is not used, but the Logic Pro project is still useful.

 

  1. Create an external instrument channel as in the first example steps 1-3 (note the midi blocking script is not needed here)
     
     
  2. Create a channel for the playback plugin as described above for the audio.
     
     
  3. Place an instance of scripter into the midifx slot. Use the following script:
    extinstscript.jpg
    var NeedsTimingInfo = true;
    
    //========= EDIT THIS ARRAY ====================
    
    var programChanges = [
       {beatPos:4.99, channel:1,  number:2}
      ,{beatPos:8.99, channel:1,  number:3}
      // copy the above line for more Program changes
    ];
    
    
    //========= DO NOT EDIT BELOW HERE =============
    
    var idx = 0;
    var pc = new ProgramChange;  //reuse in a loop
    
    function ProcessMIDI() {
      /* Get current timing info */
      var info = GetTimingInfo();
     
      /* only do work if playing */
      if ( !info.playing ) {
          idx = 0;
          return;
      }
      
      if (idx < programChanges.length) {
      
         while ( idx < programChanges.length &&
                 info.blockStartBeat <= programChanges[idx].beatPos && 
                 info.blockEndBeat >= programChanges[idx].beatPos ) {
                      
            pc.number = programChanges[idx].number;    
            pc.beatPos = programChanges[idx].beatPos;
            pc.channel = programChanges[idx].channel;  
            pc.send();
            // pc.trace();
            idx++;
         }              
      }
      
    }
    
    // ignore incoming midi, might not need this, not sure right now
    function HandleMIDI(event) {
    }
    



     

  4. The script now needs to be edited. The following section of the script is where you can edit it, change these lines to have the program changes you want at the beat positions you want:
    //========= EDIT THIS ARRAY ====================
    var programChanges = [
       {beatPos:4.99, channel:1,  number:2}
      ,{beatPos:8.99, channel:1,  number:3}
      // copy the above line for more Program changes
    ];
    //========= DO NOT EDIT BELOW HERE =============
    


     
    In order to edit this section, you will need to edit the beatPos, channel and number values for each program change you want to send. You can add more program changes by copying the 2nd line as many times as you want. The beatPos represents the time in total beats from the start, without regard to bars; when the program change should be sent. Channel and Number are self explanatory.
     
    The beatPos is a fractional value in order to place in between or just before beats for example. In this example I have two program changes happening just before beats 5 and 9 of the sequence. You can use trial and error to figure out the beatPos values to use.
     
    Or....
     
    you can use a special logging script I made which you can insert into the midi track of the LogicPro project you used earlier. Then hit play and watch the script src window to see the exact beatPos values logged for you that line up with the program changes and markers you made earlier in LogicPro.
     
    Here is that script and some example output

    var NeedsTimingInfo = true;
    
    function HandleMIDI(event) {
       event.send();
       if (event instanceof ProgramChange) {
           Trace(event.beatPos + " : Program Change " + event.number);
       }
    }
    


    4.624444431749483 : Program Change 4
    6.499637178517878 : Program Change 5
    15.666303845246633 : Program Change 6
    

 

Set that up and playback of the program changes should happen exactly in sync with the audio as in the other examples.

 

The special logging script for the Beatpos does not work for me

Link to comment
Share on other sites

  • 3 weeks later...
Well actually no you’d need to modify the script some more to handle other midi event types. Not sure what you meant by "other var entries". I had to modify the script guts a bit to handle the bank messages.

 

But in theory something very similar can be done. I have been able to sequence entire songs using scripter: viewtopic.php?f=45&t=132562&p=676821&hilit=mainstage#p676821

 

I meant other variable definitions in the header of the script. So, for example, CC's or notes.

Link to comment
Share on other sites

You’d have to modify more of the guts of the script to handle other event types. Wouldn’t be that hard. Writing a little sequencer is actually not that hard. I once posted a video on here somewhere of a rendition of toto’s song, roseanna; all played by a fairly simple scripter script and one big ass array full of event data. It all works the same way.

 

What I put here was just something super simple to handle program changes. But like I said if you go into the code where the events are actually created and sent you could put a bit of if then logic for different events types and then it would work as you’re hoping.

Link to comment
Share on other sites

Here's a bit more of an example that can send other types of midi events. This is just one way to approach it... There are different ways it could be done, but maybe gives you some ideas. Let me know if you have any questions about it. The factory methods might throw some people for a loop

 

var NeedsTimingInfo = true;

var lastIdx = 0;

//=================================
// ProcessMIDI callback
//=================================
function ProcessMIDI() {

   var info = GetTimingInfo();
   if (!info.playing) return;

   while (lastIdx < eventList.length &&
           eventList[lastIdx].beatPos <= info.blockEndBeat) {
       
       sendEvent(eventList[lastIdx]);
       lastIdx++;
   }
}

//==============================
// send event
//==============================
function sendEvent(event) {
   if (event instanceof ProgramChange) {
       handleBankSelect(event);
   }
   event.send();
}
           
var cc = new ControlChange;
//==============================
// handle bank select
//==============================
function handleBankSelect(event) {

   if (event.msb != undefined && event.lsb != undefined) {

       cc.channel = event.channel;
       cc.beatPos = event.beatPos;
       cc.number = 0;
       cc.value = event.msb;
       cc.send();

       cc.number = 32;
       cc.value = event.lsb;
       cc.send();
   }
}


//==============================
// Factory Methods
//==============================
function _NoteOn(json) {
   var out = new NoteOn;
   out.beatPos = json.beatPos;
   out.channel = json.channel;
   out.pitch = json.pitch;
   out.velocity = json.velocity;
   return out;
}

function _NoteOff(json) {
   var out = new NoteOff;
   out.beatPos = json.beatPos;
   out.channel = json.channel;
   out.pitch = json.pitch;
   out.velocity = json.velocity;
   return out;
}

function _ControlChange(json) {
   var out = new ControlChange;
   out.beatPos = json.beatPos;
   out.channel = json.channel;
   out.number = json.number;
   out.value = json.value;
   return out;
}

function _PitchBend(json) {
   var out = new PitchBend;
   out.beatPos = json.beatPos;
   out.channel = json.channel;
   out.value = json.value;
   return out;
}

function _ProgramChange(json) {
   out = new ProgramChange;
   out.beatPos = json.beatPos;
   out.channel = json.channel;
   out.number = json.number;
   out.msb = json.msb;
   out.lsb = json.lsb
   return out;
}

//==============================
// Reset index on play
//==============================
function Reset() {
   lastIdx = 0;
}

//==============================
// ignore incoming midi
//==============================
function HandleMIDI(event) {}

//==============================================
// sequence data - must be in order for now
//==============================================
var eventList = [
_NoteOn(         {beatPos:4,  channel:1,pitch:50,velocity:100})
,_NoteOff(        {beatPos:7,  channel:1,pitch:50,velocity:100})
,_ControlChange(  {beatPos:6,  channel:1,number:20,value:100})
,_PitchBend(      {beatPos:6.5,channel:1,value:456})
,_ProgramChange(  {beatPos:7,  channel:1,number:35,msb:34,lsb:0})
];
Edited by Dewdman42
Link to comment
Share on other sites

So here's another version of a simple sequencer...similar as above, but slightly different approach, perhaps easier to read. I'd be curious which one of the two some of you would prefer.

 

var NeedsTimingInfo = true;

var lastIdx = 0;

//==========================================
// ProcessMIDI callback
//==========================================
function ProcessMIDI() {

   var info = GetTimingInfo();    
   if(!info.playing) return;
   
   while(lastIdx < eventList.length &&
           eventList[lastIdx].beatPos <= info.blockEndBeat) {
           
       sendEvent(eventList[lastIdx]);
       lastIdx++;
   }    
}

//==========================================
// send event including pc bank select
//==========================================
function sendEvent(event) {
   if(event instanceof ProgramChange) {
       handleBankSelect(event);
   }
   event.send();
}

var cc = new ControlChange;  // resuable cc object
//==========================================
// Check for Bank Select and send if needed
//==========================================
function handleBankSelect(event) {

   if(event.msb != undefined && event.lsb != undefined) {
                               
       cc.channel = event.channel;
       cc.beatPos = event.beatPos;
       cc.number = 0;
       cc.value = event.msb;
       cc.send();
          
       cc.number = 32;
       cc.value = event.lsb;
       cc.send();
   }
}

//==========================================
// reset sequence index on play
//==========================================
function Reset() {
   lastIdx = 0;
}

//==========================================
// block incoming midi
//==========================================
function HandleMIDI(event) {
}

//==========================================
// Factory Method
//==========================================
function seq(type,json) {

   var out = new type;
   out.beatPos = json.beatPos;
   out.channel = json.channel;
   
   switch(type) {
   case NoteOn:
       out.pitch = json.pitch;
       out.velocity = json.velocity;
       break;
       
   case NoteOff:
       out.pitch = json.pitch;
       out.velocity = json.velocity;
       break;
       
   case ControlChange:
       out.number = json.number;
       out.value = json.value;
       break;
       
   case PitchBend:
       out.value = json.value;
       break;
       
   case ProgramChange:
       out.number = json.number;
       out.msb = json.msb;
       out.lsb = json.lsb
       break;
   }
   return out;
}

//==============================================
// sequence data - must be in order for now
//==============================================
var eventList = [
seq(NoteOn,       {beatPos:4,  channel:1,pitch:50,velocity:100})
,seq(NoteOff,      {beatPos:7,  channel:1,pitch:50,velocity:100})
,seq(ControlChange,{beatPos:6,  channel:1,number:20,value:100})
,seq(PitchBend,    {beatPos:6.5,channel:1,value:456})
,seq(ProgramChange,{beatPos:7,  channel:1,number:35,msb:34,lsb:0})
];
Edited by Dewdman42
Link to comment
Share on other sites

So here's another version of a simple sequencer...similar as above, but slightly different approach, perhaps easier to read. I'd be curious which one of the two some of you would prefer.

 

var NeedsTimingInfo = true;

var lastIdx = 0;

//==========================================
// ProcessMIDI callback
//==========================================
function ProcessMIDI() {

   var info = GetTimingInfo();    
   if(!info.playing) return;
   
   while(lastIdx < eventList.length &&
           eventList[lastIdx].beatPos <= info.blockEndBeat) {
           
       sendEvent(eventList[lastIdx]); // TODO - handle when starting mid song
       lastIdx++;
   }    
}

//==========================================
// send event including pc bank select
//==========================================
function sendEvent(event) {
   if(event instanceof ProgramChange) {
       handleBankSelect(event);
   }
   event.send();
}

var cc = new ControlChange;  // resuable cc object
//==========================================
// Check for Bank Select and send if needed
//==========================================
function handleBankSelect(event) {

   if(event.msb != undefined && event.lsb != undefined) {
                               
       cc.channel = event.channel;
       cc.beatPos = event.beatPos;
       cc.number = 0;
       cc.value = event.msb;
       cc.send();
          
       cc.number = 32;
       cc.value = event.lsb;
       cc.send();
   }
}

//==========================================
// reset sequence index on play
//==========================================
function Reset() {
   lastIdx = 0;
}

//==========================================
// block incoming midi
//==========================================
function HandleMIDI(event) {
}

//==========================================
// Factory Method
//==========================================
function seq(type,json) {

   var out = new type;
   out.beatPos = json.beatPos;
   out.channel = json.channel;
   
   switch(type) {
   case NoteOn:
       out.pitch = json.pitch;
       out.velocity = json.velocity;
       break;
       
   case NoteOff:
       out.pitch = json.pitch;
       out.velocity = json.velocity;
       break;
       
   case ControlChange:
       out.number = json.number;
       out.value = json.value;
       break;
       
   case PitchBend:
       out.value = json.value;
       break;
       
   case ProgramChange:
       out.number = json.number;
       out.msb = json.msb;
       out.lsb = json.lsb
       break;
   }
   return out;
}

//==============================================
// sequence data - must be in order for now
//==============================================
var eventList = [
seq(NoteOn,       {beatPos:4,  channel:1,pitch:50,velocity:100})
,seq(NoteOff,      {beatPos:7,  channel:1,pitch:50,velocity:100})
,seq(ControlChange,{beatPos:6,  channel:1,number:20,value:100})
,seq(PitchBend,    {beatPos:6.5,channel:1,value:456})
,seq(ProgramChange,{beatPos:7,  channel:1,number:35,msb:34,lsb:0})
];

 

Nice!

 

Yeah, so the Factory method is essentially defining all of the variable cases with the integer values that each case needs?

 

Again, this is where my coding inexperience really hurts me. I'm following everything in the script, but coming up with that and putting it in the correct order into the correct syntax is where I'm really weak.

 

I tried a few different versions of this, but I just never quite got it into the right syntax for it to work as expected.

 

Again, so cool. Thank you.

Link to comment
Share on other sites

The array at the very end of the script is where you define the midi events you want played back. You shouldn’t have to change anything else.

 

Note that this script cannot work correctly if you start the playback partway into the song. It needs to be more complicated script to handle that, I was just trying to keep it simple as possible for the sake of learning.

 

I was afraid the factory method approach might confuse many people but it’s actually a clean and simple way to do it if you understand the concept.

 

Here’s a little computer sci lesson about “factory methods”, they are basically functions that spit out newly created objects:

 

If, for example, you would normally say the following to create an object in javascript:

var myEvent = new NoteOn;

 

With a factory method/function you instead call a function which you define, instead of calling “new”. That function calls new inside it and does other extra things to prep the object and then returns that object. For example here is a factory method/function:

 

function _NoteOn(json) {
   var out = new NoteOn;
   out.beatPos = json.beatPos;
   out.channel = json.channel;
   out.pitch = json.pitch;
   out.velocity = json.velocity;
   return out;
}

 

And here is an example of calling that factory method/function instead of calling new:

var myEvent = _NoteOn({beatpos:4,channel:1,pitch:50,velocity:100});

 

The data between {} curly braces above is a JSON structure, you can google about JSON, but that is a fundamental part of javascript. Anyway, the data is provided to the function as JSON in this case.

 

You call that factory function and pass in some values to use, it creates a new event object and returns it. This is just like calling “new” directly except that Apple did not provide a way by default to set the beatpos while calling new. This lets us do that.

 

The array at the end of the script has as each element a call to one of those factory functions, passing in the json data. When the array is actually initializing itself at the end of the script there, it will be calling that factory function and computing the actual value to put into the each array element. Each array element will end up having an event object of some kind...depending one which factory function was called for each one.

 

var eventList = [
_NoteOn(         {beatPos:4,  channel:1,pitch:50,velocity:100})
,_NoteOff(        {beatPos:7,  channel:1,pitch:50,velocity:100})
,_ControlChange(  {beatPos:6,  channel:1,number:20,value:100})
,_PitchBend(      {beatPos:6.5,channel:1,value:456})
,_ProgramChange(  {beatPos:7,  channel:1,number:35,msb:34,lsb:0})
];

In this way the actual script code during playback can be very simple, it just accesses each array element which happens to be an event object that can just be sent without any fuss at play time. You only have to concern yourself with the beatpos timing for when to send each one.

 

The array at the end is where you define each array element as a factory function call with the midi event you want.

 

The second sequencer variation I gave earlier does all of this with only a single factory function called seq() that accepts a type parameter to indicate which type of event object to create. I like the readability of this approach a little better but they are very similar.

 

var eventList = [
seq(NoteOn,       {beatPos:4,  channel:1,pitch:50,velocity:100})
,seq(NoteOff,      {beatPos:7,  channel:1,pitch:50,velocity:100})
,seq(ControlChange,{beatPos:6,  channel:1,number:20,value:100})
,seq(PitchBend,    {beatPos:6.5,channel:1,value:456})
,seq(ProgramChange,{beatPos:7,  channel:1,number:35,msb:34,lsb:0})
];

 

This could be done other ways too, maybe some will be inspired to use a different approach.

Edited by Dewdman42
Link to comment
Share on other sites

Sorry that I ask this question of sort of a different subject here. I’m a bit stressed up because my band’s having our last rehearsal for a show and I can’t seem to fix this issue:

 

I have successfully made MainStage slave to Logic’s midi clock through tempo changes in a project, using IAC, but the changes don’t happen in sync. Every time I push play in Logic MainStage starts the Playback sound file slightly differently in time. How do I fix this?

Link to comment
Share on other sites

It seems like I tried to sync logic and mainstage once and decided it was futile. Why do you need them sync’d? Maybe you can change your stage setup so that logic handles all sequencing and mainstage only handles live instruments?

 

It’s a worthy discussion though you should create a separate discussion thread for this. If it’s even possible it’s probably not easy to set up

Link to comment
Share on other sites

It seems like I tried to sync logic and mainstage once and decided it was futile. Why do you need them sync’d? Maybe you can change your stage setup so that logic handles all sequencing and mainstage only handles live instruments?

 

Maybe so. However why won’t it work?

The answer to why I need them synced is because of tempo changes. On the first beat of bar nine I need to make a tempo change. There is also a ritardando later on in the song. I’m using several plugins that are midi clock dependant, the Mainstage/Logic MIDI Arpeggio for instance.

Link to comment
Share on other sites

  • 2 months later...

Hi!

I was reading this thread and I must say I didn‘t understand eeeeverything :P

 

Right now, I‘m using Logic as a live sequencer for the backing tracks of my band, which worked perfect for the last years.

2 different click tracks, one for the two guitars and one for drums/bass to come into the in ear setup and the programmed synths to foh.

Additionally we used logic also for changing the guitar‘s axe fx/ax8 presets, which is really nice for us and which we don‘t want to lose..

 

So in some of our new songs I would love to play also some synth parts live and here I was thinking about changing to mainstage 3 and now finally here comes my question;

 

Did I get it right that with one of the 3 described methods I can get mainstage to change the presets on the guitar rigs?

Link to comment
Share on other sites

Btw, thanks for the awesome tutorials, as i‘m not an engineer it‘s exactly what i need :roll:

 

And as far as I got it, it would be also possible to sync logic to mainstage, press the „play“ button in mainstage; logic starts our songs and I can use mainstage live?

 

Thanks for the quick reply

Link to comment
Share on other sites

  • 1 month later...

Hi there...

I just joined the forum because of this topic and first of all I want to thank you very much for sharing this wonderful script, which for me as a keyboarder is a major step forward to a relaxed set, where all you have to focus on, is PLAY not step through patches and recall the right sound at the right moment. Which can be very difficult indeed playing with both hands, one foot on the sustain, and the other one on the expression pedal...

 

I also want to make a remark concerning the source of timing information beeing used to recall the right patch.

The problem I have, is that the recall always depends on the song played from the very start. So in rehearsels when we - lets say - play a part in cycle, I still have to change manually. What if the script uses the PLAYBACK/STATUS/POSITION info instead of the beatcount? With this as a Base for the program change, it would always be in perfect timing, no matter if you skip a marker, repeat a marker 3 or 5 times.... always perfect timed patch recalls!

 

Cheers & thanks again!

 

Stef

Link to comment
Share on other sites

Hi there,

 

I just joined this forum because of this topic. And first of all I want to throw out a big thank you to Dewdman42 for sharing this wonderful script! This is a major step forward for me as a keyboarder to focus on the performance instead of changing sounds and having the right sound at the right time, which can be kind of difficult when playing with two hands, one foot on the sustain and one on the expression.

 

Concerning the beatcount as the source for the program changes I have a remark and question. Is it possible to change the timing reference of the script from the beatcount to the PLAYBACK/STATUS/POSITION. This way the program changes would be sent at the right moment every time, even when playing a part in cycle mode for rehearsels. Mainstage delivers the position as a absolute timing reference, regardless if you jump from marker 1 to 7 or repeat a certain part 45 times.

 

I´d be very interested in what you think about this approach!

 

Cheers

Stef

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...