Synthetic Legato



  • A synthetic legato script that includes automatic chord detection, has the ability to play formant correct glides, and includes an auto-trill feature. The code is well commented and I've added tooltips for most of the controls.

    /**
     * Title: Synthetic Legato v3.5
     * Author: David Healey
     * Date: 27/01/2017
     * Modified: 11/04/2017
     * License: GPLv3 - https://www.gnu.org/licenses/gpl-3.0.en.html
    */
    
    reg lastNote = -1;
    reg lastEventId = -1;
    reg retriggerNote = -1;		
    reg lastVelo = 0;
    reg lastTime;
    reg interval;
    reg fadeTime;
    
    reg bendAmount;
    reg bendLookup = []; //Bend amount lookup table - generated on init
    
    reg glideBend; //The bend amount for glides, either 100 or -100 depending on if an up or down glide
    reg glideNote; //Currently sounding note during a glide/trill
    reg rate; //Timer rate for glide/trill
    reg notes = []; //Origin and target notes for glide/trill
    reg count; //Counter for switching between origin and target notes for trill
    
    reg CHORD_THRESHOLD = 25; //If two notes are played within this many milliseconds then it's a chord
    
    //Get all child sample start constant modulators
    const var modulatorNames = Synth.getIdList("Constant"); //Get child constant modulator names
    const var startModulators = []; //For offsetting sample start position
    
    for (modName in modulatorNames)
    {
    	if (Engine.matchesRegex(modName, "(?=.*tart)(?=.*ffset)")) //Sample start offset
    	{
    		startModulators.push(Synth.getModulator(modName));
    	}
    }
    
    //GUI
    
    Content.setHeight(150);
    
    const var btnBypass = Content.addButton("Bypass", 0, 10);
    btnBypass.set("radioGroup", 1);
    
    const var btnLegato = Content.addButton("Legato", 150, 10);
    btnLegato.set("radioGroup", 1);
    
    const var btnGlide = Content.addButton("Glide", 300, 10);
    btnGlide.set("radioGroup", 1);
    
    const var btnTrill = Content.addButton("Trill", 450, 10);
    btnTrill.set("radioGroup", 1);
    
    const var btnWholeStep = Content.addButton("Whole Step Glide", 600, 10);
    btnWholeStep.set("tooltip", "When active each step of a glide will be a whole tone rather than chromatic.");
    
    const var knbBendTm = Content.addKnob("Bend Time", 0, 50);
    knbBendTm.setRange(-50, 50, 0.1);
    knbBendTm.set("suffix", "ms");
    knbBendTm.set("tooltip", "The pitch bend lasts the same duration as the crossfade time by default but can be adjusted with this knob. If the bend time ends up being less than 0 it will automatically (behind the scenes) be set to 10ms.");
    
    const var knbMinBend = Content.addKnob("Min Bend", 150, 50);
    knbMinBend.setRange(0, 100, 1);
    knbMinBend.set("suffix", "ct");
    knbMinBend.set("tooltip", "The amount of pitch bend in cents (ct) for an interval of 1 semitone");
    
    const var knbMaxBend = Content.addKnob("Max Bend", 300, 50);
    knbMaxBend.setRange(0, 100, 1);
    knbMaxBend.set("suffix", "ct");
    knbMaxBend.set("tooltip", "The amount of pitch bend in cents (ct) for an interval of 12 semitones");
    
    const var knbFadeTm = Content.addKnob("Fade Time", 450, 50);
    knbFadeTm.setRange(10, 500, 0.1);
    knbFadeTm.set("suffix", "ms");
    knbFadeTm.set("tooltip", "Maximum crossfade time in milliseconds, the actual time used will vary based on playing speed and velocity.");
    
    const knbFadeOutRatio = Content.addKnob("Fade Out Ratio", 600, 50);
    knbFadeOutRatio.setRange(0, 100, 1);
    knbFadeOutRatio.set("defaultValue", 100);
    knbFadeOutRatio.set("text", "Fade Out Ratio");
    knbFadeOutRatio.set("suffix", "%");
    knbFadeOutRatio.set("tooltip", "Shortens the fade out time to a percentage of the fade time. 100% = the same as fade in time.");
    
    const var knbOffset = Content.addKnob("SS Offset", 0, 100);
    knbOffset.set("tooltip", "The value to set sample start constant modulators to during a legato phrase.");
    
    const var knbRate = Content.addKnob("Rate", 150, 100);
    knbRate.set("mode", "TempoSync");
    knbRate.set("max", 11);
    knbRate.set("tooltip", "Rate for glide and trill timer relative to current tempo. If velocity is selected then the glide time will be based on the played velocity (doesn't apply to trills which will instead play at maximum speed)");
    
    //FUNCTIONS
    /**
     * Sets the sample start offset constant modulators to the given value.
     * @param {number} value Constant modulator value between 0 and 1
     */
    inline function setSSOffset(value)
    {
    	if (startModulators.length > 0)
    	{
    		for (mod in startModulators)
    		{
    			mod.setIntensity(value);
    		}
    	}
    }
    
    /**
     * A lookup table is used for pitch bending. This function fills that lookup table based on the min bend and max bend values.
     * @param  {number} minBend The amount of bend for an interval of 1 semitone
     * @param  {number} maxBend The amount of bend for an interval of 12 semitones
      */
    inline function updateBendLookupTable(minBend, maxBend)
    {
    	for (i = 0; i < 12; i++) //Each semitone
    	{
    		bendLookup[i] = ((i + 1) * (maxBend - minBend)) / 12 + minBend;
    	}
    }
    
    /**
     * The fade time to be used when crossfading legato notes
     * @param  {number} interval Distance between the two notes that will be crossfaded
     * @param  {number} velocity Velocity of one of the note that will be faded in, a higher velocity = shorter fade time
     * @return {number}          Crossfade time in ms
     */
    inline function getFadeTime(interval, velocity)
    {
    	local timeDif = (Engine.getUptime() - lastTime) * 1000; //Get time difference between now and last note
    	local fadeTime = timeDif; //Default fade time is played time difference
    
    	if (timeDif <= knbFadeTm.getValue() * 0.5) fadeTime = knbFadeTm.getValue() * 0.5; //Fade time minimum is 50% of knbFadeTm.getValue() - when playing fast
    	if (timeDif >= knbFadeTm.getValue()) fadeTime = knbFadeTm.getValue();
    
    	fadeTime = fadeTime + (interval * 2); //Adjust the fade time based on the interval size
    
        if (velocity > 64) fadeTime = fadeTime - (fadeTime * 0.2); //If a harder velocity is played reduce the fade time by 20%
    
    	return fadeTime;
    }
    
    /**
     * Returns the timer rate for glides and trills.
     * @param  {number} interval [The distance between the two notes that will be glided/trilled]
     * @param  {number} velocity [A velocity value for velocity based glide rates]
     * @return {number}          [Timer rate]
     */
    inline function getRate(interval, velocity)
    {
    	reg rate = knbRate.getValue(); //Get rate knob value
    
    	if (btnTrill.getValue()) //Trill
    	{
    		if (rate > knbRate.get("max")) rate = max-1; //Cap rate at max rate
    		rate = Engine.getMilliSecondsForTempo(rate) / 1000;
    	}
    	else //Glide
    	{
    		//If rate knob is set to the maximum then the actual rate will be determined by velocity
    		if (rate == knbRate.get("max"))
    		{
    			rate = Math.floor((velocity / (knbRate.get("max") - 1)));
    
    			if (rate > knbRate.get("max")) rate = max-1; //Cap rate at max rate
    		}
    
    		rate = (Engine.getMilliSecondsForTempo(rate) / 1000) / interval; //Get rate based on host tempo and selected knob value		
    	}
    
    	if (rate < 0.04) rate = 0.04; //Cap lowest rate at timer's minimum
    
    	return rate;
    }
    
    function onNoteOn()
    {
    	if (!btnBypass.getValue())
    	{
    		Synth.stopTimer();
    
    		if ((Engine.getUptime() - lastTime) * 1000 > CHORD_THRESHOLD) //Not a chord
    		{
    			Message.ignoreEvent(true);
    
    			if (lastNote != -1) //First note of phrase has already been played
    			{
    				interval = Math.abs(Message.getNoteNumber() - lastNote); //Get played interval
    				fadeTime = getFadeTime(interval, Message.getVelocity()); //Get fade time
    				bendTime = fadeTime + knbBendTm.getValue(); //Get bend time
    				if (bendTime < 10) bendTime = 10; //Bend time can't be less than 10ms
    
    				//Get bend amount
    				interval > 12 ? bendAmount = bendLookup[11] : bendAmount = bendLookup[interval - 1]; //Get bend amount from lookup table
    				if (lastNote > Message.getNoteNumber()) bendAmount = -bendAmount; //Invert bend amount for down interval
    
    				setSSOffset(knbOffset.getValue()); //Set sample start offset modulators
    
    				if (btnGlide.getValue() || btnTrill.getValue()) //Glide mode 
    				{
    					count = 0; //Reset count, for trills
    					notes[0] = lastNote; //Origin
    					notes[1] = Message.getNoteNumber(); //Target
    					glideNote = lastNote; //First glide note is the same as the origin
    					lastVelo = Message.getVelocity();
    
    					rate = getRate(Math.abs(notes[0] - notes[1]), lastVelo);
    
    					Synth.startTimer(rate);
    				}
    				else //Legato mode
    				{
    					Synth.addVolumeFade(lastEventId, fadeTime / 100 * knbFadeOutRatio.getValue(), -100); //Fade out old note
    					Synth.addPitchFade(lastEventId, bendTime / 100 * knbFadeOutRatio.getValue(), 0, Message.getFineDetune() + bendAmount); //Pitch fade old note
    
    					retriggerNote = lastNote;
    
    					lastEventId = Synth.playNote(Message.getNoteNumber() + Message.getTransposeAmount(), Message.getVelocity()); //Play new note
    					Synth.addPitchFade(lastEventId, 0, Message.getCoarseDetune(), Message.getFineDetune()); //Pass on any message detuning to new note
    
    					Synth.addVolumeFade(lastEventId, 0, -99); //Set new note's initial volume
    					Synth.addVolumeFade(lastEventId, fadeTime, 0); //Fade in new note
    					Synth.addPitchFade(lastEventId, 0, Message.getCoarseDetune(), Message.getFineDetune() - bendAmount); //Set new note's initial detuning
    					Synth.addPitchFade(lastEventId, bendTime, Message.getCoarseDetune(), Message.getFineDetune()); //Pitch fade new note to 0 (or fineDetune)		
    				}
    			}
    			else //First note of phrase
    			{
    				lastEventId = Synth.playNote(Message.getNoteNumber() + Message.getTransposeAmount(), Message.getVelocity()); //Play new note
    				Synth.addPitchFade(lastEventId, 0, Message.getCoarseDetune(), Message.getFineDetune()); //Pass on any message detuning to new note
    			}
    
    			lastNote = Message.getNoteNumber();
    			lastVelo = Message.getVelocity();
    			lastTime = Engine.getUptime();
    		}
    	}
    }
    
    function onNoteOff()
    {
    	if (!btnBypass.getValue())
    	{
    		Synth.stopTimer();
    
    		if (Message.getNoteNumber() == retriggerNote)
    		{
    			retriggerNote = -1;
    		}
    
    		if (Message.getNoteNumber() == lastNote)
    		{
    			Message.ignoreEvent(true);
    
    			if (retriggerNote != -1)
    			{
    				Synth.addVolumeFade(lastEventId, fadeTime / 100 * knbFadeOutRatio.getValue(), -100); //Fade out old note
    				Synth.addPitchFade(lastEventId, bendTime / 100 * knbFadeOutRatio.getValue(), 0, Message.getFineDetune() + bendAmount); //Pitch fade old note
    
    				lastEventId = Synth.playNote(retriggerNote, lastVelo);
    				Synth.addVolumeFade(lastEventId, 0, -99); //Set new note's initial volume
    				Synth.addVolumeFade(lastEventId, fadeTime, 0); //Fade in new note
    
    				lastNote = retriggerNote;
    				retriggerNote = -1;
    			}
    			else
    			{
    				Synth.noteOffByEventId(lastEventId);
    				lastEventId = -1;
    				lastNote = -1;
    				setSSOffset(1); //Reset sample start offset modulators
    			}
    		}
    	}
    	else //Script is bypassed
    	{
    		//Turn off any hanging notes
    		if (lastEventId != -1)
    		{
    			Synth.noteOffByEventId(lastEventId);
    			lastEventId = -1;
    			lastNote = -1;
    			setSSOffset(1); //Reset sample start offset modulators
    		}
    	}
    }
    
    function onController()
    {
    	
    }
    function onTimer()
    {
    	if (!btnBypass.getValue())
    	{
    		if (btnGlide.getValue()) //Glide
    		{
    			notes[1] > notes[0] ? glideNote++ : glideNote--; //Increment/decrement the glideNote number by 1 (a half step)
    
    			//If the whole step button is enabled then increment/decrement the glideNote by another half step
    			if (btnWholeStep.getValue())
    			{
    				notes[1] > notes[0] ? glideNote++ : glideNote--;
    			}
    
    			//If glide has not completed - i.e. it hasn't reached the target note yet
    			if (lastEventId != -1 && notes[0] != -1 && ((notes[1] > notes[0] && glideNote <= notes[1]) || (notes[1] < notes[0] && glideNote >= notes[1])))
    			{
    				glideBend = 100;
    				if (notes[0] > notes[1]) glideBend = -glideBend;
    			}
    			else 
    			{
    				notes[0] = notes[1]; //Origin becomes target
    				glideNote = notes[1];
    				Synth.stopTimer();
    			}
    		}
    		else if (btnTrill.getValue()) //Trill
    		{
    			count = 1-count; //Toggle count - to switch between origin and target notes
    			glideNote = notes[count];
    			glideBend = bendAmount; //Trill uses same bend settings as normal legato
    		}
    
    		if (Synth.isTimerRunning()) //Timer may have been stopped if glide target reached, so check before proceeding
    		{
    			Synth.addPitchFade(lastEventId, rate*1000, 0, glideBend); //Pitch fade old note to bend amount
    			Synth.addVolumeFade(lastEventId, rate*1000, -100); //Fade out last note
    
    			lastEventId = Synth.playNote(glideNote, lastVelo); //Play new note
    
    			Synth.addVolumeFade(lastEventId, 0, -99); //Set new note's initial volume
    			Synth.addVolumeFade(lastEventId, rate*1000, 0); //Fade in new note
    			Synth.addPitchFade(lastEventId, 0, 0, -glideBend); //Set new note's initial detuning
    			Synth.addPitchFade(lastEventId, rate*1000, 0, 0); //Pitch fade new note to 0
    		}
    	}
    }
    
    function onControl(number, value)
    {
    	switch (number)
    	{
    		case btnBypass: case btnLegato: case btnGlide: case btnTrill:
    			Synth.stopTimer();
    		break;
    
    		case knbMaxBend:
    			updateBendLookupTable(knbMinBend.getValue(), knbMaxBend.getValue()); //Update the bend amount lookup table
    		break;
    
    		case knbRate:
    			//If timer's already started then update its Rate
    			if (Synth.isTimerRunning())
    			{
    				rate = getRate(Math.abs(notes[0] - notes[1]), lastVelo);
    				Synth.startTimer(rate);
    			} 
    
    			knbRate.set("text", "Rate"); //Default
    			if (knbRate.getValue() == knbRate.get("max"))
    			{
    				knbRate.set("text", "Velocity");
    			}
    		break;
    	}
    }


  • Nice! The glide time is a bit too long for my taste (or maybe I did not find a way to make them shorter). Also if playing fast passages, the glide mode ignores some notes.



  • The rate knob controls the glide time. I intended it for the odd glide between legato notes rather than playing repeated glides in a row.



  • You mean something like the octave step in "Somewhere over the rainbow"? 🙂



  • Not really, more for a smooth portamento on a violin, you just need to play the first and last note of the glide and it will smoothly go between them.



  • I added an update 😄 same note legato is now possible.

    /**
     * Title: Synthetic Legato v3.5.1
     * Author: David Healey
     * Date: 27/01/2017
     * Modified: 11/04/2017
     * License: GPLv3 - https://www.gnu.org/licenses/gpl-3.0.en.html
    */
    
    reg lastNote = -1;
    reg lastEventId = -1;
    reg retriggerNote = -1;		
    reg lastVelo = 0;
    reg lastTime;
    reg interval;
    reg fadeTime;
    
    reg bendAmount = 0;
    reg bendLookup = []; //Bend amount lookup table - generated on init
    
    reg glideBend; //The bend amount for glides, either 100 or -100 depending on if an up or down glide
    reg glideNote; //Currently sounding note during a glide/trill
    reg rate; //Timer rate for glide/trill
    reg notes = []; //Origin and target notes for glide/trill
    reg count; //Counter for switching between origin and target notes for trill
    
    reg CHORD_THRESHOLD = 25; //If two notes are played within this many milliseconds then it's a chord
    
    //Get all child sample start constant modulators
    const var modulatorNames = Synth.getIdList("Constant"); //Get child constant modulator names
    const var startModulators = []; //For offsetting sample start position
    
    for (modName in modulatorNames)
    {
    	if (Engine.matchesRegex(modName, "(?=.*tart)(?=.*ffset)")) //Sample start offset
    	{
    		startModulators.push(Synth.getModulator(modName));
    	}
    }
    
    //GUI
    
    Content.setHeight(150);
    
    const var btnBypass = Content.addButton("Bypass", 0, 10);
    btnBypass.set("radioGroup", 1);
    
    const var btnLegato = Content.addButton("Legato", 150, 10);
    btnLegato.set("radioGroup", 1);
    
    const var btnGlide = Content.addButton("Glide", 300, 10);
    btnGlide.set("radioGroup", 1);
    
    const var btnTrill = Content.addButton("Trill", 450, 10);
    btnTrill.set("radioGroup", 1);
    
    const var btnWholeStep = Content.addButton("Whole Step Glide", 600, 10);
    btnWholeStep.set("tooltip", "When active each step of a glide will be a whole tone rather than chromatic.");
    
    const var knbBendTm = Content.addKnob("Bend Time", 0, 50);
    knbBendTm.setRange(-50, 50, 0.1);
    knbBendTm.set("suffix", "ms");
    knbBendTm.set("tooltip", "The pitch bend lasts the same duration as the crossfade time by default but can be adjusted with this knob. If the bend time ends up being less than 0 it will automatically (behind the scenes) be set to 10ms.");
    
    const var knbMinBend = Content.addKnob("Min Bend", 150, 50);
    knbMinBend.setRange(0, 100, 1);
    knbMinBend.set("suffix", "ct");
    knbMinBend.set("tooltip", "The amount of pitch bend in cents (ct) for an interval of 1 semitone");
    
    const var knbMaxBend = Content.addKnob("Max Bend", 300, 50);
    knbMaxBend.setRange(0, 100, 1);
    knbMaxBend.set("suffix", "ct");
    knbMaxBend.set("tooltip", "The amount of pitch bend in cents (ct) for an interval of 12 semitones");
    
    const var knbFadeTm = Content.addKnob("Fade Time", 450, 50);
    knbFadeTm.setRange(10, 500, 0.1);
    knbFadeTm.set("suffix", "ms");
    knbFadeTm.set("tooltip", "Maximum crossfade time in milliseconds, the actual time used will vary based on playing speed and velocity.");
    
    const knbFadeOutRatio = Content.addKnob("Fade Out Ratio", 600, 50);
    knbFadeOutRatio.setRange(0, 100, 1);
    knbFadeOutRatio.set("defaultValue", 100);
    knbFadeOutRatio.set("text", "Fade Out Ratio");
    knbFadeOutRatio.set("suffix", "%");
    knbFadeOutRatio.set("tooltip", "Shortens the fade out time to a percentage of the fade time. 100% = the same as fade in time.");
    
    const var knbOffset = Content.addKnob("SS Offset", 0, 100);
    knbOffset.set("tooltip", "The value to set sample start constant modulators to during a legato phrase.");
    
    const var knbRate = Content.addKnob("Rate", 150, 100);
    knbRate.set("mode", "TempoSync");
    knbRate.set("max", 11);
    knbRate.set("tooltip", "Rate for glide and trill timer relative to current tempo. If velocity is selected then the glide time will be based on the played velocity (doesn't apply to trills which will instead play at maximum speed)");
    
    const var btnSameNote = Content.addButton("Same Note Legato", 300, 110);
    btnSameNote.set("tooltip", "When active releasing a note in normal legato mode will retrigger the note that was released with a transition.");
    
    //FUNCTIONS
    /**
     * Sets the sample start offset constant modulators to the given value.
     * @param {number} value Constant modulator value between 0 and 1
     */
    inline function setSSOffset(value)
    {
    	if (startModulators.length > 0)
    	{
    		for (mod in startModulators)
    		{
    			mod.setIntensity(value);
    		}
    	}
    }
    
    /**
     * A lookup table is used for pitch bending. This function fills that lookup table based on the min bend and max bend values.
     * @param  {number} minBend The amount of bend for an interval of 1 semitone
     * @param  {number} maxBend The amount of bend for an interval of 12 semitones
      */
    inline function updateBendLookupTable(minBend, maxBend)
    {
    	for (i = 0; i < 12; i++) //Each semitone
    	{
    		bendLookup[i] = ((i + 1) * (maxBend - minBend)) / 12 + minBend;
    	}
    }
    
    /**
     * The fade time to be used when crossfading legato notes
     * @param  {number} interval Distance between the two notes that will be crossfaded
     * @param  {number} velocity Velocity of one of the note that will be faded in, a higher velocity = shorter fade time
     * @return {number}          Crossfade time in ms
     */
    inline function getFadeTime(interval, velocity)
    {
    	local timeDif = (Engine.getUptime() - lastTime) * 1000; //Get time difference between now and last note
    	local fadeTime = timeDif; //Default fade time is played time difference
    
    	if (timeDif <= knbFadeTm.getValue() * 0.5) fadeTime = knbFadeTm.getValue() * 0.5; //Fade time minimum is 50% of knbFadeTm.getValue() - when playing fast
    	if (timeDif >= knbFadeTm.getValue()) fadeTime = knbFadeTm.getValue();
    
    	fadeTime = fadeTime + (interval * 2); //Adjust the fade time based on the interval size
    
        if (velocity > 64) fadeTime = fadeTime - (fadeTime * 0.2); //If a harder velocity is played reduce the fade time by 20%
    
    	return fadeTime;
    }
    
    /**
     * Returns the timer rate for glides and trills.
     * @param  {number} interval [The distance between the two notes that will be glided/trilled]
     * @param  {number} velocity [A velocity value for velocity based glide rates]
     * @return {number}          [Timer rate]
     */
    inline function getRate(interval, velocity)
    {
    	reg rate = knbRate.getValue(); //Get rate knob value
    
    	if (btnTrill.getValue()) //Trill
    	{
    		if (rate > knbRate.get("max")) rate = max-1; //Cap rate at max rate
    		rate = Engine.getMilliSecondsForTempo(rate) / 1000;
    	}
    	else //Glide
    	{
    		//If rate knob is set to the maximum then the actual rate will be determined by velocity
    		if (rate == knbRate.get("max"))
    		{
    			rate = Math.floor((velocity / (knbRate.get("max") - 1)));
    
    			if (rate > knbRate.get("max")) rate = max-1; //Cap rate at max rate
    		}
    
    		rate = (Engine.getMilliSecondsForTempo(rate) / 1000) / interval; //Get rate based on host tempo and selected knob value		
    	}
    
    	if (rate < 0.04) rate = 0.04; //Cap lowest rate at timer's minimum
    
    	return rate;
    }
    
    function onNoteOn()
    {
    	if (!btnBypass.getValue())
    	{
    		Synth.stopTimer();
    
    		if ((Engine.getUptime() - lastTime) * 1000 > CHORD_THRESHOLD) //Not a chord
    		{
    			Message.ignoreEvent(true);
    
    			if (lastNote != -1) //First note of phrase has already been played
    			{
    				interval = Math.abs(Message.getNoteNumber() - lastNote); //Get played interval
    				fadeTime = getFadeTime(interval, Message.getVelocity()); //Get fade time
    				bendTime = fadeTime + knbBendTm.getValue(); //Get bend time
    				if (bendTime < 10) bendTime = 10; //Bend time can't be less than 10ms
    
    				//Get bend amount
    				bendAmount = 0;
    				if (interval != 0) //Same note legato
    				{
    					interval > 12 ? bendAmount = bendLookup[11] : bendAmount = bendLookup[interval - 1]; //Get bend amount from lookup table
    					if (lastNote > Message.getNoteNumber()) bendAmount = -bendAmount; //Invert bend amount for down interval
    				}
    
    				setSSOffset(knbOffset.getValue()); //Set sample start offset modulators
    
    				if (btnGlide.getValue() || btnTrill.getValue()) //Glide mode 
    				{
    					count = 0; //Reset count, for trills
    					notes[0] = lastNote; //Origin
    					notes[1] = Message.getNoteNumber(); //Target
    					glideNote = lastNote; //First glide note is the same as the origin
    					lastVelo = Message.getVelocity();
    
    					rate = getRate(Math.abs(notes[0] - notes[1]), lastVelo);
    
    					Synth.startTimer(rate);
    				}
    				else //Legato mode
    				{
    					Synth.addVolumeFade(lastEventId, fadeTime / 100 * knbFadeOutRatio.getValue(), -100); //Fade out old note
    					Synth.addPitchFade(lastEventId, bendTime / 100 * knbFadeOutRatio.getValue(), 0, Message.getFineDetune() + bendAmount); //Pitch fade old note
    
    					retriggerNote = lastNote;
    
    					lastEventId = Synth.playNote(Message.getNoteNumber() + Message.getTransposeAmount(), Message.getVelocity()); //Play new note
    					Synth.addPitchFade(lastEventId, 0, Message.getCoarseDetune(), Message.getFineDetune()); //Pass on any message detuning to new note
    
    					Synth.addVolumeFade(lastEventId, 0, -99); //Set new note's initial volume
    					Synth.addVolumeFade(lastEventId, fadeTime, 0); //Fade in new note
    					Synth.addPitchFade(lastEventId, 0, Message.getCoarseDetune(), Message.getFineDetune() - bendAmount); //Set new note's initial detuning
    					Synth.addPitchFade(lastEventId, bendTime, Message.getCoarseDetune(), Message.getFineDetune()); //Pitch fade new note to 0 (or fineDetune)		
    				}
    			}
    			else //First note of phrase
    			{
    				lastEventId = Synth.playNote(Message.getNoteNumber() + Message.getTransposeAmount(), Message.getVelocity()); //Play new note
    				Synth.addPitchFade(lastEventId, 0, Message.getCoarseDetune(), Message.getFineDetune()); //Pass on any message detuning to new note
    			}
    
    			lastNote = Message.getNoteNumber();
    			lastVelo = Message.getVelocity();
    			lastTime = Engine.getUptime();
    		}
    	}
    }
    
    function onNoteOff()
    {
    	if (!btnBypass.getValue())
    	{
    		Synth.stopTimer();
    
    		if (Message.getNoteNumber() == retriggerNote)
    		{
    			retriggerNote = -1;
    		}
    
    		//Legato mode active and same note legato button enabled
    		if (btnLegato.getValue() && btnSameNote.getValue())
    		{
    			retriggerNote = lastNote; //Retrigger note becomes the last note
    		}
    
    		if (Message.getNoteNumber() == lastNote)
    		{
    			Message.ignoreEvent(true);
    
    			if (retriggerNote != -1)
    			{
    				Synth.addVolumeFade(lastEventId, fadeTime / 100 * knbFadeOutRatio.getValue(), -100); //Fade out old note
    				Synth.addPitchFade(lastEventId, bendTime / 100 * knbFadeOutRatio.getValue(), 0, Message.getFineDetune() + bendAmount); //Pitch fade old note
    
    				lastEventId = Synth.playNote(retriggerNote, lastVelo);
    				Synth.addVolumeFade(lastEventId, 0, -99); //Set new note's initial volume
    				Synth.addVolumeFade(lastEventId, fadeTime, 0); //Fade in new note
    
    				lastNote = retriggerNote;
    				retriggerNote = -1;
    			}
    			else
    			{
    				Synth.noteOffByEventId(lastEventId);
    				lastEventId = -1;
    				lastNote = -1;
    				setSSOffset(1); //Reset sample start offset modulators
    			}
    		}
    	}
    	else //Script is bypassed
    	{
    		//Turn off any hanging notes
    		if (lastEventId != -1)
    		{
    			Synth.noteOffByEventId(lastEventId);
    			lastEventId = -1;
    			lastNote = -1;
    			setSSOffset(1); //Reset sample start offset modulators
    		}
    	}
    }
    
    function onController()
    {
    }
    
    function onTimer()
    {
    	if (!btnBypass.getValue())
    	{
    		if (btnGlide.getValue()) //Glide
    		{
    			notes[1] > notes[0] ? glideNote++ : glideNote--; //Increment/decrement the glideNote number by 1 (a half step)
    
    			//If the whole step button is enabled then increment/decrement the glideNote by another half step
    			if (btnWholeStep.getValue())
    			{
    				notes[1] > notes[0] ? glideNote++ : glideNote--;
    			}
    
    			//If glide has not completed - i.e. it hasn't reached the target note yet
    			if (lastEventId != -1 && notes[0] != -1 && ((notes[1] > notes[0] && glideNote <= notes[1]) || (notes[1] < notes[0] && glideNote >= notes[1])))
    			{
    				glideBend = 100;
    				if (notes[0] > notes[1]) glideBend = -glideBend;
    			}
    			else 
    			{
    				notes[0] = notes[1]; //Origin becomes target
    				glideNote = notes[1];
    				Synth.stopTimer();
    			}
    		}
    		else if (btnTrill.getValue()) //Trill
    		{
    			count = 1-count; //Toggle count - to switch between origin and target notes
    			glideNote = notes[count];
    			glideBend = bendAmount; //Trill uses same bend settings as normal legato
    		}
    
    		if (Synth.isTimerRunning()) //Timer may have been stopped if glide target reached, so check before proceeding
    		{
    			Synth.addPitchFade(lastEventId, rate*1000, 0, glideBend); //Pitch fade old note to bend amount
    			Synth.addVolumeFade(lastEventId, rate*1000, -100); //Fade out last note
    
    			lastEventId = Synth.playNote(glideNote, lastVelo); //Play new note
    
    			Synth.addVolumeFade(lastEventId, 0, -99); //Set new note's initial volume
    			Synth.addVolumeFade(lastEventId, rate*1000, 0); //Fade in new note
    			Synth.addPitchFade(lastEventId, 0, 0, -glideBend); //Set new note's initial detuning
    			Synth.addPitchFade(lastEventId, rate*1000, 0, 0); //Pitch fade new note to 0
    		}
    	}
    }
    
    function onControl(number, value)
    {
    	switch (number)
    	{
    		case btnBypass: case btnLegato: case btnGlide: case btnTrill:
    			Synth.stopTimer();
    		break;
    
    		case knbMaxBend:
    			updateBendLookupTable(knbMinBend.getValue(), knbMaxBend.getValue()); //Update the bend amount lookup table
    		break;
    
    		case knbFadeTm:
    			fadeTime = value; //Default fade time
    		break;
    
    		case knbRate:
    			//If timer's already started then update its Rate
    			if (Synth.isTimerRunning())
    			{
    				rate = getRate(Math.abs(notes[0] - notes[1]), lastVelo);
    				Synth.startTimer(rate);
    			} 
    
    			knbRate.set("text", "Rate"); //Default
    			if (knbRate.getValue() == knbRate.get("max"))
    			{
    				knbRate.set("text", "Velocity");
    			}
    		break;
    
    		case btnSameNote:
    
    			if (value == 0 && !Synth.isKeyDown(lastNote) && lastEventId != -1)
    			{
    				Synth.noteOffByEventId(lastEventId);
    				lastEventId = -1;
    				lastNote = -1;
    			}
    
    			retriggerNote = -1;
    
    		break;
    	}
    }


  • Here's a play test from an instrument I'm working on - another woodwind library 😛 - I've set it up so the front end script controls the legato script, I have the sustain pedal activating the same note legato or the glide depending on if I'm playing a legato transition or not.



  • This is great for acoustic and woodwind sounds. I wonder how this would sound with traditional Synth lead and synth bass sounds. Could this be tweaked to accommodate the traditional Moog type synth legato and portamento functions?

    Also does it work with polyphonic material?

    That's one of the things missing from Hise is a good Poly/Mono portamento glide function. That was very smart to include a formant shaper to it, so it sounds realistic.



  • It's monophonic only although it automatically detects when a chord is played and switches to a standard sustain polyphonic mode. The glide works by playing all of the samples in between the first and last note so it retains the natural formants and only does a pitch bend between each semi-tone. I'm not sure how this would work with a synth but you can try it out and let me know 🙂



  • After careful consideration I am open to the possibility of dual licensing my GPL snippets for use in proprietary projects, pm/email me to discuss this further if you're interested.



  • I've added an update to this script, just improving some of the code around the glide rate stuff, nothing major. I'm also posting an example project (HISE snippet) below - you'll need to add your own samples, and give them some s.mod time for the start offset to work.

    /**
     * Title: Synthetic Legato v3.5.3
     * Author: David Healey
     * Date: 27/01/2017
     * Modified: 02/07/2017
     * License: GPLv3 - https://www.gnu.org/licenses/gpl-3.0.en.html
    */
    
    reg lastNote = -1;
    reg lastEventId = -1;
    reg retriggerNote = -1;		
    reg lastVelo = 0;
    reg lastTime;
    reg interval;
    reg fadeTime;
    
    reg bendAmount = 0;
    reg bendLookup = []; //Bend amount lookup table - generated on init
    
    reg glideBend; //The bend amount for glides, either 100 or -100 depending on if an up or down glide
    reg glideNote; //Currently sounding note during a glide/trill
    reg rate; //Timer rate for glide/trill
    reg notes = []; //Origin and target notes for glide/trill
    reg count; //Counter for switching between origin and target notes for trill
    
    reg CHORD_THRESHOLD = 25; //If two notes are played within this many milliseconds then it's a chord
    
    //Get all child sample start constant modulators
    const var modulatorNames = Synth.getIdList("Constant"); //Get child constant modulator names
    const var startModulators = []; //For offsetting sample start position
    
    for (modName in modulatorNames)
    {
    	if (Engine.matchesRegex(modName, "(?=.*tart)(?=.*ffset)")) //Sample start offset
    	{
    		startModulators.push(Synth.getModulator(modName));
    	}
    }
    
    //GUI
    
    Content.setHeight(150);
    
    const var btnBypass = Content.addButton("Bypass", 0, 10);
    btnBypass.set("radioGroup", 1);
    
    const var btnLegato = Content.addButton("Legato", 150, 10);
    btnLegato.set("radioGroup", 1);
    
    const var btnGlide = Content.addButton("Glide", 300, 10);
    btnGlide.set("radioGroup", 1);
    
    const var btnTrill = Content.addButton("Trill", 450, 10);
    btnTrill.set("radioGroup", 1);
    
    const var btnWholeStep = Content.addButton("Whole Step Glide", 600, 10);
    btnWholeStep.set("tooltip", "When active each step of a glide will be a whole tone rather than chromatic.");
    
    const var knbBendTm = Content.addKnob("Bend Time", 0, 50);
    knbBendTm.setRange(-50, 50, 0.1);
    knbBendTm.set("suffix", "ms");
    knbBendTm.set("tooltip", "The pitch bend lasts the same duration as the crossfade time by default but can be adjusted with this knob. If the bend time ends up being less than 0 it will automatically (behind the scenes) be set to 10ms.");
    
    const var knbMinBend = Content.addKnob("Min Bend", 150, 50);
    knbMinBend.setRange(0, 100, 1);
    knbMinBend.set("suffix", "ct");
    knbMinBend.set("tooltip", "The amount of pitch bend in cents (ct) for an interval of 1 semitone");
    
    const var knbMaxBend = Content.addKnob("Max Bend", 300, 50);
    knbMaxBend.setRange(0, 100, 1);
    knbMaxBend.set("suffix", "ct");
    knbMaxBend.set("tooltip", "The amount of pitch bend in cents (ct) for an interval of 12 semitones");
    
    const var knbFadeTm = Content.addKnob("Fade Time", 450, 50);
    knbFadeTm.setRange(10, 500, 0.1);
    knbFadeTm.set("suffix", "ms");
    knbFadeTm.set("tooltip", "Maximum crossfade time in milliseconds, the actual time used will vary based on playing speed and velocity.");
    
    const knbFadeOutRatio = Content.addKnob("Fade Out Ratio", 600, 50);
    knbFadeOutRatio.setRange(0, 100, 1);
    knbFadeOutRatio.set("defaultValue", 100);
    knbFadeOutRatio.set("text", "Fade Out Ratio");
    knbFadeOutRatio.set("suffix", "%");
    knbFadeOutRatio.set("tooltip", "Shortens the fade out time to a percentage of the fade time. 100% = the same as fade in time.");
    
    const var knbOffset = Content.addKnob("SS Offset", 0, 100);
    knbOffset.set("tooltip", "The value to set sample start constant modulators to during a legato phrase.");
    
    const var knbRate = Content.addKnob("Rate", 150, 100);
    knbRate.set("mode", "TempoSync");
    knbRate.set("max", 11);
    knbRate.set("tooltip", "Rate for glide and trill timer relative to current tempo. If velocity is selected then the glide time will be based on the played velocity (doesn't apply to trills which will instead play at maximum speed)");
    
    const var btnSameNote = Content.addButton("Same Note Legato", 300, 110);
    btnSameNote.set("tooltip", "When active releasing a note in normal legato mode will retrigger the note that was released with a transition.");
    
    //FUNCTIONS
    /**
     * Sets the sample start offset constant modulators to the given value.
     * @param {number} value Constant modulator value between 0 and 1
     */
    inline function setSSOffset(value)
    {
    	if (startModulators.length > 0)
    	{
    		for (mod in startModulators)
    		{
    			mod.setIntensity(value);
    		}
    	}
    }
    
    /**
     * A lookup table is used for pitch bending. This function fills that lookup table based on the min bend and max bend values.
     * @param  {number} minBend The amount of bend for an interval of 1 semitone
     * @param  {number} maxBend The amount of bend for an interval of 12 semitones
      */
    inline function updateBendLookupTable(minBend, maxBend)
    {
    	for (i = 0; i < 12; i++) //Each semitone
    	{
    		bendLookup[i] = ((i + 1) * (maxBend - minBend)) / 12 + minBend;
    	}
    }
    
    /**
     * The fade time to be used when crossfading legato notes
     * @param  {number} interval Distance between the two notes that will be crossfaded
     * @param  {number} velocity Velocity of one of the note that will be faded in, a higher velocity = shorter fade time
     * @return {number}          Crossfade time in ms
     */
    inline function getFadeTime(interval, velocity)
    {
    	local timeDif = (Engine.getUptime() - lastTime) * 1000; //Get time difference between now and last note
    	local fadeTime = timeDif; //Default fade time is played time difference
    
    	if (timeDif <= knbFadeTm.getValue() * 0.5) fadeTime = knbFadeTm.getValue() * 0.5; //Fade time minimum is 50% of knbFadeTm.getValue() - when playing fast
    	if (timeDif >= knbFadeTm.getValue()) fadeTime = knbFadeTm.getValue();
    
    	fadeTime = fadeTime + (interval * 2); //Adjust the fade time based on the interval size
    
        if (velocity > 64) fadeTime = fadeTime - (fadeTime * 0.2); //If a harder velocity is played reduce the fade time by 20%
    
    	return fadeTime;
    }
    
    /**
     * Returns the timer rate for glides and trills.
     * @param  {number} interval [The distance between the two notes that will be glided/trilled]
     * @param  {number} velocity [A velocity value for velocity based glide rates]
     * @return {number}          [Timer rate]
     */
    inline function getRate(interval, velocity)
    {
    	reg rate = knbRate.getValue(); //Get rate knob value
    
    	//If rate knob is set to the maximum then the actual rate will be determined by velocity
    	if (rate == knbRate.get("max"))
    	{
    		rate = Math.min(knbRate.get("max")-1, Math.floor((velocity / (knbRate.get("max") - 1)))); //Capped rate at max rate
    	}
    
    	rate = Engine.getMilliSecondsForTempo(rate) / 1000; //Rate to milliseconds for timer
    
    	if (btnGlide.getValue()) rate = rate / interval; //For glides rate is per step
    
    	if (rate < 0.04) rate = 0.04; //Cap lowest rate at timer's minimum
    
    	return rate;
    }
    
    function onNoteOn()
    {
    	if (!btnBypass.getValue())
    	{
    		Synth.stopTimer();
    
    		if ((Engine.getUptime() - lastTime) * 1000 > CHORD_THRESHOLD) //Not a chord
    		{
    			Message.ignoreEvent(true);
    
    			if (lastNote != -1) //First note of phrase has already been played
    			{
    				interval = Math.abs(Message.getNoteNumber() - lastNote); //Get played interval
    				fadeTime = getFadeTime(interval, Message.getVelocity()); //Get fade time
    				bendTime = fadeTime + knbBendTm.getValue(); //Get bend time
    				if (bendTime < 10) bendTime = 10; //Bend time can't be less than 10ms
    
    				//Get bend amount
    				bendAmount = 0;
    				if (interval != 0) //Same note legato
    				{
    					interval > 12 ? bendAmount = bendLookup[11] : bendAmount = bendLookup[interval - 1]; //Get bend amount from lookup table
    					if (lastNote > Message.getNoteNumber()) bendAmount = -bendAmount; //Invert bend amount for down interval
    				}
    
    				setSSOffset(knbOffset.getValue()); //Set sample start offset modulators
    
    				if (btnGlide.getValue() || btnTrill.getValue()) //Glide mode 
    				{
    					count = 0; //Reset count, for trills
    					notes[0] = lastNote; //Origin
    					notes[1] = Message.getNoteNumber(); //Target
    					glideNote = lastNote; //First glide note is the same as the origin
    					lastVelo = Message.getVelocity();
    
    					rate = getRate(Math.abs(notes[0] - notes[1]), lastVelo);
    
    					Synth.startTimer(rate);
    				}
    				else //Legato mode
    				{
    					Synth.addVolumeFade(lastEventId, fadeTime / 100 * knbFadeOutRatio.getValue(), -100); //Fade out old note
    					Synth.addPitchFade(lastEventId, bendTime / 100 * knbFadeOutRatio.getValue(), 0, Message.getFineDetune() + bendAmount); //Pitch fade old note
    
    					retriggerNote = lastNote;
    
    					lastEventId = Synth.playNote(Message.getNoteNumber() + Message.getTransposeAmount(), Message.getVelocity()); //Play new note
    					Synth.addPitchFade(lastEventId, 0, Message.getCoarseDetune(), Message.getFineDetune()); //Pass on any message detuning to new note
    
    					Synth.addVolumeFade(lastEventId, 0, -99); //Set new note's initial volume
    					Synth.addVolumeFade(lastEventId, fadeTime, 0); //Fade in new note
    					Synth.addPitchFade(lastEventId, 0, Message.getCoarseDetune(), Message.getFineDetune() - bendAmount); //Set new note's initial detuning
    					Synth.addPitchFade(lastEventId, bendTime, Message.getCoarseDetune(), Message.getFineDetune()); //Pitch fade new note to 0 (or fineDetune)		
    				}
    			}
    			else //First note of phrase
    			{
    				lastEventId = Synth.playNote(Message.getNoteNumber() + Message.getTransposeAmount(), Message.getVelocity()); //Play new note
    				Synth.addPitchFade(lastEventId, 0, Message.getCoarseDetune(), Message.getFineDetune()); //Pass on any message detuning to new note
    			}
    
    			lastNote = Message.getNoteNumber();
    			lastVelo = Message.getVelocity();
    			lastTime = Engine.getUptime();
    		}
    	}
    }
    
    function onNoteOff()
    {
    	if (!btnBypass.getValue())
    	{
    		Synth.stopTimer();
    
    		if (Message.getNoteNumber() == retriggerNote)
    		{
    			retriggerNote = -1;
    		}
    
    		//Legato mode active and same note legato button enabled
    		if (btnLegato.getValue() && btnSameNote.getValue())
    		{
    			retriggerNote = lastNote; //Retrigger note becomes the last note
    		}
    
    		if (Message.getNoteNumber() == lastNote)
    		{
    			Message.ignoreEvent(true);
    
    			if (retriggerNote != -1)
    			{
    				Synth.addVolumeFade(lastEventId, fadeTime / 100 * knbFadeOutRatio.getValue(), -100); //Fade out old note
    				Synth.addPitchFade(lastEventId, bendTime / 100 * knbFadeOutRatio.getValue(), 0, Message.getFineDetune() + bendAmount); //Pitch fade old note
    
    				lastEventId = Synth.playNote(retriggerNote, lastVelo);
    				Synth.addVolumeFade(lastEventId, 0, -99); //Set new note's initial volume
    				Synth.addVolumeFade(lastEventId, fadeTime, 0); //Fade in new note
    
    				lastNote = retriggerNote;
    				retriggerNote = -1;
    			}
    			else
    			{
    				Synth.noteOffByEventId(lastEventId);
    				lastEventId = -1;
    				lastNote = -1;
    				setSSOffset(1); //Reset sample start offset modulators
    			}
    		}
    	}
    	else //Script is bypassed
    	{
    		//Turn off any hanging notes
    		if (lastEventId != -1)
    		{
    			Synth.noteOffByEventId(lastEventId);
    			lastEventId = -1;
    			lastNote = -1;
    			setSSOffset(1); //Reset sample start offset modulators
    		}
    	}
    }
    
    function onController()
    {
    	
    }
    function onTimer()
    {
    	if (!btnBypass.getValue())
    	{
    		if (btnGlide.getValue()) //Glide
    		{
    			notes[1] > notes[0] ? glideNote++ : glideNote--; //Increment/decrement the glideNote number by 1 (a half step)
    
    			//If the whole step button is enabled then increment/decrement the glideNote by another half step
    			if (btnWholeStep.getValue())
    			{
    				notes[1] > notes[0] ? glideNote++ : glideNote--;
    			}
    
    			//If glide has not completed - i.e. it hasn't reached the target note yet
    			if (lastEventId != -1 && notes[0] != -1 && ((notes[1] > notes[0] && glideNote <= notes[1]) || (notes[1] < notes[0] && glideNote >= notes[1])))
    			{
    				glideBend = 100;
    				if (notes[0] > notes[1]) glideBend = -glideBend;
    			}
    			else 
    			{
    				notes[0] = notes[1]; //Origin becomes target
    				glideNote = notes[1];
    				Synth.stopTimer();
    			}
    		}
    		else if (btnTrill.getValue()) //Trill
    		{
    			count = 1-count; //Toggle count - to switch between origin and target notes
    			glideNote = notes[count];
    			glideBend = bendAmount; //Trill uses same bend settings as normal legato
    		}
    
    		if (Synth.isTimerRunning()) //Timer may have been stopped if glide target reached, so check before proceeding
    		{
    			Synth.addPitchFade(lastEventId, rate*1000, 0, glideBend); //Pitch fade old note to bend amount
    			Synth.addVolumeFade(lastEventId, rate*1000, -100); //Fade out last note
    
    			lastEventId = Synth.playNote(glideNote, lastVelo); //Play new note
    
    			Synth.addVolumeFade(lastEventId, 0, -99); //Set new note's initial volume
    			Synth.addVolumeFade(lastEventId, rate*1000, 0); //Fade in new note
    			Synth.addPitchFade(lastEventId, 0, 0, -glideBend); //Set new note's initial detuning
    			Synth.addPitchFade(lastEventId, rate*1000, 0, 0); //Pitch fade new note to 0
    		}
    	}
    }
    
    function onControl(number, value)
    {
    	switch (number)
    	{
    		case btnBypass: case btnLegato: case btnGlide: case btnTrill:
    			Synth.stopTimer();
    			btnSameNote.setValue(0);
    		break;
    
    		case knbMaxBend:
    			updateBendLookupTable(knbMinBend.getValue(), knbMaxBend.getValue()); //Update the bend amount lookup table
    		break;
    
    		case knbFadeTm:
    			fadeTime = value; //Default fade time
    		break;
    
    		case knbRate:
    			//If timer's already started then update its Rate
    			if (Synth.isTimerRunning())
    			{
    				rate = getRate(Math.abs(notes[0] - notes[1]), lastVelo);
    				Synth.startTimer(rate);
    			} 
    
    			knbRate.set("text", "Rate"); //Default
    			if (knbRate.getValue() == knbRate.get("max"))
    			{
    				knbRate.set("text", "Velocity");
    			}
    		break;
    
    		case btnSameNote:
    
    			if (value == 0 && !Synth.isKeyDown(lastNote) && lastEventId != -1)
    			{
    				Synth.noteOffByEventId(lastEventId);
    				lastEventId = -1;
    				lastNote = -1;
    			}
    
    			retriggerNote = -1;
    
    		break;
    	}
    }
    
    HiseSnippet 5482.3oc67rsbabcjfRB1lvNYs85J693wrhsADIwERQoXckhj5BKSIRSPQmMpTbFLyA.ypAyfclAfBJQU1Oh8CY2+f7x9Er+H9OX2t6y0Yv.RPYIEksBqxxXNW5S28oueNybPbjKOIIJtzBe7QSFxKsvmTt8jvz9a22wOrzt6TZg+gxA7dNoQ26ENCFFvKs0jgNIIbuRKrvEe.NnEV7Rkn+9o6rkSfSnK2zToRGG46x2yefepo0+vlemePv8c73G4OvZzWYycciB2NJHZDfPWrbyRCcbetSO9icvgcgxkF6yOIozBMKu9Z0iedqNu7t0w+1tt5u814206JANO+ji2qd8i815Z8wVK4NJNlGldLL8RKTdg+W3uEJeOO+zn31oNob.lk2JxaR69QmDJV5i8S76.zK7PqRsAbRz78iB7PhG+8tgo73tNtbqoUZ699AdGn3qIk.HefgKeQAW9KJ+HeOec6Ft8mRcvLyvlcuvExhxWJCJ25rQ4VEfdkrvtKIvN.Ih7FE.a5YvLb2V1geTXFLC4DgI9oSrkFNGnay23n6mU9.+T29EiuWn.7E1ndaiuxM+eQ460sK2M0frWp78+sut6zudnxGMCs8Ot71QgovS732sZ5KbwSihmK0w2ST8Z9+GT8JjatfE9VQiu.SCkYhiBB.gFDeqTdPj2OzmyCNaL8II7ibHTwz1tgi4wo3jLRWlk3wiFzAVHqw2dPTTZ+rRX9atCuqynfzicBFY09AaZfDsv63j5TZgup7ZWQ5BIX8eyUPGJi2G9mtKiM8s3yC2eJKDWb9L.+2sh8VBU9BAp74kamFycF3G1qMEjhPLbwxsGkf1xd2ZI6fXdPjiWa+WZMr+xlaMBXWwYa8+bSZot6fnQgYVKIYbHeH2IE18yfdGd3ChiFML+r9o6PxJGECwLAbB6N1ODCQIMi9TbTRRWfbHXkX20Aih6kU46PNnPljsMPMD15CC4AXvSKr.zj.qejyPJVs7AXUY9TVfoefC.UqQtkeOKPisj0dn0P0jEoaa4+v1z1O3Ll2MJdf07ZySSAlVxoGI2G7dSjbmo8YoUkOqba2X+goldPb6ijwzmAmDCrzE9rMJ23xWtB6xri7SC3WmQgHvS8cY6QyhMd85aTeMbD2cTZ+n3qy1wYruG6gbm.9DrcvnJLw0tVilsZrVyVWCaC1u765y8tNq4ZMZdMc66AZ.gIvvevA6Mdc1pr9ooCStdiFmbxI06ENpdTbuFAhAkzn2vfUWudy57v58SGDT4xMpTIl2iE3jj93nTN6VrUacCcS2aLD1+tdVsFySi860C7inG8hKpG+wftKzXSCDP0cwS9X39icBDO0UZJ3Fh0uCOzSnQZlN11dQQOezPnsm9ravZzXKnIliXbAhtRQIUfr6wgft.9lGKJDVK+TAf6E36wwogS+n9bBpJP.RwhAjrBi6C6RwrVMaxfVWE++df8iPOPtl.YWlSHCVPnWOPRSLQyZf7CbM1VjqTvDVBrFzrCQVk2nX72NhQ2.3hAABNpiXhH2HldxfWVCCARhlQrOrI3GBXjGv..6Mox9KZltHsRnF9CXMvAkbBZsCwnN7zS37PftlMHE.if11Ob+C24GO5gGdu1Ob+81Avn01.A9tcYomDImiSLmMLvYBra.qCrLL3eRXCbBmvF.fxOgCl88RflgU1O8afovbAcAuJUZz3AvZ6DD.M.pnrDx1ECbEEmBzRH7CXqafxaeREpM1XmXSiXFuHyhz8pCzxtd64mjVcoskyeoZHNiKjXQlFtrPDFV.mP.cPFlsh6CiMpa2DgIvrn6vHH9BvDakJHWrJ.bDy.wybnZsJ+wJKBRXUuWHrGvqOvA1b3IGx6wegZVqvVp5ctU8Ki.tF8KZQqsTsZ.Vz1dYEnSkEAftXNzt9vQI8qp4K5NTqRsZ2nxhupxqn8gmrakJX7df7bc.fOj62qeZ0VazDFjEmoSZnvRHvSTC2wyaqQooQgUWRz2RqvZtBndASUOdDnUWJ1wyOh7iBio0TfVZ0rPPK5Cm1FV.Wz5bA7Gf5JECapKXRq2zBzTiyEjOBUYJFxTWvjthMRSMNWP9G5GA9lS4CKF5T2LpeEIbUaRPOcwhkFEEj5iqDLSPYzwM0eLmwcb6CRS.Ph5pLZA5x.I0gCOeBsHv5wQKVngyTHZFPYJNBjc8cquTVz94gcPivGMHKN+cgQc.ADzjLZ9SHiPhW5IfX4gNg83UWcCpSXL0akeDUWJABPz+EHYLHYoo61hLQu.CQqeBeAnWJxPDp5R1oonhXNhFcUgDwRALj0YB3VfxLh0YDX8.HZjg38uBwJKs1Ir08bfzpyPqhJmNz74nUOvKRGNZrH.BpPv4ZBVAE7WmQoBdHXBbBqZGNX9zSfefSbvVAtf.MAbeXOcPRAr5G4GR7zB30PWLrOkNihYKmhgaSRLMExeY62lW6ltTA8miYK81BBRVrc.O.xA37UcSqQNYbB0wHfisEPjPNDfHVADnyKlIA57BEARZtZBTLkYSfl9Kl.s6+MCAtllBSllDobkJTcA6QotbE68PwTLTXKpOaEFyHJTgwtaKRDHc+AiFjWU.8hY4KeERFELeLBHOZ.iRHMBPlFnoIrNNIhnyvHCHWkC4PCX.Gig3FcgLwskkk3y9i.pATHmIm.F.iFgxPmM6PM6YtsmePUWxypvGKQCdViLk+hTj+jCMl0vML7uZliwhqC4yDiEnfXqDWOBVBhwBZ9Nrg7XT9xoGGElzCBGPcDq+JfiospAFyndwHwvALs719TLCEwka2lI5T4BWxPDMVnBwXj4gnIBxyJ.Nbb5niEYVwF1OFjVJ.MOzIkWDRhsahCPhgXiB7CVMNgb7ACifnebWZpQ3faMsZkucKJ6vLwlKhTl7xmJBcmi4SOlna4IlvRw0ibCnDwYfugDd.2EcWPg+haRBHRatJmrZ0EreYvzZfT0KhmD9MPbxCGBdIfUjvjDvwrOX.hfgOv13NdzbYN.GWpFSpc0VZp3JfPH4xr5JHrBrWF0sNnKQjQp3JTS+TCq.3QbmDwVMkVDHPFFEO.LYH24wcJA9qS0j3.znAWkfWRPXV.GkCWGf5cBEgZKjYfvxexi29nc2+wsqHSGuM23lOenxyRrj1Z.7NTHQWGgylCchcFv9igTgTekTVe6oSfPzgJ8plj7RK.BMp3GF.Q4y5NJzkB0.vf1sEpSUoYoyGHez6A7vd.AeaVyZhv6U4UfLxbCFFAMjEgtwMEc8NkqADnODouLXeAO5tYypFDUIa33hX7rA6d0YGgw3nIftjrGs6jA.YjgG3GJy9F9OPXT7.gKI1bVCqcfLNlr9XoocpALTLvjwLLm.yx4bEVQaZiF5A1C1RWjBpjYUkn7Jpki1IoMIepxFLe1MAfC++kWFya6dTb1J7l1uL083o9OClTUXpKC9q.hpphHVUwavj+PjcYUC2H6N5Q19EPQ5NJ+xnVoxitHVTR+iRfuPFnl8riOJp6ZjswcWSx+BkToULcLCdEBSsAsiU+.38XVERWZVZ8R.RvBvkU.s99P9nf4AMPtEKgbZFaHYZUAKIihCMqp9usmNhljB0PgTkUEutphOrhdgoMY3mx.e1AzbukNWdXpOYH1b0Zv1lplX3tI3mpopLDz564iE4layZCiNgzWv4QbC0BoJfF5kWrlHnjmZi0VNnlJcdjaIpHrvnP3adKSTmHNSw+TEQyl02nl8xM6gQkCQuxf.I4uAvfMfnQfczBm4pBQQUPgcABMKlc6hWxyDm.2.KZMD8OWlo2BAzdMpRP2kRfKaTTYsdomRh+KAdGJ8fHoV161rqdkLnj9mqxpp+MxlDq3tXZ08ch8rEfMaVwbuQt77HzD1ZM+JfrjBzlhnZT3Oj5R3oKsfRJlXhaYFVc0T5SQiGdmCkcZA7Dkdj68rSWi+o207agqRDG0MIX9h3hPJH4Ymtp7SM0O8YyREFCfaVpupRwJDln3+rDkjZoz.vL6EXLrSPajllof6RUgNnh3RGlmL6HZ7JllGGvGPWAnVX+UgSBU.ABkAiDQpVS59WhwOxIsec.FUmdfq1ZEQ2cAOywUMBrMXELZPXsUM3OpbwPrknjHtDhvGoei9WpnVXiMtGgoB1VjJ38iho.sI7mbPIszQQPiA4YWCXp.y3lmzhjtLa155x0i9eMLmkfr9qRIapWTGhGS0tphEW7lflWyqnAD9fjHgPVNgmjpITBW9lDk8Ki5FUodTUSKSEEhQ7teXUc.aeoo3lVnuXyRT00jzngjrpvBEMq4yWAXiIW02wHH.LPW7bY.eOhmj.4EV2uGDaMmNDmpowTHeX23JpOvmuDOCGDN22OV5hgJjAk.FXfBrWDDCoQ.ZjboYZNtPhUZQswBoPnSmjppkGnFbID23.MEgMoUnjV6TPg.okMzhc6ZAeUXCUqoAow0OBLLXpoc.XJN3z535Z0InOTfTAiahEQkYAxVM0mJEYf10AyICTpME1CKQGw0WzB7h3O0Hn8IeoVTMiE1fZJqvuLdHQjZzHEaBlcgaiwBdmrGmlU7jsZ8L10mYuZn.FAdVF1g57xhiFjI.e4paKOca1L19qkccW07D4NjtzJSc5bzIskQ53UBlocJSlZQXoxg.sc9pOHy1y5viLaxSa0g8m9S5J4mwXDvYH+RTtp16Ct58QzXGWjYIzzJlSQKQLRx64Sahw2qXcly0ydHsvgLCVJcrgzY0Ilg9fHyAUgpsvWpHq6jLkFB+cj0JacTtEpqILin89n7rpM.nItUYJhn1J5CHVOck8PXmQXPjbWbC41L9u7.vFTiF6YJMfM2VLeGOuiiBFMfiVJpZcp0qXz3IOPfUz70byrotBcju0zQvh0ZKJvSFxclUitmHSuXZyByyh0LiUr6C192A7wDhBcKaomP3CsdxJ.pvHI+O2gwq2yqX1GMGguf.PKt3XloU5kswriv5pLLJQdIaPTe1VeO.KzTH+jyASKKeX6Hm3DMmXlrHwhgm2HdLM3QJKFGFJ0nPLIBLOVEdLmxJ.lr529sZyFpoCwAfWl.evp3XZlmSQO.vFgJrXWu8YOfVWNInYPPJ104S990eGyHGqPFbipIqJXarqdz0VbQiE.5ejVAJJ.ESfH+UWV+8.Qcs6QqqyyrbcTYtryKGjLbmoiR0tFh4iJta2etgEOqsMHinLl9zU5rfamTEIOIiWDU4nwTgSxEWEdtto.UvCwHb7pnCOPdsBrhO3q+Z65lmkxJFgr8Jent91zp2ARGZ.W3L1pZOBr+L3F53pOGY.jE0DoAXTmdm5a88OWqmp0jLbtLw07l2SyOeGMZ5QJBlA6EnbwpMFiu4EKBEp2aMQhE1XjjKL8sGLGZnZxNP9V0LwMeFwsKQOx1ixAg3JXhA21QdwLEFXffjwz2AfPlPgrx5otVdIULounPWsp.QxyKEWHAOM89ZStEXk0bU7EFZgts5UZLcdr.OyptHSzQwKzYjbalND+6Xt9iKuLjbo9oUWUjWmaLe.vVZ3wk+xbfmDqQTIOr1WsXUwRhFzkJdSMRxUbwBgIHtwQzMRRZfF1mk1nkWivybof0vAPb7LCzKSES9elaFUVK4RY+yK0abGSDgH8KrfJvrgzBw8a7LfWk4WmWGu9OPeX4Chw6dkfnruIlrIhz7JTbEcEoQJcKUqVDNCcXXI27VlTzv7cMy3lyXF21ZF1rG8MskpKhUkLzf41VKk8nW0bIcyFyWddOktrBFVWCVsqSStv1oBqmQE6rNsB2vXLQrtR4ghR7mZSoOnR2u0p56X6QQ85AxohdVktBDmHO4zS8Z1VoPbl.i.usYXYKch3lFNJAX.TrLTMTjWG0DFIuYc.61QTHXE9IDi3vQgX.kRxjJl9.GzZ4XtnzeHKCKHruRVVh+R40UXIQL3GtOGFd2H7F.i2ZdtmHCCKioy1QOV.fKik5j7RpI4Y4qVbdlYpm1Y5uzZIlNpDSbWUNqf.zaV1A.LUhAUdSGPv4g9lclmyQtJHRkcCXNxh77s+17TSK7T84UU30XEl45RH0yj8H8r4hEwV6465L0yhX4MOSt4LORpTWuxLsVj65tHrOzj5qCnN7bJJaBXlaNHAthuz.V2eR6HYst0g4Jr4SHvXtZoE7JQTHpHNiTBSrpxNwCK7ziKDHXs9ttw2r7zRTmR.EAixkrfbAeaI3ciiqbeMC6NFy8u1EV7LJq3qXjFY1K3k7V7Q2frZVrAE1N8wANyijSg+Et.p7pWx3wIGy0Rr5557zDmMJrhMQ+veoh28c7I6DcRntv6T1nEFF6aqP2Ew1TTtCFJC0em5kSdNeU1bEWBMqAFEtKXzY+gb8yhieS0RyR5ROX2jINY6VIYiBFlooBd6IkWLtRkV3WHGNLzT5cE6WJeWwDWXtR9d3KJlvtSowh2h1xKX+x3d5yTXgRMSyKi37L6OrLYO67ureXYxx24ehed47u1A4w7SANswwGSv4iKqeY.xCfe8lyC.pTVcC2yO+aOuyWdAxyO+Gu47Q.5qmcd.76mK.7okydchyAk+8imSzPeqcy.fMWe8+m6LO.3CJiFvxSBe8lymvP9KKpFNefVX3uge48+PE911GSh6dg3cp.ZQ7JmK8dna8LQ36ll539bcKa9m27Pwsa0zTzl64Gxchy9tLetn04+SZRQuHrEt27OWVf5jzN6szdzq4Kvq7KzQkxJutDF+E5GYZ54M2GYgc3t9c3A4egyUKo02KgmT9pM0ezcDeeDFu+tW8eo62pab79i9cm7nksdd3K+9Gur03y78Un6j2299Jbw2NeeEl2c9OQfahuiFHx9OIZfQsLma++gm7j+64d6u3uiFzhdBtlV6+aTd8qVO6ei2+kSlrLs+eMb+LM29a59kdq+AL5c32lh4ca7yKePTvjg8iB8cuuePp7iSw+nUyLY6y9yTQ0+x8i4+ai3gtFYw+i+qM+damhY0Z+zM+9QNAmgr6qs90BWXNUv9Uk0HN6ug91Qgb+2y7Gr3o7A24yEeqILst9qiOgRkl1nvOcm2be3cdmac+WVV9Rk2FSk98gMwKobpqdaaj1BDGdBglxXde6ir+bzP9jxza2MCiz+uVJyGFMBKP7ibfz2eAjpGHd1NZTrK29yRyEvr1DO2TkNXaH2nlpOCfxNao+F1.c1R0YIWEn.ZS9ag+uKJO3F7yvT.GBT1VMR7dRReDbJI1YGfe3ZtDx4f1Z6LlKLVKVxY9874Q9tGH+9KfpKkuw6Fh9cwZLvwMN5GcE1VP16GQs.TYH8sdbQHKV3YVKUhWn.w.vH2O55hdyWEDIJdFqctmw5m6Ybky8L13bOiqdtmw0N2y32bJy.8nbW4axO3ArTo+O.4OHMd.```


  • Really appreciate!


 

0
Online

353
Users

1.1k
Topics

7.5k
Posts