Jump to content

Can I trigger patch change from Playback?


joachim_s

Recommended Posts

So if i understand correctly, it syncs to the mainstages internal midi clock?

I have Sets setup as songs, but the midi clock keeps running from song to song obviously.

 

edit: nevermind, just found the play/stop command for the midi clock that can be started at the same time as the playback starts.

Link to comment
Share on other sites

I'm not sure I understand your question. MainStage has simple start and stop. When you stop and start again it will always be back at the beginning. The patch settings can determine the tempo for the patch... I have in the past figured out some ways to program tempo changes in MainStage, but it was IAC loopback fun and it involved using more patches and basically switching patches to change the tempo...something like that. Aside from that, there is no tempo track or anything like that in MainStage. if you need fine grained tempo control you're better off using Logic.

 

Any instrument and midi plugins in MainStage will follow the clock of MainStage, and no way to map out a tempo track as I said, that I have figured out. Maybe there is some way that I didn't figure out though.

 

The other thing is that MainStage can be configured to start playback when you load a patch, or not...and there are other related settings that will determine whether the playback clock will keep running when you change patches, etc.. and I don't remember how all that works right now, but poke around in there..

 

Its not entirely clear to me what you're trying to accomplish, but if you share some details then some of us may offer some possible solutions.

Link to comment
Share on other sites

No, i mean, your sequencer patch always counts patch changes when mainstage is at beat 1.1, right?

As you start playing, mainstages internal clock starts running at whatever tempo, and then, even when you change patches, unless you stop it, it keeps going, right?

 

so unless I start and stop mainstage clock engine (resulting in a short engine dropout, which i think it's not a problem), the next song (mainstage Set) won't be sequenced from the beginning.

 

I just couldn't find a way to reset mainstage's beat clock - i did now, so i think it should work fine.

 

The scripter is on the "set" level of mainstage. So far, it seems to be working, but i need to apply it to all the sets in the concert and test it out to see if it's feasible and reliable for live use.

 

IN any case, thank you for your effort and for remedying this glaring omission. If it works as I hope it will, i will be even more thankful. Do you have a donation setup or something? Least i could do is buy you a beer (if it work hehe)

Link to comment
Share on other sites

huh. I'd really like to avoid additional software if possible. :)

I completely understand this and I’m the same. However, I’m not sure if you own (or have used) Plogue Bidule but when you use it inside MainStage, it doesn’t really feel like you’re using additional software because it runs as a plugin.

Link to comment
Share on other sites

The script uses the clock of mainstage. Umfortunately scripter is also unable to detect midi clock. It just receives current BPM from mainstage and schedules the events based on that. Pretty simple.

 

There is a third party free scripter called protoplug that might be able to detect midi clock but I haven’t used it yet. It’s based on lua. But that would end up being a much more complicated script as you’d have to write your own bpm clock inside the script and ignore the one from mainstage. It would have to look at the processing block start and end time and midi clock derive tempo from that and then schedule midi events using sample time instead of bpm. It would be a much much more complicated script.

Edited by Dewdman42
Link to comment
Share on other sites

The easiest way to create a list of events to playback with this sequencer script, is to use Logic to generate the data list.

 

  1. Go into Logic and create a track with midi events, including any CC ramps you want
     
     
  2. Insert the following DATA GENERATOR script on that track channel, hit the RunScript button
     
    var NeedsTimingInfo = true;
    
    var first = true;
    
    function HandleMIDI(event) {
       event.send();
       
       if(event instanceof PrimingEvent) return;  // WTF?
    
       var ctx = GetTimingInfo();
       if(!ctx.playing) {
           return;
       }
       
       var buffer;
       
       if(first) {
           buffer = " ";
           first = false;
        }
        else {
            buffer = ",";
        }
        
       if(event instanceof NoteOn) {
           buffer += "seq(NoteOn, {beatPos:"+event.beatPos+", channel:"+event.channel
                    +", pitch:"+event.pitch+", velocity:"+event.velocity+"})";
       }
       else if(event instanceof NoteOff) {
           buffer += "seq(NoteOff, {beatPos:"+event.beatPos+", channel:"+event.channel
                    +", pitch:"+event.pitch+", velocity:"+event.velocity+"})";
       }
       else if(event instanceof ControlChange) {
           buffer += "seq(ControlChange, {beatPos:"+event.beatPos+", channel:"+event.channel
                    +", number:"+event.number+", value:"+event.value+"})";
       }
       else if(event instanceof PitchBend) {
           buffer += "seq(PitchBend, {beatPos:"+event.beatPos+", channel:"+event.channel
                    +", value:"+event.value+"})";
       }
       else if(event instanceof ProgramChange) {
           buffer += "seq(PitchBend, {beatPos:"+event.beatPos+", channel:"+event.channel
                    +", number:"+event.number+"})";
       }
    
       CONSOLE.log(buffer);
    }
    
    function Idle() {
       CONSOLE.flush();
    }
    
    function Reset() {
       CONSOLE.log("//==========================================================");
    }
    
    function __Console() {
       this.buffer = [];
    }
    
    __Console.prototype.log = function(msg) {
       this.buffer.push(msg);
    };
    
    __Console.prototype.flush = function() {
       var iter = 20;
       while(this.buffer.length > 0 && iter > 0) {
           Trace(this.buffer.shift());
           iter--;
       }
    };
    
    var CONSOLE = new __Console;
    
    


     

  3. playback the track. You will see a bunch of code logged to the scripter console window. it might look something like this:
     
    //==========================================================
    seq(ControlChange, {beatPos:1, channel:1, number:11, value:16})
    ,seq(NoteOn, {beatPos:1, channel:1, pitch:60, velocity:80})
    ,seq(ControlChange, {beatPos:1.0389166657968114, channel:1, number:11, value:17})
    ,seq(ControlChange, {beatPos:1.077874998259358, channel:1, number:11, value:18})
    ,seq(ControlChange, {beatPos:1.1167916640561695, channel:1, number:11, value:19})
    ,seq(ControlChange, {beatPos:1.1557499965187163, channel:1, number:11, value:20})
    ,seq(ControlChange, {beatPos:1.1946666623155278, channel:1, number:11, value:21})
    ,seq(ControlChange, {beatPos:1.2336249947780744, channel:1, number:11, value:22})
    ,seq(ControlChange, {beatPos:1.2725416605748856, channel:1, number:11, value:23})
    ,seq(ControlChange, {beatPos:1.3114999930374325, channel:1, number:11, value:24})
    ,seq(ControlChange, {beatPos:1.3504166588342439, channel:1, number:11, value:25})
    ,seq(ControlChange, {beatPos:1.3893749912967905, channel:1, number:11, value:26})
    ,seq(ControlChange, {beatPos:1.428291657093602, channel:1, number:11, value:27})
    ,seq(ControlChange, {beatPos:1.4672499895561486, channel:1, number:11, value:28})
    


     

  4. Copy that to the clip board
     
     
  5. Now go to MainStage and open the SEQUENCER script and paste this data list into the section where the midi events are defined for playback at the end of the script. Make sure you're using this version of the sequencer script:
     
    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, overwrite the two entries
    // below with whatever you want to playback
    //==============================================
    var eventList = [
    seq(ControlChange, {beatPos:1, channel:1, number:11, value:16})
    ,seq(NoteOn, {beatPos:1, channel:1, pitch:60, velocity:80})
    ];
    

 

Its now ready for playback in MainStage

Link to comment
Share on other sites

  • 3 weeks later...

The index variable is something I did to reduce the amount of scanning through the array in every call to processmidi. It’s keeping track of the current position of the array globally.

 

The Reset callback function is called by LPX when you press PLAY.

 

It could probably be done a better way where I just set it during the first call to process midi. I haven’t looked at this in a while. Are you having problems?

Link to comment
Share on other sites

It will work for you. when I first wrote that script I wanted to see how far I could go with it so I ended up downloading a std midi file of Toto's Rosanna off the net somewhere and setup a MainStage project to play it back using the above script essentially. Actually I ended up adding to the script with GUI stuff in order to filter out which midi channel from the midi file that I want playing on each track where the script is or something.. But its essentially the same basic script for playing a sequence. You can put scripts on different channels in MainStage and they will all play perfectly in sync.

 

check it out here

 

Link to comment
Share on other sites

ha! neat :)

 

i found that making ramps is the best with step editor in logic - it creates the least amount of redundant date.

also, if you use Varitempo (Speed and pitch) at 100%, it will read out your midi data 2x as fast and will still be right position.

Link to comment
Share on other sites

ps - it could be possible to build some "ramp" feature into the script...have ability to specify a one liner ramp event that basically would turn into a series of CC events at some pre-determined interval of time. And I think that would be very efficient and smooth in MainStage to play back...but..I can't think of a good way to generate that out of LogicPro from the ramps there, because Scripter can't see the automation ramps, it can only see the actual midi events coming to it... so.. you'd have to add them to the MainStage script manually and I think its just easier to get the sequence playing exactly how you want it in LogicPro and then just export it to the script like that... The only thing about the step editor is that whatever grid its on may or may not be smooth enough for you depending on what you're trying to do.

 

I could also have a look at the generation script you use in Logic Pro and have it thin out the CC events to less often there automatically. But it would probably be quite a bit more scripter code to do that...

Link to comment
Share on other sites

nah, i've edited everything with list in the end - so i thinned it out as much as practically feasible. smooth enough for live purposes, and it's absolutely under control so i like it better this way instead of tons of midi data going through, more manageable.
Link to comment
Share on other sites

here's a slightly improved version of the sequencer script. for one thing the sequence data is at the top of the script. For another thing, it will be automatically sorted by beatPos value before playing anything, just in case. Without this step, you have to make sure that your list is always in correct BeatPos order, which is usually a good idea anyway, but this version just makes absolutely sure its in the right order before before starting to play anything. Also added ability to specify Note pitches by either name (i.e. 'C#4'), or by pitch number as before.

 

//==============================================
// sequence data
//==============================================

var eventList = [

seq(NoteOn, {beatPos:1, channel:1, pitch:'C3', velocity:80})
,seq(ControlChange, {beatPos:1.0249999947845936, channel:1, number:11, value:62})
,seq(ControlChange, {beatPos:1.1659166620733838, channel:1, number:11, value:66})
,seq(ControlChange, {beatPos:1.3068333200489481, channel:1, number:11, value:70})
,seq(ControlChange, {beatPos:1.4477499939190845, channel:1, number:11, value:74})
,seq(ControlChange, {beatPos:1.5886666551232338, channel:1, number:11, value:78})
,seq(ControlChange, {beatPos:1.7295833257647852, channel:1, number:11, value:82})
,seq(ControlChange, {beatPos:1.87049999323984, channel:1, number:11, value:86})
,seq(ControlChange, {beatPos:1.9762083204152683, channel:1, number:11, value:89})
,seq(NoteOff, {beatPos:1.9999999890724818, channel:1, pitch:'C3', velocity:64})
,seq(NoteOn, {beatPos:1.9999999890724818, channel:1, pitch:'D3', velocity:80})
,seq(ControlChange, {beatPos:2.1124999916180967, channel:1, number:11, value:93})
,seq(ControlChange, {beatPos:2.226874994471048, channel:1, number:11, value:92})
,seq(ControlChange, {beatPos:2.341291657059143, channel:1, number:11, value:91})
,seq(ControlChange, {beatPos:2.4556666649878025, channel:1, number:11, value:90})
,seq(ControlChange, {beatPos:2.5700833236798646, channel:1, number:11, value:89})
,seq(ControlChange, {beatPos:2.6844583233352752, channel:1, number:11, value:88})
,seq(ControlChange, {beatPos:2.798874992861723, channel:1, number:11, value:87})
,seq(ControlChange, {beatPos:2.9132499957146747, channel:1, number:11, value:86})
,seq(NoteOff, {beatPos:2.9999999940395354, channel:1, pitch:'D3', velocity:64})
,seq(NoteOn, {beatPos:2.9999999940395354, channel:1, pitch:'E3', velocity:80})
,seq(ControlChange, {beatPos:3.027666656921307, channel:1, number:11, value:85})
,seq(ControlChange, {beatPos:3.142083331507941, channel:1, number:11, value:84})
,seq(ControlChange, {beatPos:3.2564583235420286, channel:1, number:11, value:83})
,seq(ControlChange, {beatPos:3.3708749912523976, channel:1, number:11, value:82})
,seq(ControlChange, {beatPos:3.485249994105349, channel:1, number:11, value:81})
,seq(ControlChange, {beatPos:3.631833324705561, channel:1, number:11, value:84})
,seq(ControlChange, {beatPos:3.774333326766888, channel:1, number:11, value:88})
,seq(ControlChange, {beatPos:3.9168333288282158, channel:1, number:11, value:92})
,seq(ControlChange, {beatPos:3.9880833298588794, channel:1, number:11, value:94})
,seq(NoteOff, {beatPos:3.9999999940395354, channel:1, pitch:'E3', velocity:64})
,seq(NoteOn, {beatPos:3.9999999940395354, channel:1, pitch:'F3', velocity:80})
,seq(ControlChange, {beatPos:4.130583321955055, channel:1, number:11, value:98})
,seq(ControlChange, {beatPos:4.2730833294180535, channel:1, number:11, value:102})
,seq(ControlChange, {beatPos:4.41558332098648, channel:1, number:11, value:106})
,seq(ControlChange, {beatPos:4.558083328449477, channel:1, number:11, value:110})
,seq(ControlChange, {beatPos:4.664958330073084, channel:1, number:11, value:113})
,seq(NoteOff, {beatPos:4.999999992052714, channel:1, pitch:'F3', velocity:64})

];

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

var NeedsTimingInfo = true;
var lastIdx = 0;

// Sort the Event list by BeatPosition just in case
eventList.sort(function(a, b){return a.beatPos - b.beatPos});


//==========================================
// 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:
       if(typeof(json.pitch) == 'number') {
           out.pitch = json.pitch;
       }
       else if( typeof(json.pitch) == 'string') {
           out.pitch = MIDI.noteNumber(json.pitch);
       }
       else {
          // todo report error
       }
       
       out.velocity = json.velocity;
       break;
       
   case NoteOff:
       if(typeof(json.pitch) == 'number') {
           out.pitch = json.pitch;
       }
       else if( typeof(json.pitch) == 'string') {
           out.pitch = MIDI.noteNumber(json.pitch);
       }
       else {
          // todo report error
       }
       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;
}
Edited by Dewdman42
Link to comment
Share on other sites

alright, guess I needed a challenge today. This is not the best code I've ever written....but......

 

Here is an updated version of the Sequence Generator script, use this inside logic to generate the sequence data. In this version there are a couple of variables at the top of the script which you can use to specify that CC's should be thinned out, and if so, how often you want them to happen, expressed as musical division. So change these variables to use CC thinning:

 

var CC_THINNING = true;
var CC_DIVISION = 32;     // 1/32  seperation between CC events, non-quantized

 

If you leave the CC_THINNING variable set to false, then no thinning will occur.

 

Also added a variable to have the sequence note data created with pitch names rather then pitch numbers, and the sequencer script in my previous post will handle those. A little easier to read and edit the sequence data that way.

 

Here's the whole script, default is CC_THINNING turned off, like before.

 

/******************************************************************
* Script to generate midi sequence to be used by a different
* script for playback of whatever it captures.
******************************************************************/

/****************************************************
* if you want to thin CC data, set CC_THINNING=true
* and set the beat time division between CC events
****************************************************/

var CC_THINNING = false;
var CC_DIVISION = 32;     // 1/128th seperation between CC events
var notesByName = true;



/******************************************************************
********* DO NOT EDIT BELOW HERE *********************************
 ******************************************************************/

var CC_INTERVAL = 1 / (CC_DIVISION/4);

var first = true;
var NeedsTimingInfo = true;

var lastCC = new Array(17);
for(var chan=1;chan<=16;chan++) {
   lastCC[chan] = new Array(128);    
   for(var ccnum=0;ccnum<128;ccnum++) {
       lastCC[chan][ccnum] = {lastSentBeat:0,value:0,lastRecvBeat:0,pending:false};
   }
}


function HandleMIDI(event) {

   event.send();
   
   if(event instanceof PrimingEvent) return;  // WTF?

   var ctx = GetTimingInfo();
   if(!ctx.playing) {
       return;
   }
    
   /**
    * first check to see for any non-CC, chase last known CC values on this channel
    **/
   
   if(CC_THINNING && !(event instanceof ControlChange)) {
     
       for(var num=0;num<128;num++) {
       
           if(lastCC[event.channel][num].pending == true) {
                   
               CONSOLE.log(showComma()
                   +"seq(ControlChange, {beatPos:"+lastCC[event.channel][num].lastRecvBeat
                   +", channel:"+event.channel
                   +", number:"+num+", value:"+lastCC[event.channel][num].value+"})");
               
               lastCC[event.channel][num].pending = false;
               lastCC[event.channel][num].lastSentBeat = lastCC[event.channel][num].lastRecvBeat;
           }
       }
   }
   
   if(event instanceof NoteOn) {
       CONSOLE.log(showComma()
                +"seq(NoteOn, {beatPos:"+event.beatPos+", channel:"+event.channel
                +", pitch:"+showPitch(event)+", velocity:"+event.velocity+"})");
   }
   else if(event instanceof NoteOff) {
       CONSOLE.log(showComma()
                +"seq(NoteOff, {beatPos:"+event.beatPos+", channel:"+event.channel
                +", pitch:"+showPitch(event)+", velocity:"+event.velocity+"})");
   }
   else if(event instanceof ControlChange) {
     
       if(!CC_THINNING) {
           CONSOLE.log(showComma()
                   +"seq(ControlChange, {beatPos:"+event.beatPos
                   +", channel:"+event.channel
                   +", number:"+event.number+", value:"+event.value+"})");            
       }
       
       else if( lastCC[event.channel][event.number].lastSentBeat == 0 
               ||  ( lastCC[event.channel][event.number].lastSentBeat 
                      < (event.beatPos - CC_INTERVAL) 
               && lastCC[event.channel][event.number].value != event.value)) {
               
           CONSOLE.log(showComma()
                   +"seq(ControlChange, {beatPos:"+event.beatPos
                   +", channel:"+event.channel
                   +", number:"+event.number+", value:"+event.value+"})");            
                
           lastCC[event.channel][event.number].lastSentBeat = event.beatPos;  
           lastCC[event.channel][event.number].value = event.value;
           lastCC[event.channel][event.number].lastRecvBeat = event.beatPos;
           lastCC[event.channel][event.number].pending = false;       
       }

       else if( lastCC[event.channel][event.number].value != event.value) {
//            CONSOLE.log("DEBUG: received CC "+event.beatPos+":"+event.number+":"+event.value);
           lastCC[event.channel][event.number].value = event.value;
           lastCC[event.channel][event.number].lastRecvBeat = event.beatPos;
           lastCC[event.channel][event.number].pending = true;
       }
   }
   else if(event instanceof PitchBend) {
       CONSOLE.log(showComma()
                +"seq(PitchBend, {beatPos:"+event.beatPos+", channel:"+event.channel
                +", value:"+event.value+"})");
   }
   else if(event instanceof ProgramChange) {
        CONSOLE.log(showComma()
                +"seq(PitchBend, {beatPos:"+event.beatPos+", channel:"+event.channel
                +", number:"+event.number+"})");
   }
}

//=============================================
// in order to check for cc thinning gap
function ProcessMIDI() {

   if(!CC_THINNING) {
       return;
   }
   
   var ctx = GetTimingInfo();
   if(!ctx.playing) {
       return;
   }
   
   // Look through all 16 channels for any pending CC's that enough
   // time has elapsed that we need it to flush out
   
   for(var chan = 1;chan<=16;chan++) {
   
       for(var num=0;num<128;num++) {
       
           if(lastCC[chan][num].pending == true 
                   && lastCC[chan][num].lastSentBeat < (ctx.blockStartBeat - CC_INTERVAL)) {
                   
               CONSOLE.log(showComma()
                   +"seq(ControlChange, {beatPos:"+lastCC[chan][num].lastRecvBeat
                   +", channel:"+chan
                   +", number:"+num+", value:"+lastCC[chan][num].value+"})");
               
               lastCC[chan][num].pending = false;
               lastCC[chan][num].lastSentBeat = lastCC[chan][num].lastRecvBeat;
           }
       }
   }
}

function showPitch(event) {
   if(notesByName) {
       return "'"+MIDI.noteName(event.pitch)+"'";
   }
   else {
        return event.pitch;
   }
}

function showComma() {
   if(first) {
       first = false;
       return " ";
   }
   else {
       return ",";
   }
}


function Idle() {
   CONSOLE.flush();
}

function Reset() {
   CONSOLE.log("//==========================================================");
}

function __Console() {
   this.buffer = [];
}

__Console.prototype.log = function(msg) {
   this.buffer.push(msg);
};

__Console.prototype.flush = function() {
   var iter = 20;
   while(this.buffer.length > 0 && iter > 0) {
       Trace(this.buffer.shift());
       iter--;
   }
};

var CONSOLE = new __Console;

 

All that being said, I also like your approach of using the step editor, which is more explicit, you are putting CC messages exactly where you want them that way. With the above you can just use normal automation curves...and avoid overly verbose CC messages in the final sequence in MainStage.

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